class Rack::Multipart::Parser - rack-1.6.4 Documentation

# class Rack::Multipart::Parser

### Constants

BUFSIZE
CHARSET
DUMMY
TEXT_PLAIN

### Public Class Methods

create(env) click to toggle source

    # File lib/rack/multipart/parser.rb, line 12
    def self.create(env)
      return DUMMY unless env['CONTENT_TYPE'] =~ MULTIPART

      io = env['rack.input']
      io.rewind

      content_length = env['CONTENT_LENGTH']
      content_length = content_length.to_i if content_length

      tempfile = env['rack.multipart.tempfile_factory'] ||
        lambda { |filename, content_type| Tempfile.new(["RackMultipart", ::File.extname(filename)]) }
      bufsize = env['rack.multipart.buffer_size'] || BUFSIZE

      new($1, io, content_length, env, tempfile, bufsize)
    end

new(boundary, io, content_length, env, tempfile, bufsize) click to toggle source

    # File lib/rack/multipart/parser.rb, line 28
    def initialize(boundary, io, content_length, env, tempfile, bufsize)
      @buf            = ""

      if @buf.respond_to? :force_encoding
        @buf.force_encoding Encoding::ASCII_8BIT
      end

      @params         = Utils::KeySpaceConstrainedParams.new
      @boundary       = "--#{boundary}"
      @io             = io
      @content_length = content_length
      @boundary_size  = Utils.bytesize(@boundary) + EOL.size
      @env = env
      @tempfile       = tempfile
      @bufsize        = bufsize

      if @content_length
        @content_length -= @boundary_size
      end

      @rx = /(?:#{EOL})?#{Regexp.quote(@boundary)}(#{EOL}|--)/n
      @full_boundary = @boundary + EOL
    end

### Public Instance Methods

parse() click to toggle source

    # File lib/rack/multipart/parser.rb, line 52
    def parse
      fast_forward_to_first_boundary

      opened_files = 0
      loop do

        head, filename, content_type, name, body =
          get_current_head_and_filename_and_content_type_and_name_and_body

        if Utils.multipart_part_limit > 0
          opened_files += 1 if filename
          raise MultipartPartLimitError, 'Maximum file multiparts in content reached' if opened_files >= Utils.multipart_part_limit
        end

        # Save the rest.
        if i = @buf.index(rx)
          body << @buf.slice!(0, i)
          @buf.slice!(0, @boundary_size+2)

          @content_length = -1  if $1 == "--"
        end

        get_data(filename, body, content_type, name, head) do |data|
          tag_multipart_encoding(filename, content_type, name, data)

          Utils.normalize_params(@params, name, data)
        end

        # break if we're at the end of a buffer, but not if it is the end of a field
        break if (@buf.empty? && $1 != EOL) || @content_length == -1
      end

      @io.rewind

      @params.to_params_hash
    end

### Private Instance Methods

fast_forward_to_first_boundary() click to toggle source

    # File lib/rack/multipart/parser.rb, line 94
    def fast_forward_to_first_boundary
      loop do
        content = @io.read(@bufsize)
        raise EOFError, "bad content body" unless content
        @buf << content

        while @buf.gsub!(/\A([^\n]*\n)/, '')
          read_buffer = $1
          return if read_buffer == full_boundary
        end

        raise EOFError, "bad content body" if Utils.bytesize(@buf) >= @bufsize
      end
    end

full_boundary() click to toggle source

    # File lib/rack/multipart/parser.rb, line 90
    def full_boundary; @full_boundary; end

get_current_head_and_filename_and_content_type_and_name_and_body() click to toggle source

    # File lib/rack/multipart/parser.rb, line 109
    def get_current_head_and_filename_and_content_type_and_name_and_body
      head = nil
      body = ''

      if body.respond_to? :force_encoding
        body.force_encoding Encoding::ASCII_8BIT
      end

      filename = content_type = name = nil

      until head && @buf =~ rx
        if !head && i = @buf.index(EOL+EOL)
          head = @buf.slice!(0, i+2) # First \r\n

          @buf.slice!(0, 2)          # Second \r\n

          content_type = head[MULTIPART_CONTENT_TYPE, 1]
          name = head[MULTIPART_CONTENT_DISPOSITION, 1] || head[MULTIPART_CONTENT_ID, 1]

          filename = get_filename(head)

          if name.nil? || name.empty? && filename
            name = filename
          end

          if filename
            (@env['rack.tempfiles'] ||= []) << body = @tempfile.call(filename, content_type)
            body.binmode  if body.respond_to?(:binmode)
          end

          next
        end

        # Save the read body part.
        if head && (@boundary_size+4 < @buf.size)
          body << @buf.slice!(0, @buf.size - (@boundary_size+4))
        end

        content = @io.read(@content_length && @bufsize >= @content_length ? @content_length : @bufsize)
        raise EOFError, "bad content body"  if content.nil? || content.empty?

        @buf << content
        @content_length -= content.size if @content_length
      end

      [head, filename, content_type, name, body]
    end

get_data(filename, body, content_type, name, head) { |data| ... } click to toggle source

    # File lib/rack/multipart/parser.rb, line 226
    def get_data(filename, body, content_type, name, head)
      data = body
      if filename == ""
        # filename is blank which means no file has been selected
        return
      elsif filename
        body.rewind if body.respond_to?(:rewind)

        # Take the basename of the upload's original filename.
        # This handles the full Windows paths given by Internet Explorer
        # (and perhaps other broken user agents) without affecting
        # those which give the lone filename.
        filename = filename.split(/[\/\]/).last

        data = {:filename => filename, :type => content_type,
                :name => name, :tempfile => body, :head => head}
      elsif !filename && content_type && body.is_a?(IO)
        body.rewind

        # Generic multipart cases, not coming from a form
        data = {:type => content_type,
                :name => name, :tempfile => body, :head => head}
      end

      yield data
    end

get_filename(head) click to toggle source

    # File lib/rack/multipart/parser.rb, line 157
    def get_filename(head)
      filename = nil
      case head
      when RFC2183
        filename = Hash[head.scan(DISPPARM)]['filename']
        filename = $1 if filename and filename =~ /^"(.*)"$/
      when BROKEN_QUOTED, BROKEN_UNQUOTED
        filename = $1
      end

      return unless filename

      if filename.scan(/%.?.?/).all? { |s| s =~ /%[0-9a-fA-F]{2}/ }
        filename = Utils.unescape(filename)
      end

      scrub_filename filename

      if filename !~ /\[^\"]/
        filename = filename.gsub(/\(.)/, '\1')
      end
      filename
    end

rx() click to toggle source

    # File lib/rack/multipart/parser.rb, line 92
    def rx; @rx; end

scrub_filename(filename) click to toggle source

    # File lib/rack/multipart/parser.rb, line 182
    def scrub_filename(filename)
      unless filename.valid_encoding?
        # FIXME: this force_encoding is for Ruby 2.0 and 1.9 support.
        # We can remove it after they are dropped
        filename.force_encoding(Encoding::ASCII_8BIT)
        filename.encode!(:invalid => :replace, :undef => :replace)
      end
    end

tag_multipart_encoding(filename, content_type, name, body) click to toggle source

    # File lib/rack/multipart/parser.rb, line 194
    def tag_multipart_encoding(filename, content_type, name, body)
      name.force_encoding Encoding::UTF_8

      return if filename

      encoding = Encoding::UTF_8

      if content_type
        list         = content_type.split(';')
        type_subtype = list.first
        type_subtype.strip!
        if TEXT_PLAIN == type_subtype
          rest         = list.drop 1
          rest.each do |param|
            k,v = param.split('=', 2)
            k.strip!
            v.strip!
            encoding = Encoding.find v if k == CHARSET
          end
        end
      end

      name.force_encoding encoding
      body.force_encoding encoding
    end