# frozen_string_literal: true
module ActionDispatch
class ParamBuilder
# --
# This implementation is based on Rack::QueryParser,
# Copyright (C) 2007-2021 Leah Neukirchen <http://leahneukirchen.org/infopage.html>
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 :ignore_leading_brackets
LEADING_BRACKETS_COMPAT = defined?(::Rack::RELEASE) && ::Rack::RELEASE.to_s.start_with?("2.")
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
if ignore_leading_brackets || (ignore_leading_brackets.nil? && LEADING_BRACKETS_COMPAT)
# Rack 2 compatible behavior, ignore leading brackets
if name =~ /\A[\[\]]*([^\[\]]+)\]*/
k = $1
after = $' || ""
if !ignore_leading_brackets && (k != $& || !after.empty? && !after.start_with?("["))
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.")
end
else
k = name
after = ""
end
else
# 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
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