A multipart form data parser, adapted from IOWA.
Usually, Rack::Request#POST takes care of calling this.
# File lib/rack/utils.rb, line 596
596: def self.build_multipart(params, first = true)
597: if first
598: unless params.is_a?(Hash)
599: raise ArgumentError, "value must be a Hash"
600: end
601:
602: multipart = false
603: query = lambda { |value|
604: case value
605: when Array
606: value.each(&query)
607: when Hash
608: value.values.each(&query)
609: when UploadedFile
610: multipart = true
611: end
612: }
613: params.values.each(&query)
614: return nil unless multipart
615: end
616:
617: flattened_params = Hash.new
618:
619: params.each do |key, value|
620: k = first ? key.to_s : "[#{key}]"
621:
622: case value
623: when Array
624: value.map { |v|
625: build_multipart(v, false).each { |subkey, subvalue|
626: flattened_params["#{k}[]#{subkey}"] = subvalue
627: }
628: }
629: when Hash
630: build_multipart(value, false).each { |subkey, subvalue|
631: flattened_params[k + subkey] = subvalue
632: }
633: else
634: flattened_params[k] = value
635: end
636: end
637:
638: if first
639: flattened_params.map { |name, file|
640: if file.respond_to?(:original_filename)
641: ::File.open(file.path, "rb") do |f|
642: f.set_encoding(Encoding::BINARY) if f.respond_to?(:set_encoding)
643: --#{MULTIPART_BOUNDARY}\rContent-Disposition: form-data; name="#{name}"; filename="#{Utils.escape(file.original_filename)}"\rContent-Type: #{file.content_type}\rContent-Length: #{::File.stat(file.path).size}\r\r#{f.read}\r
644: end
645: else
646: --#{MULTIPART_BOUNDARY}\rContent-Disposition: form-data; name="#{name}"\r\r#{file}\r
647: end
648: }.join + "--#{MULTIPART_BOUNDARY}--\r"
649: else
650: flattened_params
651: end
652: end
# File lib/rack/utils.rb, line 474
474: def self.parse_multipart(env)
475: unless env['CONTENT_TYPE'] =~
476: %\Amultipart/.*boundary=\"?([^\";,]+)\"?|
477: nil
478: else
479: boundary = "--#{$1}"
480:
481: params = {}
482: buf = ""
483: content_length = env['CONTENT_LENGTH'].to_i
484: input = env['rack.input']
485: input.rewind
486:
487: boundary_size = Utils.bytesize(boundary) + EOL.size
488: bufsize = 16384
489:
490: content_length -= boundary_size
491:
492: read_buffer = ''
493:
494: status = input.read(boundary_size, read_buffer)
495: raise EOFError, "bad content body" unless status == boundary + EOL
496:
497: rx = /(?:#{EOL})?#{Regexp.quote boundary}(#{EOL}|--)/
498:
499: loop {
500: head = nil
501: body = ''
502: filename = content_type = name = nil
503:
504: until head && buf =~ rx
505: if !head && i = buf.index(EOL+EOL)
506: head = buf.slice!(0, i+2) # First \r\n
507: buf.slice!(0, 2) # Second \r\n
508:
509: token = /[^\s()<>,;:\\"\/\[\]?=]+/
510: condisp = /Content-Disposition:\s*#{token}\s*/
511: dispparm = /;\s*(#{token})=("(?:\\"|[^"])*"|#{token})*/
512:
513: rfc2183 = /^#{condisp}(#{dispparm})+$/
514: broken_quoted = /^#{condisp}.*;\sfilename="(.*?)"(?:\s*$|\s*;\s*#{token}=)/
515: broken_unquoted = /^#{condisp}.*;\sfilename=(#{token})/
516:
517: if head =~ rfc2183
518: filename = Hash[head.scan(dispparm)]['filename']
519: filename = $1 if filename and filename =~ /^"(.*)"$/
520: elsif head =~ broken_quoted
521: filename = $1
522: elsif head =~ broken_unquoted
523: filename = $1
524: end
525:
526: if filename && filename !~ /\\[^\\"]/
527: filename = Utils.unescape(filename).gsub(/\\(.)/, '\1')
528: end
529:
530: content_type = head[/Content-Type: (.*)#{EOL}/i, 1]
531: name = head[/Content-Disposition:.*\s+name="?([^\";]*)"?/i, 1] || head[/Content-ID:\s*([^#{EOL}]*)/i, 1]
532:
533: if filename
534: body = Tempfile.new("RackMultipart")
535: body.binmode if body.respond_to?(:binmode)
536: end
537:
538: next
539: end
540:
541: # Save the read body part.
542: if head && (boundary_size+4 < buf.size)
543: body << buf.slice!(0, buf.size - (boundary_size+4))
544: end
545:
546: c = input.read(bufsize < content_length ? bufsize : content_length, read_buffer)
547: raise EOFError, "bad content body" if c.nil? || c.empty?
548: buf << c
549: content_length -= c.size
550: end
551:
552: # Save the rest.
553: if i = buf.index(rx)
554: body << buf.slice!(0, i)
555: buf.slice!(0, boundary_size+2)
556:
557: content_length = 1 if $1 == "--"
558: end
559:
560: if filename == ""
561: # filename is blank which means no file has been selected
562: data = nil
563: elsif filename
564: body.rewind
565:
566: # Take the basename of the upload's original filename.
567: # This handles the full Windows paths given by Internet Explorer
568: # (and perhaps other broken user agents) without affecting
569: # those which give the lone filename.
570: filename = filename.split(/[\/\\]/).last
571:
572: data = {:filename => filename, :type => content_type,
573: :name => name, :tempfile => body, :head => head}
574: elsif !filename && content_type
575: body.rewind
576:
577: # Generic multipart cases, not coming from a form
578: data = {:type => content_type,
579: :name => name, :tempfile => body, :head => head}
580: else
581: data = body
582: end
583:
584: Utils.normalize_params(params, name, data) unless data.nil?
585:
586: # break if we're at the end of a buffer, but not if it is the end of a field
587: break if (buf.empty? && $1 != EOL) || content_length == 1
588: }
589:
590: input.rewind
591:
592: params
593: end
594: end
Disabled; run with --debug to generate this.
Generated with the Darkfish Rdoc Generator 1.1.6.