313 lines
10 KiB
Ruby
313 lines
10 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
# :markup: markdown
|
|
|
|
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
|
|
# # Action Controller Params Wrapper
|
|
#
|
|
# 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 by default for JSON, and can be customized by
|
|
# setting the format array:
|
|
#
|
|
# class ApplicationController < ActionController::Base
|
|
# wrap_parameters format: [:json, :xml]
|
|
# end
|
|
#
|
|
# You could also turn it on per controller:
|
|
#
|
|
# 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 `attribute_names`.
|
|
#
|
|
# If you're going to pass the parameters to an `ActiveModel` object (such as
|
|
# `User.new(params[:user])`), 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 `Admin::User` or `User` model exists, and use it to
|
|
# determine the wrapper key respectively. If both models don't exist, it will
|
|
# then fall back to use `user` as the key.
|
|
#
|
|
# To disable this functionality for a controller:
|
|
#
|
|
# class UsersController < ApplicationController
|
|
# wrap_parameters false
|
|
# end
|
|
module ParamsWrapper
|
|
extend ActiveSupport::Concern
|
|
|
|
EXCLUDE_PARAMETERS = %w(authenticity_token _method utf8)
|
|
|
|
class Options < Struct.new(:name, :format, :include, :exclude, :klass, :model) # :nodoc:
|
|
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
|
|
@mutex = Mutex.new
|
|
@include_set = include
|
|
@name_set = name
|
|
end
|
|
|
|
def model
|
|
super || self.model = _default_wrap_model
|
|
end
|
|
|
|
def include
|
|
return super if @include_set
|
|
|
|
m = model
|
|
@mutex.synchronize do
|
|
return super if @include_set
|
|
|
|
@include_set = true
|
|
|
|
unless super || exclude
|
|
if m.respond_to?(:attribute_names) && m.attribute_names.any?
|
|
self.include = m.attribute_names
|
|
|
|
if m.respond_to?(:stored_attributes) && !m.stored_attributes.empty?
|
|
self.include += m.stored_attributes.values.flatten.map(&:to_s)
|
|
end
|
|
|
|
if m.respond_to?(:attribute_aliases) && m.attribute_aliases.any?
|
|
self.include += m.attribute_aliases.keys
|
|
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
|
|
@mutex.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.delete_suffix("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
|
|
# * `:format` - The list of formats in which the parameters wrapper will be
|
|
# enabled.
|
|
# * `:include` - The list of attribute names which parameters wrapper will
|
|
# wrap into a nested hash.
|
|
# * `:exclude` - 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
|
|
|
|
private
|
|
# Performs parameters wrapping upon the request. Called automatically by the
|
|
# metal call stack.
|
|
def process_action(*)
|
|
_perform_parameter_wrapping if _wrapper_enabled?
|
|
super
|
|
end
|
|
|
|
# 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)
|
|
elsif _wrapper_options.exclude
|
|
exclude = _wrapper_options.exclude + EXCLUDE_PARAMETERS
|
|
parameters.except(*exclude)
|
|
else
|
|
parameters.except(*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)
|
|
rescue ActionDispatch::Http::Parameters::ParseError
|
|
false
|
|
end
|
|
|
|
def _perform_parameter_wrapping
|
|
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
|
|
end
|
|
end
|