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) |
|---|
-
activesupport/lib/active_support/core_ext/hash/indifferent_access.rb
old new 79 79 when Hash 80 80 value.with_indifferent_access 81 81 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 83 87 else 84 88 value 85 89 end … … 91 95 module Hash #:nodoc: 92 96 module IndifferentAccess #:nodoc: 93 97 def with_indifferent_access 98 return self if self.is_a?(HashWithIndifferentAccess) 99 94 100 hash = HashWithIndifferentAccess.new(self) 95 101 hash.default = self.default 96 102 hash -
actionpack/test/controller/request_test.rb
old new 438 438 ) 439 439 end 440 440 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 441 462 def test_query_string_with_nil 442 463 assert_equal( 443 464 { "action" => "create_customer", "full_name" => ''}, -
actionpack/lib/action_controller/request.rb
old new 584 584 end 585 585 end 586 586 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 587 600 class UrlEncodedPairParser < StringScanner #:nodoc: 588 attr_reader :top, :parent, :result 589 601 attr_reader :result 590 602 def initialize(pairs = []) 591 603 super('') 592 @result = {} 604 @result = {}.with_indifferent_access 593 605 pairs.each { |key, value| parse(key, value) } 594 606 end 595 607 596 608 KEY_REGEXP = %r{([^\[\]=&]+)} 597 BRACKETED_KEY_REGEXP = %r{\[([^\[\]=&] +)\]}609 BRACKETED_KEY_REGEXP = %r{\[([^\[\]=&]*)\]} 598 610 599 611 # Parse the query string 600 612 def parse(key, value) 613 pointer = @result 601 614 self.string = key 602 @top, @parent = result, nil615 prev_key = scan(KEY_REGEXP) or return 603 616 604 # First scan the bare key605 key = scan(KEY_REGEXP) or return606 key = post_key_check(key)607 608 # Then scan as many nestings as present609 617 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 656 630 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 659 632 end 660 633 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] 662 638 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 667 648 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 669 651 end 670 671 return value672 652 end 653 end 673 654 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 677 659 end 678 660 679 661 module UploadedFile