module Rack::Utils::Multipart

A multipart form data parser, adapted from IOWA.

Usually, Rack::Request#POST takes care of calling this.

Constants

EOL
MULTIPART_BOUNDARY

Public Class Methods

build_multipart(params, first = true) click to toggle source
# File lib/rack/utils.rb, line 642
      def self.build_multipart(params, first = true)
        if first
          unless params.is_a?(Hash)
            raise ArgumentError, "value must be a Hash"
          end

          multipart = false
          query = lambda { |value|
            case value
            when Array
              value.each(&query)
            when Hash
              value.values.each(&query)
            when UploadedFile
              multipart = true
            end
          }
          params.values.each(&query)
          return nil unless multipart
        end

        flattened_params = Hash.new

        params.each do |key, value|
          k = first ? key.to_s : "[#{key}]"

          case value
          when Array
            value.map { |v|
              build_multipart(v, false).each { |subkey, subvalue|
                flattened_params["#{k}[]#{subkey}"] = subvalue
              }
            }
          when Hash
            build_multipart(value, false).each { |subkey, subvalue|
              flattened_params[k + subkey] = subvalue
            }
          else
            flattened_params[k] = value
          end
        end

        if first
          flattened_params.map { |name, file|
            if file.respond_to?(:original_filename)
              ::File.open(file.path, "rb") do |f|
                f.set_encoding(Encoding::BINARY) if f.respond_to?(:set_encoding)
"--#{MULTIPART_BOUNDARY}\r
Content-Disposition: form-data; name="#{name}"; filename="#{Utils.escape(file.original_filename)}"\r
Content-Type: #{file.content_type}\r
Content-Length: #{::File.stat(file.path).size}\r
\r
#{f.read}\r
"
              end
            else
"--#{MULTIPART_BOUNDARY}\r
Content-Disposition: form-data; name="#{name}"\r
\r
#{file}\r
"
            end
          }.join + "--#{MULTIPART_BOUNDARY}--\r"
        else
          flattened_params
        end
      end
parse_multipart(env) click to toggle source
# File lib/rack/utils.rb, line 510
def self.parse_multipart(env)
  unless env['CONTENT_TYPE'] =~
      %r\Amultipart/.*boundary=\"?([^\";,]+)\"?|
    nil
  else
    boundary = "--#{$1}"

    params = {}
    buf = ""
    content_length = env['CONTENT_LENGTH'].to_i
    input = env['rack.input']
    input.rewind

    boundary_size = Utils.bytesize(boundary) + EOL.size
    bufsize = 16384

    content_length -= boundary_size

    read_buffer = ''

    status = input.read(boundary_size, read_buffer)
    raise EOFError, "bad content body"  unless status == boundary + EOL

    rx = %r(?:#{EOL})?#{Regexp.quote boundary}(#{EOL}|--)/

    max_key_space = Utils.key_space_limit
    bytes = 0

    loop {
      head = nil
      body = ''
      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

          token = %r[^\s()<>,;:\\"\/\[\]?=]+/
          condisp = %rContent-Disposition:\s*#{token}\s*/
          dispparm = %r;\s*(#{token})=("(?:\\"|[^"])*"|#{token})*/

          rfc2183 = %r^#{condisp}(#{dispparm})+$/
          broken_quoted = %r^#{condisp}.*;\sfilename="(.*?)"(?:\s*$|\s*;\s*#{token}=)/
          broken_unquoted = %r^#{condisp}.*;\sfilename=(#{token})/

          if head =~ rfc2183
            filename = Hash[head.scan(dispparm)]['filename']
            filename = $1 if filename and filename =~ %r^"(.*)"$/
          elsif head =~ broken_quoted
            filename = $1
          elsif head =~ broken_unquoted
            filename = $1
          end

          if filename && filename !~ %r\\[^\\"]/
            filename = Utils.unescape(filename).gsub(%r\\(.)/, '\1')
          end

          content_type = head[%rContent-Type: (.*)#{EOL}/i, 1]
          name = head[%rContent-Disposition:.*\s+name="?([^\";]*)"?/i, 1] || head[%rContent-ID:\s*([^#{EOL}]*)/i, 1]

          if name
            bytes += name.size
            if bytes > max_key_space
              raise RangeError, "exceeded available parameter key space"
            end
          end

          if content_type || filename
            body = Tempfile.new("RackMultipart")
            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

        c = input.read(bufsize < content_length ? bufsize : content_length, read_buffer)
        raise EOFError, "bad content body"  if c.nil? || c.empty?
        buf << c
        content_length -= c.size
      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

      if filename == ""
        # filename is blank which means no file has been selected
        data = nil
      elsif filename
        body.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(%r[\/\\]/).last

        data = {:filename => filename, :type => content_type,
                :name => name, :tempfile => body, :head => head}
      elsif !filename && content_type
        body.rewind

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

      Utils.normalize_params(params, name, data) unless data.nil?

      # 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
    }

    input.rewind

    params
  end
end