Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

Ticket #9030: properly_handle_depth_first_array_param_order_as_from_prototype_form_serialize.diff

File properly_handle_depth_first_array_param_order_as_from_prototype_form_serialize.diff, 8.2 kB (added by lastobelus, 10 months ago)

refactored UrlEncodedPairParser to handle array params in breadth-first order

  • activesupport/lib/active_support/core_ext/hash/indifferent_access.rb

    old new  
    7979      when Hash 
    8080        value.with_indifferent_access 
    8181      when Array 
    82         value.collect { |e| e.is_a?(Hash) ? e.with_indifferent_access : e } 
     82        # don't do collect() because we need to be sure the object_id doesn't change 
     83        value.each_index do |i| 
     84          value[i] = value[i].with_indifferent_access if value[i].is_a?(Hash) 
     85        end 
     86        value 
    8387      else 
    8488        value 
    8589      end 
     
    9195    module Hash #:nodoc: 
    9296      module IndifferentAccess #:nodoc: 
    9397        def with_indifferent_access 
     98          return self if self.is_a?(HashWithIndifferentAccess) 
     99 
    94100          hash = HashWithIndifferentAccess.new(self) 
    95101          hash.default = self.default 
    96102          hash 
  • actionpack/test/controller/request_test.rb

    old new  
    438438    ) 
    439439  end 
    440440 
     441  def test_deep_query_string_with_array_of_hashes_with_multiple_pairs_in_depth_first_order 
     442    assert_equal( 
     443      {'x' => {'y' => [{'z' => '10', 'w' => 'a'}, {'z' => '20', 'w' => 'b'}]}}, 
     444      ActionController::AbstractRequest.parse_query_parameters('x[y][][z]=10&x[y][][z]=20&x[y][][w]=a&x[y][][w]=b') 
     445    ) 
     446  end 
     447 
     448  def test_deep_query_string_with_multiple_pairs_in_breadth_first_order_cannot_have_holes 
     449    assert_equal( 
     450      {'x' => {'y' => [{'z' => '10', 'w' => 'a'}, {'z' => '20', 'w' => 'c'}, {'z' => '30'}]}}, 
     451      ActionController::AbstractRequest.parse_query_parameters('x[y][][z]=10&x[y][][w]=a&x[y][][z]=20&x[y][][z]=30&x[y][][w]=c') 
     452    ) 
     453  end 
     454   
     455  def test_deep_query_string_lots_of_arrays_and_hashes 
     456    assert_equal( 
     457      {"a" => {"b" => [{"c" => {"d" => ["1"]}}, {"c" => {"d" => ["2"]}}, {"c" => {"d" => ["3"]}}]}}, 
     458      ActionController::AbstractRequest.parse_query_parameters('a[b][][c][d][]=1&a[b][][c][d][]=2&a[b][][c][d][]=3') 
     459    ) 
     460  end 
     461 
    441462  def test_query_string_with_nil 
    442463    assert_equal( 
    443464      { "action" => "create_customer", "full_name" => ''}, 
  • actionpack/lib/action_controller/request.rb

    old new  
    584584    end 
    585585  end 
    586586 
     587  # RADAR result of refactoring parser to properly handle array params in  
     588  # depth-first order (as returned by Prototype 1.5.1+) is that if array 
     589  # params are in breadth-first order, all rows must have all keys for 
     590  # the result to match that of the previous parser. 
     591  # ie, "x[][a]=a1&x[][b]=b1&x[][a]=a2&x[][a]=a3&x[][b]=b3" 
     592  # will parse to {x=>[{a=>'a1', b=>'b1}, {a=>'a2', b=>'b3'}, {a=>'a3'}]} 
     593  # not {x=>[{a=>'a1', b=>'b1}, {a=>'a2'}, {a=>'a3', b=>'b3'}]} as before. 
     594  # However the previous parser and this one return the same result when 
     595  # the "hole" is the first param: 
     596  # 'x[][a]=a1&x[][b]=b1&x[][b]=b2&x[][a]=a3&x[][b]=b3' will parse to 
     597  # {"x"=>[{"a"=>"a1", "b"=>"b1"}, {"a"=>"a3", "b"=>"b2"}, {"b"=>"b3"}]} 
     598  # with both parsers. 
     599   
    587600  class UrlEncodedPairParser < StringScanner #:nodoc: 
    588     attr_reader :top, :parent, :result 
    589  
     601    attr_reader :result 
    590602    def initialize(pairs = []) 
    591603      super('') 
    592       @result = {} 
     604      @result = {}.with_indifferent_access 
    593605      pairs.each { |key, value| parse(key, value) } 
    594606    end 
    595607 
    596608    KEY_REGEXP = %r{([^\[\]=&]+)} 
    597     BRACKETED_KEY_REGEXP = %r{\[([^\[\]=&]+)\]} 
     609    BRACKETED_KEY_REGEXP = %r{\[([^\[\]=&]*)\]} 
    598610 
    599611    # Parse the query string 
    600612    def parse(key, value) 
     613      pointer = @result 
    601614      self.string = key 
    602       @top, @parent = result, nil 
     615      prev_key = scan(KEY_REGEXP) or return 
    603616 
    604       # First scan the bare key 
    605       key = scan(KEY_REGEXP) or return 
    606       key = post_key_check(key) 
    607  
    608       # Then scan as many nestings as present 
    609617      until eos? 
    610         r = scan(BRACKETED_KEY_REGEXP) or return 
    611         key = self[1] 
    612         key = post_key_check(key) 
    613       end 
    614  
    615       bind(key, value) 
    616     end 
    617  
    618     private 
    619       # After we see a key, we must look ahead to determine our next action. Cases: 
    620       # 
    621       #   [] follows the key. Then the value must be an array. 
    622       #   = follows the key. (A value comes next) 
    623       #   & or the end of string follows the key. Then the key is a flag. 
    624       #   otherwise, a hash follows the key. 
    625       def post_key_check(key) 
    626         if scan(/\[\]/) # a[b][] indicates that b is an array 
    627           container(key, Array) 
    628           nil 
    629         elsif check(/\[[^\]]/) # a[b] indicates that a is a hash 
    630           container(key, Hash) 
    631           nil 
    632         else # End of key? We do nothing. 
    633           key 
    634         end 
    635       end 
    636  
    637       # Add a container to the stack. 
    638       def container(key, klass) 
    639         type_conflict! klass, top[key] if top.is_a?(Hash) && top.key?(key) && ! top[key].is_a?(klass) 
    640         value = bind(key, klass.new) 
    641         type_conflict! klass, value unless value.is_a?(klass) 
    642         push(value) 
    643       end 
    644  
    645       # Push a value onto the 'stack', which is actually only the top 2 items. 
    646       def push(value) 
    647         @parent, @top = @top, value 
    648       end 
    649  
    650       # Bind a key (which may be nil for items in an array) to the provided value. 
    651       def bind(key, value) 
    652         if top.is_a? Array 
    653           if key 
    654             if top[-1].is_a?(Hash) && ! top[-1].key?(key) 
    655               top[-1][key] = value 
     618        r = scan(BRACKETED_KEY_REGEXP) or break 
     619        next_key = self[1] 
     620        if next_key.blank? 
     621          raise ArgumentError, "Can't parse multi-dimensional arrays" if prev_key.blank?  
     622          pointer[prev_key] ||= [] 
     623          type_conflict!(Array, pointer[prev_key]) unless pointer[prev_key].is_a?(Array) 
     624          pointer = pointer[prev_key] 
     625        else 
     626          if pointer.is_a?(Array) 
     627            if pointer.empty? 
     628              pointer << {}.with_indifferent_access 
     629              pointer = pointer.first 
    656630            else 
    657               top << {key => value}.with_indifferent_access 
    658               push top.last 
     631              pointer = pointer.find{|h| not h.has_key?(next_key) } || pointer.push({}.with_indifferent_access).last 
    659632            end 
    660633          else 
    661             top << value 
     634            raise ArgumentError, "don't know what to do" if prev_key.blank? 
     635            pointer[prev_key] ||= {}.with_indifferent_access 
     636            type_conflict!(Hash, pointer[prev_key]) unless pointer[prev_key].is_a?(Hash) 
     637            pointer = pointer[prev_key] 
    662638          end 
    663         elsif top.is_a? Hash 
    664           key = CGI.unescape(key) 
    665           parent << (@top = {}) if top.key?(key) && parent.is_a?(Array) 
    666           return top[key] ||= value 
     639        end 
     640        prev_key = next_key 
     641      end 
     642      # a little extra checking to avoid having to change existing tests that expect an arbitrary solution to malformed params 
     643      if rest.blank? or rest.first == '['  
     644        value = {}.with_indifferent_access if rest.first == '[' 
     645        if prev_key.blank? 
     646          type_conflict!(Array, pointer) unless pointer.is_a?(Array) 
     647          pointer << value 
    667648        else 
    668           raise ArgumentError, "Don't know what to do: top is #{top.inspect}" 
     649          type_conflict!(Hash, pointer) unless pointer.is_a?(Hash) 
     650          pointer[prev_key] = value 
    669651        end 
    670  
    671         return value 
    672652      end 
     653    end 
    673654 
    674       def type_conflict!(klass, value) 
    675         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." 
    676       end 
     655    def type_conflict!(klass, value) 
     656      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." 
     657    end 
     658 
    677659  end 
    678660 
    679661  module UploadedFile