Skip to content

Commit d1dc4dd

Browse files
authored
Merge pull request rails#53471 from matthewd/rack-2-parsing
Maintain Rack 2 parameter parsing behaviour
2 parents 723d410 + ae60fce commit d1dc4dd

File tree

5 files changed

+190
-10
lines changed

5 files changed

+190
-10
lines changed

actionpack/lib/action_dispatch/http/param_builder.rb

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
module ActionDispatch
44
class ParamBuilder
5+
# --
6+
# This implementation is based on Rack::QueryParser,
7+
# Copyright (C) 2007-2021 Leah Neukirchen <http://leahneukirchen.org/infopage.html>
8+
59
def self.make_default(param_depth_limit)
610
new param_depth_limit
711
end
@@ -12,6 +16,10 @@ def initialize(param_depth_limit)
1216
@param_depth_limit = param_depth_limit
1317
end
1418

19+
cattr_accessor :ignore_leading_brackets
20+
21+
LEADING_BRACKETS_COMPAT = defined?(::Rack::RELEASE) && ::Rack::RELEASE.to_s.start_with?("2.")
22+
1523
cattr_accessor :default
1624
self.default = make_default(100)
1725

@@ -61,15 +69,30 @@ def store_nested_param(params, name, v, depth, encoding_template = nil)
6169
# nil name, treat same as empty string (required by tests)
6270
k = after = ""
6371
elsif depth == 0
64-
# Start of parsing, don't treat [] or [ at start of string specially
65-
if start = name.index("[", 1)
66-
# Start of parameter nesting, use part before brackets as key
67-
k = name[0, start]
68-
after = name[start, name.length]
72+
if ignore_leading_brackets || (ignore_leading_brackets.nil? && LEADING_BRACKETS_COMPAT)
73+
# Rack 2 compatible behavior, ignore leading brackets
74+
if name =~ /\A[\[\]]*([^\[\]]+)\]*/
75+
k = $1
76+
after = $' || ""
77+
78+
if !ignore_leading_brackets && (k != $& || !after.empty? && !after.start_with?("["))
79+
ActionDispatch.deprecator.warn("Skipping over leading brackets in parameter name #{name.inspect} is deprecated and will parse differently in Rails 8.1 or Rack 3.0.")
80+
end
81+
else
82+
k = name
83+
after = ""
84+
end
6985
else
70-
# Plain parameter with no nesting
71-
k = name
72-
after = ""
86+
# Start of parsing, don't treat [] or [ at start of string specially
87+
if start = name.index("[", 1)
88+
# Start of parameter nesting, use part before brackets as key
89+
k = name[0, start]
90+
after = name[start, name.length]
91+
else
92+
# Plain parameter with no nesting
93+
k = name
94+
after = ""
95+
end
7396
end
7497
elsif name.start_with?("[]")
7598
# Array nesting

actionpack/lib/action_dispatch/http/query_parser.rb

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
# frozen_string_literal: true
22

33
require "uri"
4+
require "rack"
45

56
module ActionDispatch
67
class QueryParser
78
DEFAULT_SEP = /& */n
8-
COMMON_SEP = { ";" => /; */n, ";," => /[;,] */n, "&" => /& */n }
9+
COMPAT_SEP = /[&;] */n
10+
COMMON_SEP = { ";" => /; */n, ";," => /[;,] */n, "&" => /& */n, "&;" => /[&;] */n }
11+
12+
cattr_accessor :strict_query_string_separator
13+
14+
SEMICOLON_COMPAT = defined?(::Rack::QueryParser::DEFAULT_SEP) && ::Rack::QueryParser::DEFAULT_SEP.to_s.include?(";")
915

1016
#--
1117
# Note this departs from WHATWG's specified parsing algorithm by
@@ -14,7 +20,23 @@ class QueryParser
1420
def self.each_pair(s, separator = nil)
1521
return enum_for(:each_pair, s, separator) unless block_given?
1622

