aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_controller/metal/params_wrapper.rb
blob: 29ff546139dfb159dfea48f55fe5276601d52820 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
require 'active_support/core_ext/class/attribute'
require 'action_dispatch/http/mime_types'

module ActionController
  # Wraps parameters hash into nested hash. This will allow client to submit
  # POST request without having to specify a root element in it.
  #
  # By default, this functionality won't be enabled by default. You can enable
  # it globally by setting +ActionController::Base.wrap_parameters+:
  #
  #     ActionController::Base.wrap_parameters = [:json]
  #
  # You could also turn it on per controller by setting the format array to
  # non-empty array:
  #
  #     class UsersController < ApplicationController
  #       wrap_parameters :format => [:json, :xml]
  #     end
  #
  # If you enable +ParamsWrapper+ for +:json+ format. Instead of having to
  # send JSON parameters like this:
  #
  #     {"user": {"name": "Konata"}}
  #
  # You can now just send a parameters like this:
  #
  #     {"name": "Konata"}
  #
  # And it will be wrapped into a nested hash with the key name matching
  # 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 +:only+ or
  # +:except+ options like this:
  #
  #     class UsersController < ApplicationController
  #       wrap_parameters :person, :only => [:username, :password]
  #     end
  #
  # 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 +:only+ and +:except+ 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 of the model doesn't exists,
  # it will then fallback to use +user+ as the key.
  module ParamsWrapper
    extend ActiveSupport::Concern

    EXCLUDE_PARAMETERS = %w(authenticity_token _method utf8)

    included do
      class_attribute :_wrapper_options
      self._wrapper_options = {:format => []}
    end

    module ClassMethods
      # 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 parmeter wrappes for XML format
      #
      #   wrap_parameters :person
      #     # wraps parameters into +params[:person]+ hash
      #
      #   wrap_parameters Person
      #     # wraps parameters by determine the wrapper key from Person class
      #     (+person+, in this case) and the list of attribute names
      #
      #   wrap_parameters :only => [:username, :title]
      #     # wraps only +:username+ and +:title+ attributes from parameters.
      #
      #   wrap_parameters false
      #     # disable parameters wrapping for this controller altogether.
      #
      # ==== Options
      # * <tt>:format</tt> - The list of formats in which the parameters wrapper
      #   will be enabled.
      # * <tt>:only</tt> - The list of attribute names which parmeters wrapper
      #   will wrap into a nested hash.
      # * <tt>:only</tt> - The list of attribute names which parmeters wrapper
      #   will exclude from a nested hash.
      def wrap_parameters(name_or_model_or_options, options = {})
        if !name_or_model_or_options.is_a? Hash
          if name_or_model_or_options != false
            options = options.merge(:name_or_model => name_or_model_or_options)
          else
            options = opions.merge(:format => [])
          end
        else
          options = name_or_model_or_options
        end

        options[:name_or_model] ||= _default_wrap_model
        self._wrapper_options = self._wrapper_options.merge(options)
      end

      # Sets the default wrapper key or model which will be used to determine
      # wrapper key and attribute names. Will be called automatically when the
      # module is inherited.
      def inherited(klass)
        if klass._wrapper_options[:format].present?
          klass._wrapper_options = klass._wrapper_options.merge(:name_or_model => klass._default_wrap_model)
        end
        super
      end

      # 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 singularize name as the controller. For example, +UsersController+
      # will try to find if the +User+ model exists.
      def _default_wrap_model
        model_name = self.name.sub(/Controller$/, '').singularize

        begin
          model_klass = model_name.constantize
        rescue NameError => e
          unscoped_model_name = model_name.split("::", 2).last
          break if unscoped_model_name == model_name
          model_name = unscoped_model_name
        end until model_klass

        model_klass
      end
    end

    # Performs parameters wrapping upon the request. Will be called automatically
    # by the metal call stack.
    def process_action(*args)
      if _wrapper_enabled?
        wrapped_hash = { _wrapper_key => request.request_parameters.slice(*_wrapped_keys) }
        wrapped_filtered_hash = { _wrapper_key => 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 make the wrapped hash displayed in the log file
        request.filtered_parameters.merge! wrapped_filtered_hash
      end
      super
    end

    private
      # Returns the wrapper key which will use to stored wrapped parameters.
      def _wrapper_key
        @_wrapper_key ||= if _wrapper_options[:name_or_model]
            _wrapper_options[:name_or_model].to_s.demodulize.underscore
          else
            self.class.controller_name.singularize
          end
      end

      # Returns the list of parameters which will be selected for wrapped.
      def _wrapped_keys
        @_wrapped_keys ||= if _wrapper_options[:only]
            Array(_wrapper_options[:only]).collect(&:to_s)
          elsif _wrapper_options[:except]
            request.request_parameters.keys - Array(_wrapper_options[:except]).collect(&:to_s) - EXCLUDE_PARAMETERS
          elsif _wrapper_options[:name_or_model].respond_to?(:column_names)
            _wrapper_options[:name_or_model].column_names
          else
            request.request_parameters.keys - EXCLUDE_PARAMETERS
          end
      end

      # Returns the list of enabled formats.
      def _wrapper_formats
        Array(_wrapper_options[:format])
      end

      # Checks if we should perform parameters wrapping.
      def _wrapper_enabled?
        _wrapper_formats.any?{ |format| format == request.content_mime_type.try(:ref) } && request.request_parameters[_wrapper_key].nil?
      end
  end
end