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

Ticket #10101: properly_handle_depth_first_array_param_order_as_from_prototype_form_serialize.r8628.diff

File properly_handle_depth_first_array_param_order_as_from_prototype_form_serialize.r8628.diff, 8.9 kB (added by lastobelus, 10 months ago)
  • activesupport/lib/active_support/core_ext/hash/indifferent_access.rb

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

    old new  
    456456    ) 
    457457  end 
    458458 
     459  def test_deep_query_string_with_array_of_hashes_with_multiple_pairs_in_depth_first_order 
     460    assert_equal( 
     461      {'x' => {'y' => [{'z' => '10', 'w' => 'a'}, {'z' => '20', 'w' => 'b'}]}}, 
     462      ActionController::AbstractRequest.parse_query_parameters('x[y][][z]=10&x[y][][z]=20&x[y][][w]=a&x[y][][w]=b') 
     463    ) 
     464  end 
     465 
     466  def test_deep_query_string_with_multiple_pairs_in_breadth_first_order_cannot_have_holes 
     467    assert_equal( 
     468      {'x' => {'y' => [{'z' => '10', 'w' => 'a'}, {'z' => '20', 'w' => 'c'}, {'z' => '30'}]}}, 
     469      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') 
     470    ) 
     471  end 
     472   
     473  def test_deep_query_string_lots_of_arrays_and_hashes 
     474    assert_equal( 
     475      {"a" => {"b" => [{"c" => {"d" => ["1"]}}, {"c" => {"d" => ["2"]}}, {"c" => {"d" => ["3"]}}]}}, 
     476      ActionController::AbstractRequest.parse_query_parameters('a[b][][c][d][]=1&a[b][][c][d][]=2&a[b][][c][d][]=3') 
     477    ) 
     478  end 
     479 
    459480  def test_query_string_with_nil 
    460481    assert_equal( 
    461482      { "action" => "create_customer", "full_name" => ''}, 
  • actionpack/lib/action_controller/request.rb

    old new  
    602602    end 
    603603  end 
    604604 
     605  # RADAR result of refactoring parser to properly handle array params in  
     606  # depth-first order (as returned by Prototype 1.5.1+) is that if array 
     607  # params are in breadth-first order, all rows must have all keys for 
     608  # the result to match that of the previous parser. 
     609  # ie, "x[][a]=a1&x[][b]=b1&x[][a]=a2&x[][a]=a3&x[][b]=b3" 
     610  # will parse to {x=>[{a=>'a1', b=>'b1}, {a=>'a2', b=>'b3'}, {a=>'a3'}]} 
     611  # not {x=>[{a=>'a1', b=>'b1}, {a=>'a2'}, {a=>'a3', b=>'b3'}]} as before. 
     612  # However the previous parser and this one return the same result when 
     613  # the "hole" is the first param: 
     614  # 'x[][a]=a1&x[][b]=b1&x[][b]=b2&x[][a]=a3&x[][b]=b3' will parse to 
     615  # {"x"=>[{"a"=>"a1", "b"=>"b1"}, {"a"=>"a3", "b"=>"b2"}, {"b"=>"b3"}]} 
     616  # with both parsers. 
     617  # 
     618  # Handle new checkbox hack (which prepends the method_name component of the name 
     619  # of the hidden field element with '!'). 
     620  #   
    605621  class UrlEncodedPairParser < StringScanner #:nodoc: 
    606     attr_reader :top, :parent, :result 
    607  
     622    attr_reader :result 
    608623    def initialize(pairs = []) 
    609624      super('') 
    610       @result = {} 
     625      @result = {}.with_indifferent_access 
     626      @no_checkbox_hack = true 
     627      @last_key = nil 
    611628      pairs.each { |key, value| parse(key, value) } 
    612629    end 
    613630 
    614631    KEY_REGEXP = %r{([^\[\]=&]+)} 
    615     BRACKETED_KEY_REGEXP = %r{\[([^\[\]=&]+)\]} 
    616  
     632    BRACKETED_KEY_REGEXP = %r{\[([^\[\]=&]*)\]} 
     633    CHECKBOX_HACK_PREFIX = '!' 
    617634    # Parse the query string 
    618635    def parse(key, value) 
     636      pointer = @result 
    619637      self.string = key 
    620       @top, @parent = result, nil 
    621  
    622       # First scan the bare key 
    623       key = scan(KEY_REGEXP) or return 
    624       key = post_key_check(key) 
    625  
    626       # Then scan as many nestings as present 
     638      prev_key = scan(KEY_REGEXP) or return 
     639      checkbox_key = false 
    627640      until eos? 
    628         r = scan(BRACKETED_KEY_REGEXP) or return 
    629         key = self[1] 
    630         key = post_key_check(key) 
    631       end 
    632  
    633       bind(key, value) 
    634     end 
    635  
    636     private 
    637       # After we see a key, we must look ahead to determine our next action. Cases: 
    638       # 
    639       #   [] follows the key. Then the value must be an array. 
    640       #   = follows the key. (A value comes next) 
    641       #   & or the end of string follows the key. Then the key is a flag. 
    642       #   otherwise, a hash follows the key. 
    643       def post_key_check(key) 
    644         if scan(/\[\]/) # a[b][] indicates that b is an array 
    645           container(key, Array) 
    646           nil 
    647         elsif check(/\[[^\]]/) # a[b] indicates that a is a hash 
    648           container(key, Hash) 
    649           nil 
    650         else # End of key? We do nothing. 
    651           key 
    652         end 
    653       end 
    654  
    655       # Add a container to the stack. 
    656       def container(key, klass) 
    657         type_conflict! klass, top[key] if top.is_a?(Hash) && top.key?(key) && ! top[key].is_a?(klass) 
    658         value = bind(key, klass.new) 
    659         type_conflict! klass, value unless value.is_a?(klass) 
    660         push(value) 
    661       end 
    662  
    663       # Push a value onto the 'stack', which is actually only the top 2 items. 
    664       def push(value) 
    665         @parent, @top = @top, value 
    666       end 
    667  
    668       # Bind a key (which may be nil for items in an array) to the provided value. 
    669       def bind(key, value) 
    670         if top.is_a? Array 
    671           if key 
    672             if top[-1].is_a?(Hash) && ! top[-1].key?(key) 
    673               top[-1][key] = value 
     641        r = scan(BRACKETED_KEY_REGEXP) or break 
     642        next_key = self[1] 
     643        if next_key.blank? 
     644          raise ArgumentError, "Can't parse multi-dimensional arrays" if prev_key.blank?  
     645          pointer[prev_key] ||= [] 
     646          type_conflict!(Array, pointer[prev_key]) unless pointer[prev_key].is_a?(Array) 
     647          pointer = pointer[prev_key] 
     648        else 
     649          checkbox_key = (next_key[0...1] == CHECKBOX_HACK_PREFIX) 
     650          if checkbox_key  
     651            raise ArgumentError, "exclamation mark (!) as prefix is for form value names is reserved!" unless rest.blank? 
     652            next_key = next_key[1..-1]             
     653            if @last_key == next_key 
     654              @last_key = nil 
     655              return 
     656            end 
     657          end 
     658          if pointer.is_a?(Array) 
     659            if pointer.empty? 
     660              pointer << {}.with_indifferent_access 
     661              pointer = pointer.first 
    674662            else 
    675               top << {key => value}.with_indifferent_access 
    676               push top.last 
     663              pointer = pointer.find{|h| not h.has_key?(next_key) } || pointer.push({}.with_indifferent_access).last 
    677664            end 
    678665          else 
    679             top << value 
     666            raise ArgumentError, "don't know what to do" if prev_key.blank? 
     667            pointer[prev_key] ||= {}.with_indifferent_access 
     668            type_conflict!(Hash, pointer[prev_key]) unless pointer[prev_key].is_a?(Hash) 
     669            pointer = pointer[prev_key] 
    680670          end 
    681         elsif top.is_a? Hash 
    682           key = CGI.unescape(key) 
    683           parent << (@top = {}) if top.key?(key) && parent.is_a?(Array) 
    684           return top[key] ||= value 
     671        end 
     672        prev_key = next_key 
     673      end 
     674      # a little extra checking to avoid having to change existing tests that expect an arbitrary solution to malformed params 
     675      if rest.blank? or rest.first == '['  
     676        value = {}.with_indifferent_access if rest.first == '[' 
     677        if prev_key.blank? 
     678          type_conflict!(Array, pointer) unless pointer.is_a?(Array) 
     679          pointer << value 
    685680        else 
    686           raise ArgumentError, "Don't know what to do: top is #{top.inspect}" 
     681          type_conflict!(Hash, pointer) unless pointer.is_a?(Hash) 
     682          pointer[prev_key] = value 
    687683        end 
    688  
    689         return value 
    690684      end 
     685      @last_key = checkbox_key ? nil : prev_key 
     686    end 
    691687 
    692       def type_conflict!(klass, value) 
    693         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." 
    694       end 
     688    def type_conflict!(klass, value) 
     689      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." 
     690    end 
     691 
    695692  end 
    696693 
    697694  module UploadedFile