17-
(s || "").split(separator ? (COMMON_SEP[separator] || /[#{separator}] */n) : DEFAULT_SEP).each do |part|
23+
s ||= ""
24+
25+
splitter =
26+
if separator
27+
COMMON_SEP[separator] || /[#{separator}] */n
28+
elsif strict_query_string_separator
29+
DEFAULT_SEP
30+
elsif SEMICOLON_COMPAT && s.include?(";")
31+
if strict_query_string_separator.nil?
32+
ActionDispatch.deprecator.warn("Using semicolon as a query string separator is deprecated and will not be supported in Rails 8.1 or Rack 3.0. Use `&` instead.")
33+
end
34+
COMPAT_SEP
35+
else
36+
DEFAULT_SEP
37+
end
38+
39+
s.split(splitter).each do |part|
1840
next if part.empty?
1941

2042
k, v = part.split("=", 2)

actionpack/lib/action_dispatch/railtie.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ class Railtie < Rails::Railtie # :nodoc:
3131
config.action_dispatch.debug_exception_log_level = :fatal
3232
config.action_dispatch.strict_freshness = false
3333

34+
config.action_dispatch.ignore_leading_brackets = nil
35+
config.action_dispatch.strict_query_string_separator = nil
36+
3437
config.action_dispatch.default_headers = {
3538
"X-Frame-Options" => "SAMEORIGIN",
3639
"X-XSS-Protection" => "1; mode=block",
@@ -52,6 +55,9 @@ class Railtie < Rails::Railtie # :nodoc:
5255
ActionDispatch::Http::URL.secure_protocol = app.config.force_ssl
5356
ActionDispatch::Http::URL.tld_length = app.config.action_dispatch.tld_length
5457

58+
ActionDispatch::ParamBuilder.ignore_leading_brackets = app.config.action_dispatch.ignore_leading_brackets
59+
ActionDispatch::QueryParser.strict_query_string_separator = app.config.action_dispatch.strict_query_string_separator
60+
5561
ActiveSupport.on_load(:action_dispatch_request) do
5662
self.ignore_accept_header = app.config.action_dispatch.ignore_accept_header
5763
ActionDispatch::Request::Utils.perform_deep_munge = app.config.action_dispatch.perform_deep_munge
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# frozen_string_literal: true
2+
3+
require "abstract_unit"
4+
5+
class ParamBuilderTest < ActiveSupport::TestCase
6+
# Much of the behavioral details are covered by long-standing
7+
# integration tests in test/request/query_string_parsing_test.rb
8+
#
9+
# This test doesn't need to duplicate all of that: it just
10+
# offers a simple baseline of unit tests.
11+
12+
test "simple query string" do
13+
result = ActionDispatch::ParamBuilder.from_query_string("foo=bar&baz=quux")
14+
assert_equal({ "foo" => "bar", "baz" => "quux" }, result)
15+
assert_instance_of ActiveSupport::HashWithIndifferentAccess, result
16+
end
17+
18+
test "nested parameters" do
19+
result = ActionDispatch::ParamBuilder.from_query_string("foo[bar]=baz")
20+
assert_equal({ "foo" => { "bar" => "baz" } }, result)
21+
assert_instance_of ActiveSupport::HashWithIndifferentAccess, result[:foo]
22+
end
23+
24+
if ::Rack::RELEASE.start_with?("2.")
25+
test "(rack 2) defaults to ignoring leading bracket" do
26+
assert_deprecated(ActionDispatch.deprecator) do
27+
result = ActionDispatch::ParamBuilder.from_query_string("[foo]=bar")
28+
assert_equal({ "foo" => "bar" }, result)
29+
end
30+
31+
assert_deprecated(ActionDispatch.deprecator) do
32+
result = ActionDispatch::ParamBuilder.from_query_string("[foo][bar]=baz")
33+
assert_equal({ "foo" => { "bar" => "baz" } }, result)
34+
end
35+
end
36+
else
37+
test "(rack 3) defaults to retaining leading bracket" do
38+
result = ActionDispatch::ParamBuilder.from_query_string("[foo]=bar")
39+
assert_equal({ "[foo]" => "bar" }, result)
40+
41+
result = ActionDispatch::ParamBuilder.from_query_string("[foo][bar]=baz")
42+
assert_equal({ "[foo]" => { "bar" => "baz" } }, result)
43+
end
44+
end
45+
46+
test "configured for strict brackets" do
47+
previous_brackets = ActionDispatch::ParamBuilder.ignore_leading_brackets
48+
ActionDispatch::ParamBuilder.ignore_leading_brackets = false
49+
50+
result = ActionDispatch::ParamBuilder.from_query_string("[foo]=bar")
51+
assert_equal({ "[foo]" => "bar" }, result)
52+
53+
result = ActionDispatch::ParamBuilder.from_query_string("[foo][bar]=baz")
54+
assert_equal({ "[foo]" => { "bar" => "baz" } }, result)
55+
ensure
56+
ActionDispatch::ParamBuilder.ignore_leading_brackets = previous_brackets
57+
end
58+
59+
test "configured for ignoring leading brackets" do
60+
previous_brackets = ActionDispatch::ParamBuilder.ignore_leading_brackets
61+
ActionDispatch::ParamBuilder.ignore_leading_brackets = true
62+
63+
result = ActionDispatch::ParamBuilder.from_query_string("[foo]=bar")
64+
assert_equal({ "foo" => "bar" }, result)
65+
66+
result = ActionDispatch::ParamBuilder.from_query_string("[foo][bar]=baz")
67+
assert_equal({ "foo" => { "bar" => "baz" } }, result)
68+
ensure
69+
ActionDispatch::ParamBuilder.ignore_leading_brackets = previous_brackets
70+
end
71+
end
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# frozen_string_literal: true
2+
3+
require "abstract_unit"
4+
5+
class QueryParserTest < ActiveSupport::TestCase
6+
test "simple query string" do
7+
assert_equal [["foo", "bar"], ["baz", "quux"]], parsed_pairs("foo=bar&baz=quux")
8+
end
9+
10+
test "query string with empty and missing values" do
11+
assert_equal [["foo", "bar"], ["empty", ""], ["missing", nil], ["baz", "quux"]], parsed_pairs("foo=bar&empty=&missing&baz=quux")
12+
end
13+
14+
test "custom separator" do
15+
assert_equal [["foo", "bar"], ["baz", "quux"]], parsed_pairs("foo=bar;baz=quux", ";")
16+
end
17+
18+
test "non-standard separator" do
19+
assert_equal [["foo", "bar"], ["baz", "quux"]], parsed_pairs("foo=bar/baz=quux", "/")
20+
end
21+
22+
test "mixed separators" do
23+
assert_equal [["a", "aa"], ["b", "bb"], ["c", "cc"]], parsed_pairs("a=aa&b=bb;c=cc", "&;")
24+
end
25+
26+
if ::Rack::RELEASE.start_with?("2.")
27+
test "(rack 2) defaults to mixed separators" do
28+
assert_deprecated(ActionDispatch.deprecator) do
29+
assert_equal [["a", "aa"], ["b", "bb"], ["c", "cc"]], parsed_pairs("a=aa&b=bb;c=cc")
30+
end
31+
end
32+
else
33+
test "(rack 3) defaults to ampersand separator only" do
34+
assert_equal [["a", "aa"], ["b", "bb;c=cc"]], parsed_pairs("a=aa&b=bb;c=cc")
35+
end
36+
end
37+
38+
test "configured for strict separator" do
39+
previous_separator = ActionDispatch::QueryParser.strict_query_string_separator
40+
ActionDispatch::QueryParser.strict_query_string_separator = true
41+
assert_equal [["a", "aa"], ["b", "bb;c=cc"]], parsed_pairs("a=aa&b=bb;c=cc", "&")
42+
ensure
43+
ActionDispatch::QueryParser.strict_query_string_separator = previous_separator
44+
end
45+
46+
test "configured for mixed separator" do
47+
previous_separator = ActionDispatch::QueryParser.strict_query_string_separator
48+
ActionDispatch::QueryParser.strict_query_string_separator = false
49+
assert_equal [["a", "aa"], ["b", "bb"], ["c", "cc"]], parsed_pairs("a=aa&b=bb;c=cc", "&;")
50+
ensure
51+
ActionDispatch::QueryParser.strict_query_string_separator = previous_separator
52+
end
53+
54+
private
55+
def parsed_pairs(query, separator = nil)
56+
ActionDispatch::QueryParser.each_pair(query, separator).to_a
57+
end
58+
end

0 commit comments

Comments
 (0)