aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_controller/metal/params_wrapper.rb
diff options
context:
space:
mode:
Diffstat (limited to 'actionpack/lib/action_controller/metal/params_wrapper.rb')
-rw-r--r--actionpack/lib/action_controller/metal/params_wrapper.rb296
1 files changed, 296 insertions, 0 deletions
diff --git a/actionpack/lib/action_controller/metal/params_wrapper.rb b/actionpack/lib/action_controller/metal/params_wrapper.rb
new file mode 100644
index 0000000000..7361946de5
--- /dev/null
+++ b/actionpack/lib/action_controller/metal/params_wrapper.rb
@@ -0,0 +1,296 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/slice"
+require "active_support/core_ext/hash/except"
+require "active_support/core_ext/module/anonymous"
+require "action_dispatch/http/mime_type"
+
+module ActionController
+ # Wraps the parameters hash into a nested hash. This will allow clients to
+ # submit requests without having to specify any root elements.
+ #
+ # This functionality is enabled in +config/initializers/wrap_parameters.rb+
+ # and can be customized.
+ #
+ # You could also turn it on per controller by setting the format array to
+ # a non-empty array:
+ #
+ # class UsersController < ApplicationController
+ # wrap_parameters format: [:json, :xml, :url_encoded_form, :multipart_form]
+ # end
+ #
+ # If you enable +ParamsWrapper+ for +:json+ format, instead of having to
+ # send JSON parameters like this:
+ #
+ # {"user": {"name": "Konata"}}
+ #
+ # You can send parameters like this:
+ #
+ # {"name": "Konata"}
+ #
+ # And it will be wrapped into a nested hash with the key name matching the
+ # controller's name. For example, if you're posting to +UsersController+,
+ # your new +params+ hash will look like this:
+ #
+ # {"name" => "Konata", "user" => {"name" => "Konata"}}
+ #
+ # You can also specify the key in which the parameters should be wrapped to,
+ # and also the list of attributes it should wrap by using either +:include+ or
+ # +:exclude+ options like this:
+ #
+ # class UsersController < ApplicationController
+ # wrap_parameters :person, include: [:username, :password]
+ # end
+ #
+ # On Active Record models with no +:include+ or +:exclude+ option set,
+ # it will only wrap the parameters returned by the class method
+ # <tt>attribute_names</tt>.
+ #
+ # If you're going to pass the parameters to an +ActiveModel+ object (such as
+ # <tt>User.new(params[:user])</tt>), you might consider passing the model class to
+ # the method instead. The +ParamsWrapper+ will actually try to determine the
+ # list of attribute names from the model and only wrap those attributes:
+ #
+ # class UsersController < ApplicationController
+ # wrap_parameters Person
+ # end
+ #
+ # You still could pass +:include+ and +:exclude+ to set the list of attributes
+ # you want to wrap.
+ #
+ # By default, if you don't specify the key in which the parameters would be
+ # wrapped to, +ParamsWrapper+ will actually try to determine if there's
+ # a model related to it or not. This controller, for example:
+ #
+ # class Admin::UsersController < ApplicationController
+ # end
+ #
+ # will try to check if <tt>Admin::User</tt> or +User+ model exists, and use it to
+ # determine the wrapper key respectively. If both models don't exist,
+ # it will then fallback to use +user+ as the key.
+ module ParamsWrapper
+ extend ActiveSupport::Concern
+
+ EXCLUDE_PARAMETERS = %w(authenticity_token _method utf8)
+
+ require "mutex_m"
+
+ class Options < Struct.new(:name, :format, :include, :exclude, :klass, :model) # :nodoc:
+ include Mutex_m
+
+ def self.from_hash(hash)
+ name = hash[:name]
+ format = Array(hash[:format])
+ include = hash[:include] && Array(hash[:include]).collect(&:to_s)
+ exclude = hash[:exclude] && Array(hash[:exclude]).collect(&:to_s)
+ new name, format, include, exclude, nil, nil
+ end
+
+ def initialize(name, format, include, exclude, klass, model) # :nodoc:
+ super
+ @include_set = include
+ @name_set = name
+ end
+
+ def model
+ super || synchronize { super || self.model = _default_wrap_model }
+ end
+
+ def include
+ return super if @include_set
+
+ m = model
+ synchronize do
+ return super if @include_set
+
+ @include_set = true
+
+ unless super || exclude
+ if m.respond_to?(:attribute_names) && m.attribute_names.any?
+ if m.respond_to?(:stored_attributes) && !m.stored_attributes.empty?
+ self.include = m.attribute_names + m.stored_attributes.values.flatten.map(&:to_s)
+ else
+ self.include = m.attribute_names
+ end
+
+ if m.respond_to?(:nested_attributes_options) && m.nested_attributes_options.keys.any?
+ self.include += m.nested_attributes_options.keys.map do |key|
+ key.to_s.concat("_attributes")
+ end
+ end
+
+ self.include
+ end
+ end
+ end
+ end
+
+ def name
+ return super if @name_set
+
+ m = model
+ synchronize do
+ return super if @name_set
+
+ @name_set = true
+
+ unless super || klass.anonymous?
+ self.name = m ? m.to_s.demodulize.underscore :
+ klass.controller_name.singularize
+ end
+ end
+ end
+
+ private
+ # Determine the wrapper model from the controller's name. By convention,
+ # this could be done by trying to find the defined model that has the
+ # same singular name as the controller. For example, +UsersController+
+ # will try to find if the +User+ model exists.
+ #
+ # This method also does namespace lookup. Foo::Bar::UsersController will
+ # try to find Foo::Bar::User, Foo::User and finally User.
+ def _default_wrap_model
+ return nil if klass.anonymous?
+ model_name = klass.name.sub(/Controller$/, "").classify
+
+ begin
+ if model_klass = model_name.safe_constantize
+ model_klass
+ else
+ namespaces = model_name.split("::")
+ namespaces.delete_at(-2)
+ break if namespaces.last == model_name
+ model_name = namespaces.join("::")
+ end
+ end until model_klass
+
+ model_klass
+ end
+ end
+
+ included do
+ class_attribute :_wrapper_options, default: Options.from_hash(format: [])
+ end
+
+ module ClassMethods
+ def _set_wrapper_options(options)
+ self._wrapper_options = Options.from_hash(options)
+ end
+
+ # Sets the name of the wrapper key, or the model which +ParamsWrapper+
+ # would use to determine the attribute names from.
+ #
+ # ==== Examples
+ # wrap_parameters format: :xml
+ # # enables the parameter wrapper for XML format
+ #
+ # wrap_parameters :person
+ # # wraps parameters into +params[:person]+ hash
+ #
+ # wrap_parameters Person
+ # # wraps parameters by determining the wrapper key from Person class
+ # (+person+, in this case) and the list of attribute names
+ #
+ # wrap_parameters include: [:username, :title]
+ # # wraps only +:username+ and +:title+ attributes from parameters.
+ #
+ # wrap_parameters false
+ # # disables parameters wrapping for this controller altogether.
+ #
+ # ==== Options
+ # * <tt>:format</tt> - The list of formats in which the parameters wrapper
+ # will be enabled.
+ # * <tt>:include</tt> - The list of attribute names which parameters wrapper
+ # will wrap into a nested hash.
+ # * <tt>:exclude</tt> - The list of attribute names which parameters wrapper
+ # will exclude from a nested hash.
+ def wrap_parameters(name_or_model_or_options, options = {})
+ model = nil
+
+ case name_or_model_or_options
+ when Hash
+ options = name_or_model_or_options
+ when false
+ options = options.merge(format: [])
+ when Symbol, String
+ options = options.merge(name: name_or_model_or_options)
+ else
+ model = name_or_model_or_options
+ end
+
+ opts = Options.from_hash _wrapper_options.to_h.slice(:format).merge(options)
+ opts.model = model
+ opts.klass = self
+
+ self._wrapper_options = opts
+ end
+
+ # Sets the default wrapper key or model which will be used to determine
+ # wrapper key and attribute names. Called automatically when the
+ # module is inherited.
+ def inherited(klass)
+ if klass._wrapper_options.format.any?
+ params = klass._wrapper_options.dup
+ params.klass = klass
+ klass._wrapper_options = params
+ end
+ super
+ end
+ end
+
+ # Performs parameters wrapping upon the request. Called automatically
+ # by the metal call stack.
+ def process_action(*args)
+ if _wrapper_enabled?
+ wrapped_hash = _wrap_parameters request.request_parameters
+ wrapped_keys = request.request_parameters.keys
+ wrapped_filtered_hash = _wrap_parameters request.filtered_parameters.slice(*wrapped_keys)
+
+ # This will make the wrapped hash accessible from controller and view.
+ request.parameters.merge! wrapped_hash
+ request.request_parameters.merge! wrapped_hash
+
+ # This will display the wrapped hash in the log file.
+ request.filtered_parameters.merge! wrapped_filtered_hash
+ end
+ ensure
+ # NOTE: Rescues all exceptions so they
+ # may be caught in ActionController::Rescue.
+ return super
+ end
+
+ private
+
+ # Returns the wrapper key which will be used to store wrapped parameters.
+ def _wrapper_key
+ _wrapper_options.name
+ end
+
+ # Returns the list of enabled formats.
+ def _wrapper_formats
+ _wrapper_options.format
+ end
+
+ # Returns the list of parameters which will be selected for wrapped.
+ def _wrap_parameters(parameters)
+ { _wrapper_key => _extract_parameters(parameters) }
+ end
+
+ def _extract_parameters(parameters)
+ if include_only = _wrapper_options.include
+ parameters.slice(*include_only)
+ else
+ exclude = _wrapper_options.exclude || []
+ parameters.except(*(exclude + EXCLUDE_PARAMETERS))
+ end
+ end
+
+ # Checks if we should perform parameters wrapping.
+ def _wrapper_enabled?
+ return false unless request.has_content_type?
+
+ ref = request.content_mime_type.ref
+ _wrapper_formats.include?(ref) && _wrapper_key && !request.parameters.key?(_wrapper_key)
+ end
+ end
+end