aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--actionpack/CHANGELOG2
-rwxr-xr-xactionpack/lib/action_controller/cgi_ext/cgi_methods.rb156
2 files changed, 115 insertions, 43 deletions
diff --git a/actionpack/CHANGELOG b/actionpack/CHANGELOG
index ca6b21eb56..e06148c424 100644
--- a/actionpack/CHANGELOG
+++ b/actionpack/CHANGELOG
@@ -1,5 +1,7 @@
*SVN*
+* Update query parser to support adjacent hashes. [Nicholas Seckar]
+
* Make action caching aware of different formats for the same action so that, e.g. foo.xml is cached separately from foo.html. Implicitly set content type when reading in cached content with mime revealing extensions so the entire onous isn't on the webserver. [Marcel Molina Jr.]
* Restrict Request Method hacking with ?_method to POST requests. [Rick Olson]
diff --git a/actionpack/lib/action_controller/cgi_ext/cgi_methods.rb b/actionpack/lib/action_controller/cgi_ext/cgi_methods.rb
index 1af837c39a..59ee9c1338 100755
--- a/actionpack/lib/action_controller/cgi_ext/cgi_methods.rb
+++ b/actionpack/lib/action_controller/cgi_ext/cgi_methods.rb
@@ -1,40 +1,16 @@
require 'cgi'
require 'action_controller/vendor/xml_node'
+require 'strscan'
# Static methods for parsing the query and request parameters that can be used in
# a CGI extension class or testing in isolation.
class CGIMethods #:nodoc:
+
class << self
# Returns a hash with the pairs from the query string. The implicit hash construction that is done in
# parse_request_params is not done here.
def parse_query_parameters(query_string)
- parsed_params = {}
-
- query_string.split(/[&;]/).each { |p|
- # Ignore repeated delimiters.
- next if p.empty?
-
- k, v = p.split('=',2)
- v = nil if (v && v.empty?)
-
- k = CGI.unescape(k) if k
- v = CGI.unescape(v) if v
-
- unless k.include?(?[)
- parsed_params[k] = v
- else
- keys = split_key(k)
- last_key = keys.pop
- last_key = keys.pop if (use_array = last_key.empty?)
- parent = keys.inject(parsed_params) {|h, k| h[k] ||= {}}
-
- if use_array then (parent[last_key] ||= []) << v
- else parent[last_key] = v
- end
- end
- }
-
- parsed_params
+ QueryStringScanner.new(query_string).parse
end
# Returns the request (POST/GET) parameters in a parsed form where pairs such as "customer[address][street]" /
@@ -76,22 +52,6 @@ class CGIMethods #:nodoc:
end
private
- # Splits the given key into several pieces. Example keys are 'name', 'person[name]',
- # 'person[name][first]', and 'people[]'. In each instance, an Array instance is returned.
- # 'person[name][first]' produces ['person', 'name', 'first']; 'people[]' produces ['people', '']
- def split_key(key)
- if /^([^\[]+)((?:\[[^\]]*\])+)$/ =~ key
- keys = [$1]
-
- keys.concat($2[1..-2].split(']['))
- keys << '' if key[-2..-1] == '[]' # Have to add it since split will drop empty strings
-
- keys
- else
- [key]
- end
- end
-
def get_typed_value(value)
# test most frequent case first
if value.is_a?(String)
@@ -157,4 +117,114 @@ class CGIMethods #:nodoc:
end
end
end
+
+ class QueryStringScanner < StringScanner
+
+ attr_reader :top, :parent, :result
+
+ def initialize(string)
+ super(string)
+ end
+
+ KEY_REGEXP = %r{([^\[\]=&]+)}
+ BRACKETED_KEY_REGEXP = %r{\[([^\[\]=&]+)\]}
+
+ # Parse the query string
+ def parse
+ @result = {}
+ until eos?
+ # Parse each & delimited chunk
+ @parent, @top = nil, result
+
+ # First scan the bare key
+ key = scan(KEY_REGEXP) or (skip_term and next)
+ key = post_key_check(key)
+
+ # Then scan as many nestings as present
+ until check(/\=/) || eos?
+ r = scan(BRACKETED_KEY_REGEXP) or (skip_term and break)
+ key = self[1]
+ key = post_key_check(key)
+ end
+
+ # Scan the value if we see an =
+ if scan %r{=}
+ value = scan(/[^\&]+/) # scan_until doesn't handle \Z
+ value = CGI.unescape(value) if value # May be nil when eos?
+ bind key, value
+ end
+ scan %r/\&+/ # Ignore multiple adjacent &'s
+
+ end
+
+ return result
+ end
+
+ # Skip over the current term by scanning past the next &, or to
+ # then end of the string if there is no next &
+ def skip_term
+ scan_until(%r/\&+/) || scan(/.+/)
+ end
+
+ # 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 eos? || check(/\&/) # a& or a\Z indicates a is a flag.
+ bind key, nil # Curiously enough, the flag's value is nil
+ nil
+ elsif 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 # Presumably an = sign is next.
+ key
+ end
+ end
+
+ # Add a container to the stack.
+ #
+ def container(key, klass)
+ raise TypeError if top.is_a?(Hash) && top.key?(key) && ! top[key].is_a?(klass)
+ value = bind(key, klass.new)
+ raise TypeError 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}
+ push top.last
+ end
+ else
+ top << value
+ end
+ elsif top.is_a? Hash
+ key = CGI.unescape(key)
+ if top.key?(key) && parent.is_a?(Array)
+ parent << (@top = {})
+ end
+ return top[key] ||= value
+ else
+ # Do nothing?
+ end
+ return value
+ end
+ end
+
end