From e898f82a743063652aed802d99ea8b5deac2ec3c Mon Sep 17 00:00:00 2001 From: Pratik Naik Date: Thu, 25 Dec 2008 03:51:04 +0000 Subject: Move request parsing related code to ActionController::RequestParser --- actionpack/lib/action_controller.rb | 4 + actionpack/lib/action_controller/request.rb | 423 +-------------------- actionpack/lib/action_controller/request_parser.rb | 314 +++++++++++++++ actionpack/lib/action_controller/test_process.rb | 31 +- actionpack/lib/action_controller/uploaded_file.rb | 37 ++ .../action_controller/url_encoded_pair_parser.rb | 95 +++++ actionpack/test/controller/request_test.rb | 68 ++-- 7 files changed, 512 insertions(+), 460 deletions(-) create mode 100644 actionpack/lib/action_controller/request_parser.rb create mode 100644 actionpack/lib/action_controller/uploaded_file.rb create mode 100644 actionpack/lib/action_controller/url_encoded_pair_parser.rb diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb index 3bb755376f..98fb490d64 100644 --- a/actionpack/lib/action_controller.rb +++ b/actionpack/lib/action_controller.rb @@ -60,6 +60,10 @@ module ActionController autoload :MimeResponds, 'action_controller/mime_responds' autoload :PolymorphicRoutes, 'action_controller/polymorphic_routes' autoload :Request, 'action_controller/request' + autoload :RequestParser, 'action_controller/request_parser' + autoload :UrlEncodedPairParser, 'action_controller/url_encoded_pair_parser' + autoload :UploadedStringIO, 'action_controller/uploaded_file' + autoload :UploadedTempfile, 'action_controller/uploaded_file' autoload :RecordIdentifier, 'action_controller/record_identifier' autoload :Response, 'action_controller/response' autoload :RequestForgeryProtection, 'action_controller/request_forgery_protection' diff --git a/actionpack/lib/action_controller/request.rb b/actionpack/lib/action_controller/request.rb index d9eb5af849..8a02130d88 100755 --- a/actionpack/lib/action_controller/request.rb +++ b/actionpack/lib/action_controller/request.rb @@ -101,7 +101,7 @@ module ActionController # For backward compatibility, the post \format is extracted from the # X-Post-Data-Format HTTP header if present. def content_type - Mime::Type.lookup(content_type_without_parameters) + Mime::Type.lookup(parser.content_type_without_parameters) end memoize :content_type @@ -389,11 +389,7 @@ EOM # Read the request \body. This is useful for web services that need to # work with raw requests directly. 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'] + parser.raw_post end # Returns both GET and POST \parameters in a single hash. @@ -421,15 +417,8 @@ EOM @path_parameters ||= {} 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 - body_stream - end + parser.body end def remote_addr @@ -442,11 +431,11 @@ EOM alias referer referrer def query_parameters - @query_parameters ||= self.class.parse_query_parameters(query_string) + @query_parameters ||= parser.query_parameters end def request_parameters - @request_parameters ||= parse_formatted_request_parameters + @request_parameters ||= parser.request_parameters end def body_stream #:nodoc: @@ -481,411 +470,13 @@ EOM @env['SERVER_PORT'].to_i end - protected - # 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 - - # 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 - memoize :content_type_without_parameters - private - 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 - - 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 named_host?(host) !(host.nil? || /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.match(host)) 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 - end - - class UrlEncodedPairParser < StringScanner #:nodoc: - attr_reader :top, :parent, :result - - def initialize(pairs = []) - super('') - @result = {} - pairs.each { |key, value| parse(key, value) } - end - - KEY_REGEXP = %r{([^\[\]=&]+)} - BRACKETED_KEY_REGEXP = %r{\[([^\[\]=&]+)\]} - - # Parse the query string - def parse(key, value) - self.string = key - @top, @parent = result, nil - - # First scan the bare key - key = scan(KEY_REGEXP) or return - key = post_key_check(key) - - # Then scan as many nestings as present - until eos? - r = scan(BRACKETED_KEY_REGEXP) or return - key = self[1] - key = post_key_check(key) - end - - bind(key, value) - end - - private - # After we see a key, we must look ahead to determine our next action. Cases: - # - # [] follows the key. Then the value must be an array. - # = follows the key. (A value comes next) - # & or the end of string follows the key. Then the key is a flag. - # otherwise, a hash follows the key. - def post_key_check(key) - if scan(/\[\]/) # a[b][] indicates that b is an array - container(key, Array) - nil - elsif check(/\[[^\]]/) # a[b] indicates that a is a hash - container(key, Hash) - nil - else # End of key? We do nothing. - key - end - end - - # Add a container to the stack. - def container(key, klass) - type_conflict! klass, top[key] if top.is_a?(Hash) && top.key?(key) && ! top[key].is_a?(klass) - value = bind(key, klass.new) - type_conflict! klass, value unless value.is_a?(klass) - push(value) - end - - # Push a value onto the 'stack', which is actually only the top 2 items. - def push(value) - @parent, @top = @top, value + def parser + @parser ||= ActionController::RequestParser.new(@env) end - - # Bind a key (which may be nil for items in an array) to the provided value. - def bind(key, value) - if top.is_a? Array - if key - if top[-1].is_a?(Hash) && ! top[-1].key?(key) - top[-1][key] = value - else - top << {key => value}.with_indifferent_access - push top.last - value = top[key] - end - else - top << value - end - elsif top.is_a? Hash - key = CGI.unescape(key) - parent << (@top = {}) if top.key?(key) && parent.is_a?(Array) - top[key] ||= value - return top[key] - else - raise ArgumentError, "Don't know what to do: top is #{top.inspect}" - end - - return value - end - - def type_conflict!(klass, value) - raise TypeError, "Conflicting types for parameter containers. Expected an instance of #{klass} but found an instance of #{value.class}. This can be caused by colliding Array and Hash parameters like qs[]=value&qs[key]=value. (The parameters received were #{value.inspect}.)" - end - end - - module UploadedFile - def self.included(base) - base.class_eval do - attr_accessor :original_path, :content_type - alias_method :local_path, :path - end - end - - # 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. - # The Windows regexp is adapted from Perl's File::Basename. - def original_filename - unless defined? @original_filename - @original_filename = - unless original_path.blank? - if original_path =~ /^(?:.*[:\\\/])?(.*)/m - $1 - else - File.basename original_path - end - end - end - @original_filename - end - end - - class UploadedStringIO < StringIO - include UploadedFile - end - - class UploadedTempfile < Tempfile - include UploadedFile end end diff --git a/actionpack/lib/action_controller/request_parser.rb b/actionpack/lib/action_controller/request_parser.rb new file mode 100644 index 0000000000..82ee4c84c4 --- /dev/null +++ b/actionpack/lib/action_controller/request_parser.rb @@ -0,0 +1,314 @@ +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 diff --git a/actionpack/lib/action_controller/test_process.rb b/actionpack/lib/action_controller/test_process.rb index 211e22ff58..dddad1756a 100644 --- a/actionpack/lib/action_controller/test_process.rb +++ b/actionpack/lib/action_controller/test_process.rb @@ -29,18 +29,21 @@ module ActionController #:nodoc: class TestRequest < Request #:nodoc: attr_accessor :cookies, :session_options - attr_accessor :query_parameters, :request_parameters, :path, :session - attr_accessor :host, :user_agent + attr_accessor :query_parameters, :path, :session + attr_accessor :host def initialize - super(Rack::MockRequest.env_for('/')) + env = Rack::MockRequest.env_for("/") + + # TODO: Fix Request to assume env['SERVER_ADDR'] doesn't contain port number + env['SERVER_ADDR'] = env.delete("SERVER_NAME") + super(env) @query_parameters = {} - @request_parameters = {} @session = TestSession.new - initialize_containers initialize_default_values + initialize_containers end def reset_session @@ -55,7 +58,11 @@ module ActionController #:nodoc: # Either the RAW_POST_DATA environment variable or the URL-encoded request # parameters. def raw_post - env['RAW_POST_DATA'] ||= returning(url_encoded_request_parameters) { |b| b.force_encoding(Encoding::BINARY) if b.respond_to?(:force_encoding) } + @env['RAW_POST_DATA'] ||= begin + data = url_encoded_request_parameters + data.force_encoding(Encoding::BINARY) if data.respond_to?(:force_encoding) + data + end end def port=(number) @@ -125,26 +132,30 @@ module ActionController #:nodoc: path_parameters[key.to_s] = value end end + raw_post # populate env['RAW_POST_DATA'] @parameters = nil # reset TestRequest#parameters to use the new path_parameters end def recycle! - self.request_parameters = {} self.query_parameters = {} self.path_parameters = {} unmemoize_all end + def user_agent=(user_agent) + @env['HTTP_USER_AGENT'] = user_agent + end + private def initialize_containers - @env, @cookies = {}, {} + @cookies = {} end def initialize_default_values @host = "test.host" @request_uri = "/" - @user_agent = "Rails Testing" - self.remote_addr = "0.0.0.0" + @env['HTTP_USER_AGENT'] = "Rails Testing" + @env['REMOTE_ADDR'] = "0.0.0.0" @env["SERVER_PORT"] = 80 @env['REQUEST_METHOD'] = "GET" end diff --git a/actionpack/lib/action_controller/uploaded_file.rb b/actionpack/lib/action_controller/uploaded_file.rb new file mode 100644 index 0000000000..ea4845c68f --- /dev/null +++ b/actionpack/lib/action_controller/uploaded_file.rb @@ -0,0 +1,37 @@ +module ActionController + module UploadedFile + def self.included(base) + base.class_eval do + attr_accessor :original_path, :content_type + alias_method :local_path, :path + end + end + + # 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. + # The Windows regexp is adapted from Perl's File::Basename. + def original_filename + unless defined? @original_filename + @original_filename = + unless original_path.blank? + if original_path =~ /^(?:.*[:\\\/])?(.*)/m + $1 + else + File.basename original_path + end + end + end + @original_filename + end + end + + class UploadedStringIO < StringIO + include UploadedFile + end + + class UploadedTempfile < Tempfile + include UploadedFile + end +end diff --git a/actionpack/lib/action_controller/url_encoded_pair_parser.rb b/actionpack/lib/action_controller/url_encoded_pair_parser.rb new file mode 100644 index 0000000000..bea96c711d --- /dev/null +++ b/actionpack/lib/action_controller/url_encoded_pair_parser.rb @@ -0,0 +1,95 @@ +module ActionController + class UrlEncodedPairParser < StringScanner #:nodoc: + attr_reader :top, :parent, :result + + def initialize(pairs = []) + super('') + @result = {} + pairs.each { |key, value| parse(key, value) } + end + + KEY_REGEXP = %r{([^\[\]=&]+)} + BRACKETED_KEY_REGEXP = %r{\[([^\[\]=&]+)\]} + + # Parse the query string + def parse(key, value) + self.string = key + @top, @parent = result, nil + + # First scan the bare key + key = scan(KEY_REGEXP) or return + key = post_key_check(key) + + # Then scan as many nestings as present + until eos? + r = scan(BRACKETED_KEY_REGEXP) or return + key = self[1] + key = post_key_check(key) + end + + bind(key, value) + end + + private + # After we see a key, we must look ahead to determine our next action. Cases: + # + # [] follows the key. Then the value must be an array. + # = follows the key. (A value comes next) + # & or the end of string follows the key. Then the key is a flag. + # otherwise, a hash follows the key. + def post_key_check(key) + if scan(/\[\]/) # a[b][] indicates that b is an array + container(key, Array) + nil + elsif check(/\[[^\]]/) # a[b] indicates that a is a hash + container(key, Hash) + nil + else # End of key? We do nothing. + key + end + end + + # Add a container to the stack. + def container(key, klass) + type_conflict! klass, top[key] if top.is_a?(Hash) && top.key?(key) && ! top[key].is_a?(klass) + value = bind(key, klass.new) + type_conflict! klass, value unless value.is_a?(klass) + push(value) + end + + # Push a value onto the 'stack', which is actually only the top 2 items. + def push(value) + @parent, @top = @top, value + end + + # Bind a key (which may be nil for items in an array) to the provided value. + def bind(key, value) + if top.is_a? Array + if key + if top[-1].is_a?(Hash) && ! top[-1].key?(key) + top[-1][key] = value + else + top << {key => value}.with_indifferent_access + push top.last + value = top[key] + end + else + top << value + end + elsif top.is_a? Hash + key = CGI.unescape(key) + parent << (@top = {}) if top.key?(key) && parent.is_a?(Array) + top[key] ||= value + return top[key] + else + raise ArgumentError, "Don't know what to do: top is #{top.inspect}" + end + + return value + end + + def type_conflict!(klass, value) + raise TypeError, "Conflicting types for parameter containers. Expected an instance of #{klass} but found an instance of #{value.class}. This can be caused by colliding Array and Hash parameters like qs[]=value&qs[key]=value. (The parameters received were #{value.inspect}.)" + end + end +end \ No newline at end of file diff --git a/actionpack/test/controller/request_test.rb b/actionpack/test/controller/request_test.rb index 3e10a4665e..349cea268f 100644 --- a/actionpack/test/controller/request_test.rb +++ b/actionpack/test/controller/request_test.rb @@ -424,95 +424,95 @@ class UrlEncodedRequestParameterParsingTest < ActiveSupport::TestCase def test_query_string assert_equal( { "action" => "create_customer", "full_name" => "David Heinemeier Hansson", "customerId" => "1"}, - ActionController::Request.parse_query_parameters(@query_string) + ActionController::RequestParser.parse_query_parameters(@query_string) ) end def test_deep_query_string expected = {'x' => {'y' => {'z' => '10'}}} - assert_equal(expected, ActionController::Request.parse_query_parameters('x[y][z]=10')) + assert_equal(expected, ActionController::RequestParser.parse_query_parameters('x[y][z]=10')) end def test_deep_query_string_with_array - assert_equal({'x' => {'y' => {'z' => ['10']}}}, ActionController::Request.parse_query_parameters('x[y][z][]=10')) - assert_equal({'x' => {'y' => {'z' => ['10', '5']}}}, ActionController::Request.parse_query_parameters('x[y][z][]=10&x[y][z][]=5')) + assert_equal({'x' => {'y' => {'z' => ['10']}}}, ActionController::RequestParser.parse_query_parameters('x[y][z][]=10')) + assert_equal({'x' => {'y' => {'z' => ['10', '5']}}}, ActionController::RequestParser.parse_query_parameters('x[y][z][]=10&x[y][z][]=5')) end def test_deep_query_string_with_array_of_hash - assert_equal({'x' => {'y' => [{'z' => '10'}]}}, ActionController::Request.parse_query_parameters('x[y][][z]=10')) - assert_equal({'x' => {'y' => [{'z' => '10', 'w' => '10'}]}}, ActionController::Request.parse_query_parameters('x[y][][z]=10&x[y][][w]=10')) + assert_equal({'x' => {'y' => [{'z' => '10'}]}}, ActionController::RequestParser.parse_query_parameters('x[y][][z]=10')) + assert_equal({'x' => {'y' => [{'z' => '10', 'w' => '10'}]}}, ActionController::RequestParser.parse_query_parameters('x[y][][z]=10&x[y][][w]=10')) end def test_deep_query_string_with_array_of_hashes_with_one_pair - assert_equal({'x' => {'y' => [{'z' => '10'}, {'z' => '20'}]}}, ActionController::Request.parse_query_parameters('x[y][][z]=10&x[y][][z]=20')) - assert_equal("10", ActionController::Request.parse_query_parameters('x[y][][z]=10&x[y][][z]=20')["x"]["y"].first["z"]) - assert_equal("10", ActionController::Request.parse_query_parameters('x[y][][z]=10&x[y][][z]=20').with_indifferent_access[:x][:y].first[:z]) + assert_equal({'x' => {'y' => [{'z' => '10'}, {'z' => '20'}]}}, ActionController::RequestParser.parse_query_parameters('x[y][][z]=10&x[y][][z]=20')) + assert_equal("10", ActionController::RequestParser.parse_query_parameters('x[y][][z]=10&x[y][][z]=20')["x"]["y"].first["z"]) + assert_equal("10", ActionController::RequestParser.parse_query_parameters('x[y][][z]=10&x[y][][z]=20').with_indifferent_access[:x][:y].first[:z]) end def test_deep_query_string_with_array_of_hashes_with_multiple_pairs assert_equal( {'x' => {'y' => [{'z' => '10', 'w' => 'a'}, {'z' => '20', 'w' => 'b'}]}}, - ActionController::Request.parse_query_parameters('x[y][][z]=10&x[y][][w]=a&x[y][][z]=20&x[y][][w]=b') + ActionController::RequestParser.parse_query_parameters('x[y][][z]=10&x[y][][w]=a&x[y][][z]=20&x[y][][w]=b') ) end def test_query_string_with_nil assert_equal( { "action" => "create_customer", "full_name" => ''}, - ActionController::Request.parse_query_parameters(@query_string_with_empty) + ActionController::RequestParser.parse_query_parameters(@query_string_with_empty) ) end def test_query_string_with_array assert_equal( { "action" => "create_customer", "selected" => ["1", "2", "3"]}, - ActionController::Request.parse_query_parameters(@query_string_with_array) + ActionController::RequestParser.parse_query_parameters(@query_string_with_array) ) end def test_query_string_with_amps assert_equal( { "action" => "create_customer", "name" => "Don't & Does"}, - ActionController::Request.parse_query_parameters(@query_string_with_amps) + ActionController::RequestParser.parse_query_parameters(@query_string_with_amps) ) end def test_query_string_with_many_equal assert_equal( { "action" => "create_customer", "full_name" => "abc=def=ghi"}, - ActionController::Request.parse_query_parameters(@query_string_with_many_equal) + ActionController::RequestParser.parse_query_parameters(@query_string_with_many_equal) ) end def test_query_string_without_equal assert_equal( { "action" => nil }, - ActionController::Request.parse_query_parameters(@query_string_without_equal) + ActionController::RequestParser.parse_query_parameters(@query_string_without_equal) ) end def test_query_string_with_empty_key assert_equal( { "action" => "create_customer", "full_name" => "David Heinemeier Hansson" }, - ActionController::Request.parse_query_parameters(@query_string_with_empty_key) + ActionController::RequestParser.parse_query_parameters(@query_string_with_empty_key) ) end def test_query_string_with_many_ampersands assert_equal( { "action" => "create_customer", "full_name" => "David Heinemeier Hansson"}, - ActionController::Request.parse_query_parameters(@query_string_with_many_ampersands) + ActionController::RequestParser.parse_query_parameters(@query_string_with_many_ampersands) ) end def test_unbalanced_query_string_with_array assert_equal( {'location' => ["1", "2"], 'age_group' => ["2"]}, - ActionController::Request.parse_query_parameters("location[]=1&location[]=2&age_group[]=2") + ActionController::RequestParser.parse_query_parameters("location[]=1&location[]=2&age_group[]=2") ) assert_equal( {'location' => ["1", "2"], 'age_group' => ["2"]}, - ActionController::Request.parse_request_parameters({'location[]' => ["1", "2"], + ActionController::RequestParser.parse_request_parameters({'location[]' => ["1", "2"], 'age_group[]' => ["2"]}) ) end @@ -525,7 +525,7 @@ class UrlEncodedRequestParameterParsingTest < ActiveSupport::TestCase expected = { "note" => { "viewers"=>{"viewer"=>[{ "id"=>"1", "type"=>"User"}, {"type"=>"Group", "id"=>"2"} ]} } } - assert_equal(expected, ActionController::Request.parse_request_parameters(query)) + assert_equal(expected, ActionController::RequestParser.parse_request_parameters(query)) end def test_parse_params @@ -564,7 +564,7 @@ class UrlEncodedRequestParameterParsingTest < ActiveSupport::TestCase } } - assert_equal expected_output, ActionController::Request.parse_request_parameters(input) + assert_equal expected_output, ActionController::RequestParser.parse_request_parameters(input) end UploadedStringIO = ActionController::UploadedStringIO @@ -619,7 +619,7 @@ class UrlEncodedRequestParameterParsingTest < ActiveSupport::TestCase "text_part" => "abc" } - params = ActionController::Request.parse_request_parameters(input) + params = ActionController::RequestParser.parse_request_parameters(input) assert_equal expected_output, params # Lone filenames are preserved. @@ -650,7 +650,7 @@ class UrlEncodedRequestParameterParsingTest < ActiveSupport::TestCase "logo" => File.new(File.dirname(__FILE__) + "/rack_test.rb").path, } - assert_equal expected_output, ActionController::Request.parse_request_parameters(input) + assert_equal expected_output, ActionController::RequestParser.parse_request_parameters(input) end def test_parse_params_with_array @@ -658,55 +658,55 @@ class UrlEncodedRequestParameterParsingTest < ActiveSupport::TestCase expected_output = { "selected" => [ "1", "2", "3" ] } - assert_equal expected_output, ActionController::Request.parse_request_parameters(input) + assert_equal expected_output, ActionController::RequestParser.parse_request_parameters(input) end def test_parse_params_with_non_alphanumeric_name input = { "a/b[c]" => %w(d) } expected = { "a/b" => { "c" => "d" }} - assert_equal expected, ActionController::Request.parse_request_parameters(input) + assert_equal expected, ActionController::RequestParser.parse_request_parameters(input) end def test_parse_params_with_single_brackets_in_middle input = { "a/b[c]d" => %w(e) } expected = { "a/b" => {} } - assert_equal expected, ActionController::Request.parse_request_parameters(input) + assert_equal expected, ActionController::RequestParser.parse_request_parameters(input) end def test_parse_params_with_separated_brackets input = { "a/b@[c]d[e]" => %w(f) } expected = { "a/b@" => { }} - assert_equal expected, ActionController::Request.parse_request_parameters(input) + assert_equal expected, ActionController::RequestParser.parse_request_parameters(input) end def test_parse_params_with_separated_brackets_and_array input = { "a/b@[c]d[e][]" => %w(f) } expected = { "a/b@" => { }} - assert_equal expected , ActionController::Request.parse_request_parameters(input) + assert_equal expected , ActionController::RequestParser.parse_request_parameters(input) end def test_parse_params_with_unmatched_brackets_and_array input = { "a/b@[c][d[e][]" => %w(f) } expected = { "a/b@" => { "c" => { }}} - assert_equal expected, ActionController::Request.parse_request_parameters(input) + assert_equal expected, ActionController::RequestParser.parse_request_parameters(input) end def test_parse_params_with_nil_key input = { nil => nil, "test2" => %w(value1) } expected = { "test2" => "value1" } - assert_equal expected, ActionController::Request.parse_request_parameters(input) + assert_equal expected, ActionController::RequestParser.parse_request_parameters(input) end def test_parse_params_with_array_prefix_and_hashes input = { "a[][b][c]" => %w(d) } expected = {"a" => [{"b" => {"c" => "d"}}]} - assert_equal expected, ActionController::Request.parse_request_parameters(input) + assert_equal expected, ActionController::RequestParser.parse_request_parameters(input) end def test_parse_params_with_complex_nesting input = { "a[][b][c][][d][]" => %w(e) } expected = {"a" => [{"b" => {"c" => [{"d" => ["e"]}]}}]} - assert_equal expected, ActionController::Request.parse_request_parameters(input) + assert_equal expected, ActionController::RequestParser.parse_request_parameters(input) end end @@ -768,7 +768,7 @@ class MultipartRequestParameterParsingTest < ActiveSupport::TestCase # Ensures that parse_multipart_form_parameters works with streams that cannot be rewound file = File.open(File.join(FIXTURE_PATH, 'large_text_file'), 'rb') file.expects(:rewind).raises(Errno::ESPIPE) - params = ActionController::Request.parse_multipart_form_parameters(file, 'AaB03x', file.stat.size, {}) + params = ActionController::RequestParser.parse_multipart_form_parameters(file, 'AaB03x', file.stat.size, {}) assert_not_equal 0, file.pos # file was not rewound after reading end end @@ -807,7 +807,7 @@ class MultipartRequestParameterParsingTest < ActiveSupport::TestCase private def parse_multipart(name) File.open(File.join(FIXTURE_PATH, name), 'rb') do |file| - params = ActionController::Request.parse_multipart_form_parameters(file, 'AaB03x', file.stat.size, {}) + params = ActionController::RequestParser.parse_multipart_form_parameters(file, 'AaB03x', file.stat.size, {}) assert_equal 0, file.pos # file was rewound after reading params end -- cgit v1.2.3