mirror of
https://github.com/github/rails.git
synced 2026-01-25 06:18:04 -05:00
315 lines
9.9 KiB
Ruby
315 lines
9.9 KiB
Ruby
module ActionController
|
|
class RequestParser
|
|
def initialize(env)
|
|
@env = env
|
|
end
|
|
|
|
def request_parameters
|
|
@request_parameters ||= parse_formatted_request_parameters
|
|
end
|
|
|
|
def query_parameters
|
|
@query_parameters ||= self.class.parse_query_parameters(query_string)
|
|
end
|
|
|
|
# Returns the query string, accounting for server idiosyncrasies.
|
|
def query_string
|
|
@env['QUERY_STRING'].present? ? @env['QUERY_STRING'] : (@env['REQUEST_URI'].split('?', 2)[1] || '')
|
|
end
|
|
|
|
# The request body is an IO input stream. If the RAW_POST_DATA environment
|
|
# variable is already set, wrap it in a StringIO.
|
|
def body
|
|
if raw_post = @env['RAW_POST_DATA']
|
|
raw_post.force_encoding(Encoding::BINARY) if raw_post.respond_to?(:force_encoding)
|
|
StringIO.new(raw_post)
|
|
else
|
|
@env['rack.input']
|
|
end
|
|
end
|
|
|
|
# The raw content type string with its parameters stripped off.
|
|
def content_type_without_parameters
|
|
self.class.extract_content_type_without_parameters(content_type_with_parameters)
|
|
end
|
|
|
|
def raw_post
|
|
unless @env.include? 'RAW_POST_DATA'
|
|
@env['RAW_POST_DATA'] = body.read(content_length)
|
|
body.rewind if body.respond_to?(:rewind)
|
|
end
|
|
@env['RAW_POST_DATA']
|
|
end
|
|
|
|
private
|
|
|
|
def parse_formatted_request_parameters
|
|
return {} if content_length.zero?
|
|
|
|
content_type, boundary = self.class.extract_multipart_boundary(content_type_with_parameters)
|
|
|
|
# Don't parse params for unknown requests.
|
|
return {} if content_type.blank?
|
|
|
|
mime_type = Mime::Type.lookup(content_type)
|
|
strategy = ActionController::Base.param_parsers[mime_type]
|
|
|
|
# Only multipart form parsing expects a stream.
|
|
body = (strategy && strategy != :multipart_form) ? raw_post : self.body
|
|
|
|
case strategy
|
|
when Proc
|
|
strategy.call(body)
|
|
when :url_encoded_form
|
|
self.class.clean_up_ajax_request_body! body
|
|
self.class.parse_query_parameters(body)
|
|
when :multipart_form
|
|
self.class.parse_multipart_form_parameters(body, boundary, content_length, @env)
|
|
when :xml_simple, :xml_node
|
|
body.blank? ? {} : Hash.from_xml(body).with_indifferent_access
|
|
when :yaml
|
|
YAML.load(body)
|
|
when :json
|
|
if body.blank?
|
|
{}
|
|
else
|
|
data = ActiveSupport::JSON.decode(body)
|
|
data = {:_json => data} unless data.is_a?(Hash)
|
|
data.with_indifferent_access
|
|
end
|
|
else
|
|
{}
|
|
end
|
|
rescue Exception => e # YAML, XML or Ruby code block errors
|
|
raise
|
|
{ "body" => body,
|
|
"content_type" => content_type_with_parameters,
|
|
"content_length" => content_length,
|
|
"exception" => "#{e.message} (#{e.class})",
|
|
"backtrace" => e.backtrace }
|
|
end
|
|
|
|
def content_length
|
|
@content_length ||= @env['CONTENT_LENGTH'].to_i
|
|
end
|
|
|
|
# The raw content type string. Use when you need parameters such as
|
|
# charset or boundary which aren't included in the content_type MIME type.
|
|
# Overridden by the X-POST_DATA_FORMAT header for backward compatibility.
|
|
def content_type_with_parameters
|
|
content_type_from_legacy_post_data_format_header || @env['CONTENT_TYPE'].to_s
|
|
end
|
|
|
|
def content_type_from_legacy_post_data_format_header
|
|
if x_post_format = @env['HTTP_X_POST_DATA_FORMAT']
|
|
case x_post_format.to_s.downcase
|
|
when 'yaml'; 'application/x-yaml'
|
|
when 'xml'; 'application/xml'
|
|
end
|
|
end
|
|
end
|
|
|
|
class << self
|
|
def parse_query_parameters(query_string)
|
|
return {} if query_string.blank?
|
|
|
|
pairs = query_string.split('&').collect do |chunk|
|
|
next if chunk.empty?
|
|
key, value = chunk.split('=', 2)
|
|
next if key.empty?
|
|
value = value.nil? ? nil : CGI.unescape(value)
|
|
[ CGI.unescape(key), value ]
|
|
end.compact
|
|
|
|
UrlEncodedPairParser.new(pairs).result
|
|
end
|
|
|
|
def parse_request_parameters(params)
|
|
parser = UrlEncodedPairParser.new
|
|
|
|
params = params.dup
|
|
until params.empty?
|
|
for key, value in params
|
|
if key.blank?
|
|
params.delete key
|
|
elsif !key.include?('[')
|
|
# much faster to test for the most common case first (GET)
|
|
# and avoid the call to build_deep_hash
|
|
parser.result[key] = get_typed_value(value[0])
|
|
params.delete key
|
|
elsif value.is_a?(Array)
|
|
parser.parse(key, get_typed_value(value.shift))
|
|
params.delete key if value.empty?
|
|
else
|
|
raise TypeError, "Expected array, found #{value.inspect}"
|
|
end
|
|
end
|
|
end
|
|
|
|
parser.result
|
|
end
|
|
|
|
def parse_multipart_form_parameters(body, boundary, body_size, env)
|
|
parse_request_parameters(read_multipart(body, boundary, body_size, env))
|
|
end
|
|
|
|
def extract_multipart_boundary(content_type_with_parameters)
|
|
if content_type_with_parameters =~ MULTIPART_BOUNDARY
|
|
['multipart/form-data', $1.dup]
|
|
else
|
|
extract_content_type_without_parameters(content_type_with_parameters)
|
|
end
|
|
end
|
|
|
|
def extract_content_type_without_parameters(content_type_with_parameters)
|
|
$1.strip.downcase if content_type_with_parameters =~ /^([^,\;]*)/
|
|
end
|
|
|
|
def clean_up_ajax_request_body!(body)
|
|
body.chop! if body[-1] == 0
|
|
body.gsub!(/&_=$/, '')
|
|
end
|
|
|
|
|
|
private
|
|
def get_typed_value(value)
|
|
case value
|
|
when String
|
|
value
|
|
when NilClass
|
|
''
|
|
when Array
|
|
value.map { |v| get_typed_value(v) }
|
|
else
|
|
if value.respond_to? :original_filename
|
|
# Uploaded file
|
|
if value.original_filename
|
|
value
|
|
# Multipart param
|
|
else
|
|
result = value.read
|
|
value.rewind
|
|
result
|
|
end
|
|
# Unknown value, neither string nor multipart.
|
|
else
|
|
raise "Unknown form value: #{value.inspect}"
|
|
end
|
|
end
|
|
end
|
|
|
|
MULTIPART_BOUNDARY = %r|\Amultipart/form-data.*boundary=\"?([^\";,]+)\"?|n
|
|
|
|
EOL = "\015\012"
|
|
|
|
def read_multipart(body, boundary, body_size, env)
|
|
params = Hash.new([])
|
|
boundary = "--" + boundary
|
|
quoted_boundary = Regexp.quote(boundary)
|
|
buf = ""
|
|
bufsize = 10 * 1024
|
|
boundary_end=""
|
|
|
|
# start multipart/form-data
|
|
body.binmode if defined? body.binmode
|
|
case body
|
|
when File
|
|
body.set_encoding(Encoding::BINARY) if body.respond_to?(:set_encoding)
|
|
when StringIO
|
|
body.string.force_encoding(Encoding::BINARY) if body.string.respond_to?(:force_encoding)
|
|
end
|
|
boundary_size = boundary.size + EOL.size
|
|
body_size -= boundary_size
|
|
status = body.read(boundary_size)
|
|
if nil == status
|
|
raise EOFError, "no content body"
|
|
elsif boundary + EOL != status
|
|
raise EOFError, "bad content body"
|
|
end
|
|
|
|
loop do
|
|
head = nil
|
|
content =
|
|
if 10240 < body_size
|
|
UploadedTempfile.new("CGI")
|
|
else
|
|
UploadedStringIO.new
|
|
end
|
|
content.binmode if defined? content.binmode
|
|
|
|
until head and /#{quoted_boundary}(?:#{EOL}|--)/n.match(buf)
|
|
|
|
if (not head) and /#{EOL}#{EOL}/n.match(buf)
|
|
buf = buf.sub(/\A((?:.|\n)*?#{EOL})#{EOL}/n) do
|
|
head = $1.dup
|
|
""
|
|
end
|
|
next
|
|
end
|
|
|
|
if head and ( (EOL + boundary + EOL).size < buf.size )
|
|
content.print buf[0 ... (buf.size - (EOL + boundary + EOL).size)]
|
|
buf[0 ... (buf.size - (EOL + boundary + EOL).size)] = ""
|
|
end
|
|
|
|
c = if bufsize < body_size
|
|
body.read(bufsize)
|
|
else
|
|
body.read(body_size)
|
|
end
|
|
if c.nil? || c.empty?
|
|
raise EOFError, "bad content body"
|
|
end
|
|
buf.concat(c)
|
|
body_size -= c.size
|
|
end
|
|
|
|
buf = buf.sub(/\A((?:.|\n)*?)(?:[\r\n]{1,2})?#{quoted_boundary}([\r\n]{1,2}|--)/n) do
|
|
content.print $1
|
|
if "--" == $2
|
|
body_size = -1
|
|
end
|
|
boundary_end = $2.dup
|
|
""
|
|
end
|
|
|
|
content.rewind
|
|
|
|
head =~ /Content-Disposition:.* filename=(?:"((?:\\.|[^\"])*)"|([^;]*))/ni
|
|
if filename = $1 || $2
|
|
if /Mac/ni.match(env['HTTP_USER_AGENT']) and
|
|
/Mozilla/ni.match(env['HTTP_USER_AGENT']) and
|
|
(not /MSIE/ni.match(env['HTTP_USER_AGENT']))
|
|
filename = CGI.unescape(filename)
|
|
end
|
|
content.original_path = filename.dup
|
|
end
|
|
|
|
head =~ /Content-Type: ([^\r]*)/ni
|
|
content.content_type = $1.dup if $1
|
|
|
|
head =~ /Content-Disposition:.* name="?([^\";]*)"?/ni
|
|
name = $1.dup if $1
|
|
|
|
if params.has_key?(name)
|
|
params[name].push(content)
|
|
else
|
|
params[name] = [content]
|
|
end
|
|
break if body_size == -1
|
|
end
|
|
raise EOFError, "bad boundary end of body part" unless boundary_end=~/--/
|
|
|
|
begin
|
|
body.rewind if body.respond_to?(:rewind)
|
|
rescue Errno::ESPIPE
|
|
# Handles exceptions raised by input streams that cannot be rewound
|
|
# such as when using plain CGI under Apache
|
|
end
|
|
|
|
params
|
|
end
|
|
end # class << self
|
|
end
|
|
end
|