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 80 80 when Hash 81 81 value.with_indifferent_access 82 82 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 84 88 else 85 89 value 86 90 end … … 92 96 module Hash #:nodoc: 93 97 module IndifferentAccess #:nodoc: 94 98 def with_indifferent_access 99 return self if self.is_a?(HashWithIndifferentAccess) 100 95 101 hash = HashWithIndifferentAccess.new(self) 96 102 hash.default = self.default 97 103 hash -
actionpack/test/controller/request_test.rb
old new 456 456 ) 457 457 end 458 458 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 459 480 def test_query_string_with_nil 460 481 assert_equal( 461 482 { "action" => "create_customer", "full_name" => ''}, -
actionpack/lib/action_controller/request.rb
old new 602 602 end 603 603 end 604 604 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 # 605 621 class UrlEncodedPairParser < StringScanner #:nodoc: 606 attr_reader :top, :parent, :result 607 622 attr_reader :result 608 623 def initialize(pairs = []) 609 624 super('') 610 @result = {} 625 @result = {}.with_indifferent_access 626 @no_checkbox_hack = true 627 @last_key = nil 611 628 pairs.each { |key, value| parse(key, value) } 612 629 end 613 630 614 631 KEY_REGEXP = %r{([^\[\]=&]+)} 615 BRACKETED_KEY_REGEXP = %r{\[([^\[\]=&] +)\]}616 632 BRACKETED_KEY_REGEXP = %r{\[([^\[\]=&]*)\]} 633 CHECKBOX_HACK_PREFIX = '!' 617 634 # Parse the query string 618 635 def parse(key, value) 636 pointer = @result 619 637 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 627 640 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 674 662 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 677 664 end 678 665 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] 680 670 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 685 680 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 687 683 end 688 689 return value690 684 end 685 @last_key = checkbox_key ? nil : prev_key 686 end 691 687 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 695 692 end 696 693 697 694 module UploadedFile