forked from rails/rails
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request rails#53193 from matthewd/query-parsing
Do more request parameter parsing ourselves
- Loading branch information
Showing
15 changed files
with
432 additions
and
39 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
# frozen_string_literal: true | ||
|
||
module ActionDispatch | ||
class ParamBuilder | ||
def self.make_default(param_depth_limit) | ||
new param_depth_limit | ||
end | ||
|
||
attr_reader :param_depth_limit | ||
|
||
def initialize(param_depth_limit) | ||
@param_depth_limit = param_depth_limit | ||
end | ||
|
||
cattr_accessor :default | ||
self.default = make_default(100) | ||
|
||
class << self | ||
delegate :from_query_string, :from_pairs, :from_hash, to: :default | ||
end | ||
|
||
def from_query_string(qs, separator: nil, encoding_template: nil) | ||
from_pairs QueryParser.each_pair(qs, separator), encoding_template: encoding_template | ||
end | ||
|
||
def from_pairs(pairs, encoding_template: nil) | ||
params = make_params | ||
|
||
pairs.each do |k, v| | ||
if Hash === v | ||
v = ActionDispatch::Http::UploadedFile.new(v) | ||
end | ||
|
||
store_nested_param(params, k, v, 0, encoding_template) | ||
end | ||
|
||
params | ||
rescue ArgumentError => e | ||
raise InvalidParameterError, e.message, e.backtrace | ||
end | ||
|
||
def from_hash(hash, encoding_template: nil) | ||
# Force encodings from encoding template | ||
hash = Request::Utils::CustomParamEncoder.encode_for_template(hash, encoding_template) | ||
|
||
# Assert valid encoding | ||
Request::Utils.check_param_encoding(hash) | ||
|
||
# Convert hashes to HWIA (or UploadedFile), and deep-munge nils | ||
# out of arrays | ||
hash = Request::Utils.normalize_encode_params(hash) | ||
|
||
hash | ||
end | ||
|
||
private | ||
def store_nested_param(params, name, v, depth, encoding_template = nil) | ||
raise ParamsTooDeepError if depth >= param_depth_limit | ||
|
||
if !name | ||
# nil name, treat same as empty string (required by tests) | ||
k = after = "" | ||
elsif depth == 0 | ||
# Start of parsing, don't treat [] or [ at start of string specially | ||
if start = name.index("[", 1) | ||
# Start of parameter nesting, use part before brackets as key | ||
k = name[0, start] | ||
after = name[start, name.length] | ||
else | ||
# Plain parameter with no nesting | ||
k = name | ||
after = "" | ||
end | ||
elsif name.start_with?("[]") | ||
# Array nesting | ||
k = "[]" | ||
after = name[2, name.length] | ||
elsif name.start_with?("[") && (start = name.index("]", 1)) | ||
# Hash nesting, use the part inside brackets as the key | ||
k = name[1, start - 1] | ||
after = name[start + 1, name.length] | ||
else | ||
# Probably malformed input, nested but not starting with [ | ||
# treat full name as key for backwards compatibility. | ||
k = name | ||
after = "" | ||
end | ||
|
||
return if k.empty? | ||
|
||
if depth == 0 && String === v | ||
# We have to wait until we've found the top part of the name, | ||
# because that's what the encoding template is configured with | ||
if encoding_template && (designated_encoding = encoding_template[k]) && !v.frozen? | ||
v.force_encoding(designated_encoding) | ||
end | ||
|
||
# ... and we can't validate the encoding until after we've | ||
# applied any template override | ||
unless v.valid_encoding? | ||
raise InvalidParameterError, "Invalid encoding for parameter: #{v.scrub}" | ||
end | ||
end | ||
|
||
if after == "" | ||
if k == "[]" && depth != 0 | ||
return (v || !ActionDispatch::Request::Utils.perform_deep_munge) ? [v] : [] | ||
else | ||
params[k] = v | ||
end | ||
elsif after == "[" | ||
params[name] = v | ||
elsif after == "[]" | ||
params[k] ||= [] | ||
raise ParameterTypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array) | ||
params[k] << v if v || !ActionDispatch::Request::Utils.perform_deep_munge | ||
elsif after.start_with?("[]") | ||
# Recognize x[][y] (hash inside array) parameters | ||
unless after[2] == "[" && after.end_with?("]") && (child_key = after[3, after.length - 4]) && !child_key.empty? && !child_key.index("[") && !child_key.index("]") | ||
# Handle other nested array parameters | ||
child_key = after[2, after.length] | ||
end | ||
params[k] ||= [] | ||
raise ParameterTypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array) | ||
if params_hash_type?(params[k].last) && !params_hash_has_key?(params[k].last, child_key) | ||
store_nested_param(params[k].last, child_key, v, depth + 1) | ||
else | ||
params[k] << store_nested_param(make_params, child_key, v, depth + 1) | ||
end | ||
else | ||
params[k] ||= make_params | ||
raise ParameterTypeError, "expected Hash (got #{params[k].class.name}) for param `#{k}'" unless params_hash_type?(params[k]) | ||
params[k] = store_nested_param(params[k], after, v, depth + 1) | ||
end | ||
|
||
params | ||
end | ||
|
||
def make_params | ||
ActiveSupport::HashWithIndifferentAccess.new | ||
end | ||
|
||
def new_depth_limit(param_depth_limit) | ||
self.class.new @params_class, param_depth_limit | ||
end | ||
|
||
def params_hash_type?(obj) | ||
Hash === obj | ||
end | ||
|
||
def params_hash_has_key?(hash, key) | ||
return false if key.include?("[]") | ||
|
||
key.split(/[\[\]]+/).inject(hash) do |h, part| | ||
next h if part == "" | ||
return false unless params_hash_type?(h) && h.key?(part) | ||
h[part] | ||
end | ||
|
||
true | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
# frozen_string_literal: true | ||
|
||
module ActionDispatch | ||
class ParamError < ActionDispatch::Http::Parameters::ParseError | ||
def initialize(message = nil) | ||
super | ||
end | ||
|
||
def self.===(other) | ||
super || ( | ||
defined?(Rack::Utils::ParameterTypeError) && Rack::Utils::ParameterTypeError === other || | ||
defined?(Rack::Utils::InvalidParameterError) && Rack::Utils::InvalidParameterError === other || | ||
defined?(Rack::QueryParser::ParamsTooDeepError) && Rack::QueryParser::ParamsTooDeepError === other | ||
) | ||
end | ||
end | ||
|
||
class ParameterTypeError < ParamError | ||
end | ||
|
||
class InvalidParameterError < ParamError | ||
end | ||
|
||
class ParamsTooDeepError < ParamError | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
# frozen_string_literal: true | ||
|
||
require "uri" | ||
|
||
module ActionDispatch | ||
class QueryParser | ||
DEFAULT_SEP = /& */n | ||
COMMON_SEP = { ";" => /; */n, ";," => /[;,] */n, "&" => /& */n } | ||
|
||
#-- | ||
# Note this departs from WHATWG's specified parsing algorithm by | ||
# giving a nil value for keys that do not use '='. Callers that need | ||
# the standard's interpretation can use `v.to_s`. | ||
def self.each_pair(s, separator = nil) | ||
return enum_for(:each_pair, s, separator) unless block_given? | ||
|
||
(s || "").split(separator ? (COMMON_SEP[separator] || /[#{separator}] */n) : DEFAULT_SEP).each do |part| | ||
next if part.empty? | ||
|
||
k, v = part.split("=", 2) | ||
|
||
k = URI.decode_www_form_component(k) | ||
v &&= URI.decode_www_form_component(v) | ||
|
||
yield k, v | ||
end | ||
|
||
nil | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.