require 'date' require 'yaml' require 'bigdecimal' require 'stringio' require 'active_support/concern' require 'active_support/core_ext/hash/indifferent_access' require 'active_support/core_ext/array/wrap' module RubyPitaya class ParameterMissing < IndexError attr_reader :param, :keys def initialize(param, keys = nil) @param = param @keys = keys super("param is missing or the value is empty: #{param}") end class Correction def initialize(error) @error = error end def corrections if @error.param && @error.keys maybe_these = @error.keys maybe_these.sort_by { |n| DidYouMean::Jaro.distance(@error.param.to_s, n) }.reverse.first(4) else [] end end end # We may not have DYM, and DYM might not let us register error handlers if defined?(DidYouMean) && DidYouMean.respond_to?(:correct_error) DidYouMean.correct_error(self, Correction) end end class UnpermittedParameters < IndexError attr_reader :params def initialize(params) @params = params super("found unpermitted parameters: #{params.join(", ")}") end end class UnfilteredParameters < ArgumentError def initialize # :nodoc: super("unable to convert unpermitted parameters to hash") end end class Parameters cattr_accessor :permit_all_parameters, instance_accessor: false, default: false cattr_accessor :action_on_unpermitted_parameters, instance_accessor: false delegate :keys, :key?, :has_key?, :member?, :values, :has_value?, :value?, :empty?, :include?, :as_json, :to_s, :each_key, to: :@parameters cattr_accessor :always_permitted_parameters, default: %w( controller action ) class << self def nested_attribute?(key, value) # :nodoc: /\A-?\d+\z/.match?(key) && (value.is_a?(Hash) || value.is_a?(Parameters)) end end def initialize(parameters = {}) @parameters = parameters.with_indifferent_access @permitted = self.class.permit_all_parameters end def ==(other) if other.respond_to?(:permitted?) permitted? == other.permitted? && parameters == other.parameters else @parameters == other end end alias eql? == def hash [@parameters.hash, @permitted].hash end def to_h if permitted? convert_parameters_to_hashes(@parameters, :to_h) else raise UnfilteredParameters end end def to_hash to_h.to_hash end def to_query(*args) to_h.to_query(*args) end alias_method :to_param, :to_query def to_unsafe_h convert_parameters_to_hashes(@parameters, :to_unsafe_h) end alias_method :to_unsafe_hash, :to_unsafe_h def each_pair(&block) return to_enum(__callee__) unless block_given? @parameters.each_pair do |key, value| yield [key, convert_hashes_to_parameters(key, value)] end self end alias_method :each, :each_pair def each_value(&block) return to_enum(:each_value) unless block_given? @parameters.each_pair do |key, value| yield convert_hashes_to_parameters(key, value) end self end def converted_arrays @converted_arrays ||= Set.new end def permitted? @permitted end def permit! each_pair do |key, value| Array.wrap(value).flatten.each do |v| v.permit! if v.respond_to? :permit! end end @permitted = true self end def require(key) return key.map { |k| require(k) } if key.is_a?(Array) value = self[key] if value.present? || value == false value else raise ParameterMissing.new(key, @parameters.keys) end end alias :required :require def permit(*filters) params = self.class.new filters.flatten.each do |filter| case filter when Symbol, String permitted_scalar_filter(params, filter) when Hash hash_filter(params, filter) end end unpermitted_parameters!(params) if self.class.action_on_unpermitted_parameters params.permit! end def [](key) convert_hashes_to_parameters(key, @parameters[key]) end def []=(key, value) @parameters[key] = value end def fetch(key, *args) convert_value_to_parameters( @parameters.fetch(key) { if block_given? yield else args.fetch(0) { raise ActionController::ParameterMissing.new(key, @parameters.keys) } end } ) end def dig(*keys) convert_hashes_to_parameters(keys.first, @parameters[keys.first]) @parameters.dig(*keys) end def slice!(*keys) @parameters.slice!(*keys) self end def except(*keys) new_instance_with_inherited_permitted_status(@parameters.except(*keys)) end def extract!(*keys) new_instance_with_inherited_permitted_status(@parameters.extract!(*keys)) end def transform_values return to_enum(:transform_values) unless block_given? new_instance_with_inherited_permitted_status( @parameters.transform_values { |v| yield convert_value_to_parameters(v) } ) end def transform_values! return to_enum(:transform_values!) unless block_given? @parameters.transform_values! { |v| yield convert_value_to_parameters(v) } self end def transform_keys(&block) return to_enum(:transform_keys) unless block_given? new_instance_with_inherited_permitted_status( @parameters.transform_keys(&block) ) end def transform_keys!(&block) return to_enum(:transform_keys!) unless block_given? @parameters.transform_keys!(&block) self end def deep_transform_keys(&block) new_instance_with_inherited_permitted_status( @parameters.deep_transform_keys(&block) ) end def deep_transform_keys!(&block) @parameters.deep_transform_keys!(&block) self end def delete(key, &block) convert_value_to_parameters(@parameters.delete(key, &block)) end def select(&block) new_instance_with_inherited_permitted_status(@parameters.select(&block)) end def select!(&block) @parameters.select!(&block) self end alias_method :keep_if, :select! def reject(&block) new_instance_with_inherited_permitted_status(@parameters.reject(&block)) end def reject!(&block) @parameters.reject!(&block) self end alias_method :delete_if, :reject! def compact new_instance_with_inherited_permitted_status(@parameters.compact) end def compact! self if @parameters.compact! end def compact_blank reject { |_k, v| v.blank? } end def compact_blank! reject! { |_k, v| v.blank? } end def values_at(*keys) convert_value_to_parameters(@parameters.values_at(*keys)) end def merge(other_hash) new_instance_with_inherited_permitted_status( @parameters.merge(other_hash.to_h) ) end def merge!(other_hash) @parameters.merge!(other_hash.to_h) self end def reverse_merge(other_hash) new_instance_with_inherited_permitted_status( other_hash.to_h.merge(@parameters) ) end alias_method :with_defaults, :reverse_merge def reverse_merge!(other_hash) @parameters.merge!(other_hash.to_h) { |key, left, right| left } self end alias_method :with_defaults!, :reverse_merge! def stringify_keys dup end def inspect "#<#{self.class} #{@parameters} permitted: #{@permitted}>" end def self.hook_into_yaml_loading YAML.load_tags["!ruby/hash-with-ivars:ActionController::Parameters"] = name YAML.load_tags["!ruby/hash:ActionController::Parameters"] = name end hook_into_yaml_loading def init_with(coder) case coder.tag when "!ruby/hash:ActionController::Parameters" @parameters = coder.map.with_indifferent_access @permitted = false when "!ruby/hash-with-ivars:ActionController::Parameters" @parameters = coder.map["elements"].with_indifferent_access @permitted = coder.map["ivars"][:@permitted] when "!ruby/object:ActionController::Parameters" @parameters, @permitted = coder.map["parameters"], coder.map["permitted"] end end def deep_dup self.class.new(@parameters.deep_dup).tap do |duplicate| duplicate.permitted = @permitted end end protected attr_reader :parameters attr_writer :permitted def nested_attributes? @parameters.any? { |k, v| Parameters.nested_attribute?(k, v) } end def each_nested_attribute hash = self.class.new self.each { |k, v| hash[k] = yield v if Parameters.nested_attribute?(k, v) } hash end private def new_instance_with_inherited_permitted_status(hash) self.class.new(hash).tap do |new_instance| new_instance.permitted = @permitted end end def convert_parameters_to_hashes(value, using) case value when Array value.map { |v| convert_parameters_to_hashes(v, using) } when Hash value.transform_values do |v| convert_parameters_to_hashes(v, using) end.with_indifferent_access when Parameters value.send(using) else value end end def convert_hashes_to_parameters(key, value) converted = convert_value_to_parameters(value) @parameters[key] = converted unless converted.equal?(value) converted end def convert_value_to_parameters(value) case value when Array return value if converted_arrays.member?(value) converted = value.map { |_| convert_value_to_parameters(_) } converted_arrays << converted converted when Hash self.class.new(value) else value end end def each_element(object, &block) case object when Array object.grep(Parameters).map { |el| yield el }.compact when Parameters if object.nested_attributes? object.each_nested_attribute(&block) else yield object end end end def unpermitted_parameters!(params) unpermitted_keys = unpermitted_keys(params) if unpermitted_keys.any? case self.class.action_on_unpermitted_parameters when :log name = "unpermitted_parameters.action_controller" ActiveSupport::Notifications.instrument(name, keys: unpermitted_keys) when :raise raise ActionController::UnpermittedParameters.new(unpermitted_keys) end end end def unpermitted_keys(params) keys - params.keys - always_permitted_parameters end PERMITTED_SCALAR_TYPES = [ String, Symbol, NilClass, Numeric, TrueClass, FalseClass, Date, Time, # DateTimes are Dates, we document the type but avoid the redundant check. StringIO, IO, ] def permitted_scalar?(value) PERMITTED_SCALAR_TYPES.any? { |type| value.is_a?(type) } end def permitted_scalar_filter(params, permitted_key) permitted_key = permitted_key.to_s if has_key?(permitted_key) && permitted_scalar?(self[permitted_key]) params[permitted_key] = self[permitted_key] end each_key do |key| next unless key =~ /\(\d+[if]?\)\z/ next unless $~.pre_match == permitted_key params[key] = self[key] if permitted_scalar?(self[key]) end end def array_of_permitted_scalars?(value) if value.is_a?(Array) && value.all? { |element| permitted_scalar?(element) } yield value end end def non_scalar?(value) value.is_a?(Array) || value.is_a?(Parameters) end EMPTY_ARRAY = [] EMPTY_HASH = {} def hash_filter(params, filter) filter = filter.with_indifferent_access # Slicing filters out non-declared keys. slice(*filter.keys).each do |key, value| next unless value next unless has_key? key if filter[key] == EMPTY_ARRAY # Declaration { comment_ids: [] }. array_of_permitted_scalars?(self[key]) do |val| params[key] = val end elsif filter[key] == EMPTY_HASH # Declaration { preferences: {} }. if value.is_a?(Parameters) params[key] = permit_any_in_parameters(value) end elsif non_scalar?(value) # Declaration { user: :name } or { user: [:name, :age, { address: ... }] }. params[key] = each_element(value) do |element| element.permit(*Array.wrap(filter[key])) end end end end def permit_any_in_parameters(params) self.class.new.tap do |sanitized| params.each do |key, value| case value when ->(v) { permitted_scalar?(v) } sanitized[key] = value when Array sanitized[key] = permit_any_in_array(value) when Parameters sanitized[key] = permit_any_in_parameters(value) else # Filter this one out. end end end end def permit_any_in_array(array) [].tap do |sanitized| array.each do |element| case element when ->(e) { permitted_scalar?(e) } sanitized << element when Parameters sanitized << permit_any_in_parameters(element) else # Filter this one out. end end end end def initialize_copy(source) super @parameters = @parameters.dup end end end