actionpack 4.2.11.3 → 5.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of actionpack might be problematic. Click here for more details.

Files changed (125) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +379 -462
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +2 -3
  5. data/lib/abstract_controller.rb +0 -2
  6. data/lib/abstract_controller/base.rb +17 -32
  7. data/lib/abstract_controller/callbacks.rb +52 -19
  8. data/lib/abstract_controller/collector.rb +4 -9
  9. data/lib/abstract_controller/helpers.rb +2 -2
  10. data/lib/abstract_controller/railties/routes_helpers.rb +2 -2
  11. data/lib/abstract_controller/rendering.rb +27 -22
  12. data/lib/abstract_controller/translation.rb +8 -7
  13. data/lib/action_controller.rb +4 -3
  14. data/lib/action_controller/api.rb +146 -0
  15. data/lib/action_controller/base.rb +6 -10
  16. data/lib/action_controller/caching.rb +1 -3
  17. data/lib/action_controller/caching/fragments.rb +48 -3
  18. data/lib/action_controller/form_builder.rb +48 -0
  19. data/lib/action_controller/log_subscriber.rb +1 -10
  20. data/lib/action_controller/metal.rb +89 -62
  21. data/lib/action_controller/metal/basic_implicit_render.rb +11 -0
  22. data/lib/action_controller/metal/conditional_get.rb +65 -24
  23. data/lib/action_controller/metal/cookies.rb +0 -2
  24. data/lib/action_controller/metal/data_streaming.rb +2 -22
  25. data/lib/action_controller/metal/etag_with_template_digest.rb +1 -1
  26. data/lib/action_controller/metal/exceptions.rb +11 -6
  27. data/lib/action_controller/metal/force_ssl.rb +6 -6
  28. data/lib/action_controller/metal/head.rb +14 -7
  29. data/lib/action_controller/metal/helpers.rb +9 -5
  30. data/lib/action_controller/metal/http_authentication.rb +37 -38
  31. data/lib/action_controller/metal/implicit_render.rb +23 -6
  32. data/lib/action_controller/metal/instrumentation.rb +0 -1
  33. data/lib/action_controller/metal/live.rb +17 -55
  34. data/lib/action_controller/metal/mime_responds.rb +17 -37
  35. data/lib/action_controller/metal/params_wrapper.rb +8 -8
  36. data/lib/action_controller/metal/redirecting.rb +32 -9
  37. data/lib/action_controller/metal/renderers.rb +10 -8
  38. data/lib/action_controller/metal/rendering.rb +38 -6
  39. data/lib/action_controller/metal/request_forgery_protection.rb +67 -35
  40. data/lib/action_controller/metal/rescue.rb +2 -4
  41. data/lib/action_controller/metal/streaming.rb +4 -4
  42. data/lib/action_controller/metal/strong_parameters.rb +231 -78
  43. data/lib/action_controller/metal/testing.rb +1 -12
  44. data/lib/action_controller/metal/url_for.rb +12 -5
  45. data/lib/action_controller/renderer.rb +111 -0
  46. data/lib/action_controller/template_assertions.rb +9 -0
  47. data/lib/action_controller/test_case.rb +267 -363
  48. data/lib/action_dispatch.rb +2 -1
  49. data/lib/action_dispatch/http/cache.rb +23 -26
  50. data/lib/action_dispatch/http/filter_parameters.rb +6 -8
  51. data/lib/action_dispatch/http/filter_redirect.rb +7 -8
  52. data/lib/action_dispatch/http/headers.rb +28 -11
  53. data/lib/action_dispatch/http/mime_negotiation.rb +40 -26
  54. data/lib/action_dispatch/http/mime_type.rb +92 -61
  55. data/lib/action_dispatch/http/mime_types.rb +1 -4
  56. data/lib/action_dispatch/http/parameter_filter.rb +18 -8
  57. data/lib/action_dispatch/http/parameters.rb +45 -41
  58. data/lib/action_dispatch/http/request.rb +146 -82
  59. data/lib/action_dispatch/http/response.rb +180 -99
  60. data/lib/action_dispatch/http/url.rb +117 -8
  61. data/lib/action_dispatch/journey/formatter.rb +34 -28
  62. data/lib/action_dispatch/journey/gtg/transition_table.rb +1 -1
  63. data/lib/action_dispatch/journey/nfa/dot.rb +0 -2
  64. data/lib/action_dispatch/journey/nfa/transition_table.rb +1 -46
  65. data/lib/action_dispatch/journey/nodes/node.rb +14 -4
  66. data/lib/action_dispatch/journey/parser_extras.rb +4 -0
  67. data/lib/action_dispatch/journey/path/pattern.rb +37 -41
  68. data/lib/action_dispatch/journey/route.rb +71 -17
  69. data/lib/action_dispatch/journey/router.rb +5 -6
  70. data/lib/action_dispatch/journey/router/utils.rb +5 -5
  71. data/lib/action_dispatch/journey/routes.rb +14 -15
  72. data/lib/action_dispatch/journey/visitors.rb +86 -43
  73. data/lib/action_dispatch/middleware/cookies.rb +184 -135
  74. data/lib/action_dispatch/middleware/debug_exceptions.rb +115 -45
  75. data/lib/action_dispatch/middleware/exception_wrapper.rb +21 -20
  76. data/lib/action_dispatch/middleware/flash.rb +61 -45
  77. data/lib/action_dispatch/middleware/load_interlock.rb +21 -0
  78. data/lib/action_dispatch/middleware/params_parser.rb +30 -46
  79. data/lib/action_dispatch/middleware/public_exceptions.rb +2 -2
  80. data/lib/action_dispatch/middleware/reloader.rb +2 -4
  81. data/lib/action_dispatch/middleware/remote_ip.rb +29 -19
  82. data/lib/action_dispatch/middleware/request_id.rb +11 -6
  83. data/lib/action_dispatch/middleware/session/abstract_store.rb +23 -11
  84. data/lib/action_dispatch/middleware/session/cache_store.rb +9 -6
  85. data/lib/action_dispatch/middleware/session/cookie_store.rb +29 -23
  86. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +4 -0
  87. data/lib/action_dispatch/middleware/show_exceptions.rb +11 -9
  88. data/lib/action_dispatch/middleware/ssl.rb +93 -36
  89. data/lib/action_dispatch/middleware/stack.rb +43 -48
  90. data/lib/action_dispatch/middleware/static.rb +52 -40
  91. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +2 -14
  92. data/lib/action_dispatch/middleware/templates/rescues/{_source.erb → _source.html.erb} +0 -0
  93. data/lib/action_dispatch/middleware/templates/rescues/_source.text.erb +8 -0
  94. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +1 -1
  95. data/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb +1 -1
  96. data/lib/action_dispatch/middleware/templates/routes/_route.html.erb +4 -4
  97. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +59 -63
  98. data/lib/action_dispatch/railtie.rb +0 -2
  99. data/lib/action_dispatch/request/session.rb +66 -34
  100. data/lib/action_dispatch/request/utils.rb +51 -19
  101. data/lib/action_dispatch/routing.rb +3 -8
  102. data/lib/action_dispatch/routing/inspector.rb +6 -30
  103. data/lib/action_dispatch/routing/mapper.rb +447 -322
  104. data/lib/action_dispatch/routing/polymorphic_routes.rb +8 -14
  105. data/lib/action_dispatch/routing/redirection.rb +3 -3
  106. data/lib/action_dispatch/routing/route_set.rb +124 -227
  107. data/lib/action_dispatch/routing/url_for.rb +27 -10
  108. data/lib/action_dispatch/testing/assertions.rb +1 -1
  109. data/lib/action_dispatch/testing/assertions/response.rb +27 -9
  110. data/lib/action_dispatch/testing/assertions/routing.rb +9 -9
  111. data/lib/action_dispatch/testing/integration.rb +237 -76
  112. data/lib/action_dispatch/testing/test_process.rb +5 -5
  113. data/lib/action_dispatch/testing/test_request.rb +12 -21
  114. data/lib/action_dispatch/testing/test_response.rb +1 -4
  115. data/lib/action_pack.rb +1 -1
  116. data/lib/action_pack/gem_version.rb +4 -4
  117. metadata +26 -25
  118. data/lib/action_controller/metal/hide_actions.rb +0 -40
  119. data/lib/action_controller/metal/rack_delegation.rb +0 -32
  120. data/lib/action_controller/middleware.rb +0 -39
  121. data/lib/action_controller/model_naming.rb +0 -12
  122. data/lib/action_dispatch/journey/router/strexp.rb +0 -27
  123. data/lib/action_dispatch/testing/assertions/dom.rb +0 -3
  124. data/lib/action_dispatch/testing/assertions/selector.rb +0 -3
  125. data/lib/action_dispatch/testing/assertions/tag.rb +0 -3
@@ -1,9 +1,29 @@
1
+ require 'active_support/core_ext/string/filters'
2
+
1
3
  module ActionController
2
4
  module Rendering
3
5
  extend ActiveSupport::Concern
4
6
 
5
7
  RENDER_FORMATS_IN_PRIORITY = [:body, :text, :plain, :html]
6
8
 
9
+ module ClassMethods
10
+ # Documentation at ActionController::Renderer#render
11
+ delegate :render, to: :renderer
12
+
13
+ # Returns a renderer instance (inherited from ActionController::Renderer)
14
+ # for the controller.
15
+ attr_reader :renderer
16
+
17
+ def setup_renderer! # :nodoc:
18
+ @renderer = Renderer.for(self)
19
+ end
20
+
21
+ def inherited(klass)
22
+ klass.setup_renderer!
23
+ super
24
+ end
25
+ end
26
+
7
27
  # Before processing, set the request formats in current controller formats.
8
28
  def process_action(*) #:nodoc:
9
29
  self.formats = request.formats.map(&:ref).compact
@@ -42,13 +62,13 @@ module ActionController
42
62
  nil
43
63
  end
44
64
 
45
- def _process_format(format, options = {})
46
- super
65
+ def _set_html_content_type
66
+ self.content_type = Mime[:html].to_s
67
+ end
47
68
 
48
- if options[:plain]
49
- self.content_type = Mime::TEXT
50
- else
51
- self.content_type ||= format.to_s
69
+ def _set_rendered_content_type(format)
70
+ unless response.content_type
71
+ self.content_type = format.to_s
52
72
  end
53
73
  end
54
74
 
@@ -63,11 +83,23 @@ module ActionController
63
83
  def _normalize_options(options) #:nodoc:
64
84
  _normalize_text(options)
65
85
 
86
+ if options[:text]
87
+ ActiveSupport::Deprecation.warn <<-WARNING.squish
88
+ `render :text` is deprecated because it does not actually render a
89
+ `text/plain` response. Switch to `render plain: 'plain text'` to
90
+ render as `text/plain`, `render html: '<strong>HTML</strong>'` to
91
+ render as `text/html`, or `render body: 'raw'` to match the deprecated
92
+ behavior and render with the default Content-Type, which is
93
+ `text/plain`.
94
+ WARNING
95
+ end
96
+
66
97
  if options[:html]
67
98
  options[:html] = ERB::Util.html_escape(options[:html])
68
99
  end
69
100
 
70
101
  if options.delete(:nothing)
102
+ ActiveSupport::Deprecation.warn("`:nothing` option is deprecated and will be removed in Rails 5.1. Use `head` method to respond with empty response body.")
71
103
  options[:body] = nil
72
104
  end
73
105
 
@@ -13,9 +13,14 @@ module ActionController #:nodoc:
13
13
  # by including a token in the rendered HTML for your application. This token is
14
14
  # stored as a random string in the session, to which an attacker does not have
15
15
  # access. When a request reaches your application, \Rails verifies the received
16
- # token with the token in the session. Only HTML and JavaScript requests are checked,
17
- # so this will not protect your XML API (presumably you'll have a different
18
- # authentication scheme there anyway).
16
+ # token with the token in the session. All requests are checked except GET requests
17
+ # as these should be idempotent. Keep in mind that all session-oriented requests
18
+ # should be CSRF protected, including JavaScript and HTML requests.
19
+ #
20
+ # Since HTML and JavaScript requests are typically made from the browser, we
21
+ # need to ensure to verify request authenticity for the web browser. We can
22
+ # use session-oriented authentication for these types of requests, by using
23
+ # the `protect_from_forgery` method in our controllers.
19
24
  #
20
25
  # GET requests are not protected since they don't have side effects like writing
21
26
  # to the database and don't leak sensitive information. JavaScript requests are
@@ -26,22 +31,21 @@ module ActionController #:nodoc:
26
31
  # Ajax) requests are allowed to make GET requests for JavaScript responses.
27
32
  #
28
33
  # It's important to remember that XML or JSON requests are also affected and if
29
- # you're building an API you'll need something like:
34
+ # you're building an API you should change forgery protection method in
35
+ # <tt>ApplicationController</tt> (by default: <tt>:exception</tt>):
30
36
  #
31
37
  # class ApplicationController < ActionController::Base
32
- # protect_from_forgery
33
- # skip_before_action :verify_authenticity_token, if: :json_request?
34
- #
35
- # protected
36
- #
37
- # def json_request?
38
- # request.format.json?
39
- # end
38
+ # protect_from_forgery unless: -> { request.format.json? }
40
39
  # end
41
40
  #
42
- # CSRF protection is turned on with the <tt>protect_from_forgery</tt> method,
43
- # which checks the token and resets the session if it doesn't match what was expected.
44
- # A call to this method is generated for new \Rails applications by default.
41
+ # CSRF protection is turned on with the <tt>protect_from_forgery</tt> method.
42
+ # By default <tt>protect_from_forgery</tt> protects your session with
43
+ # <tt>:null_session</tt> method, which provides an empty session
44
+ # during request.
45
+ #
46
+ # We may want to disable CSRF protection for APIs since they are typically
47
+ # designed to be state-less. That is, the request API client will handle
48
+ # the session for you instead of Rails.
45
49
  #
46
50
  # The token parameter is named <tt>authenticity_token</tt> by default. The name and
47
51
  # value of this token must be added to every layout that renders forms by including
@@ -73,6 +77,10 @@ module ActionController #:nodoc:
73
77
  config_accessor :log_warning_on_csrf_failure
74
78
  self.log_warning_on_csrf_failure = true
75
79
 
80
+ # Controls whether the Origin header is checked in addition to the CSRF token.
81
+ config_accessor :forgery_protection_origin_check
82
+ self.forgery_protection_origin_check = false
83
+
76
84
  helper_method :form_authenticity_token
77
85
  helper_method :protect_against_forgery?
78
86
  end
@@ -86,13 +94,21 @@ module ActionController #:nodoc:
86
94
  #
87
95
  # class FooController < ApplicationController
88
96
  # protect_from_forgery except: :index
97
+ # end
98
+ #
99
+ # You can disable forgery protection on controller by skipping the verification before_action:
89
100
  #
90
- # You can disable CSRF protection on controller by skipping the verification before_action:
91
101
  # skip_before_action :verify_authenticity_token
92
102
  #
93
103
  # Valid Options:
94
104
  #
95
- # * <tt>:only/:except</tt> - Passed to the <tt>before_action</tt> call. Set which actions are verified.
105
+ # * <tt>:only/:except</tt> - Only apply forgery protection to a subset of actions. For example <tt>only: [ :create, :create_all ]</tt>.
106
+ # * <tt>:if/:unless</tt> - Turn off the forgery protection entirely depending on the passed Proc or method reference.
107
+ # * <tt>:prepend</tt> - By default, the verification of the authentication token will be added at the position of the
108
+ # protect_from_forgery call in your application. This means any callbacks added before are run first. This is useful
109
+ # when you want your forgery protection to depend on other callbacks, like authentication methods (Oauth vs Cookie auth).
110
+ #
111
+ # If you need to add verification to the beginning of the callback chain, use <tt>prepend: true</tt>.
96
112
  # * <tt>:with</tt> - Set the method to handle unverified request.
97
113
  #
98
114
  # Valid unverified request handling methods are:
@@ -100,9 +116,11 @@ module ActionController #:nodoc:
100
116
  # * <tt>:reset_session</tt> - Resets the session.
101
117
  # * <tt>:null_session</tt> - Provides an empty session during request but doesn't reset it completely. Used as default if <tt>:with</tt> option is not specified.
102
118
  def protect_from_forgery(options = {})
119
+ options = options.reverse_merge(prepend: false)
120
+
103
121
  self.forgery_protection_strategy = protection_method_class(options[:with] || :null_session)
104
122
  self.request_forgery_protection_token ||= :authenticity_token
105
- prepend_before_action :verify_authenticity_token, options
123
+ before_action :verify_authenticity_token, options
106
124
  append_after_action :verify_same_origin_request
107
125
  end
108
126
 
@@ -124,17 +142,17 @@ module ActionController #:nodoc:
124
142
  # This is the method that defines the application behavior when a request is found to be unverified.
125
143
  def handle_unverified_request
126
144
  request = @controller.request
127
- request.session = NullSessionHash.new(request.env)
128
- request.env['action_dispatch.request.flash_hash'] = nil
129
- request.env['rack.session.options'] = { skip: true }
130
- request.env['action_dispatch.cookies'] = NullCookieJar.build(request)
145
+ request.session = NullSessionHash.new(request)
146
+ request.flash = nil
147
+ request.session_options = { skip: true }
148
+ request.cookie_jar = NullCookieJar.build(request, {})
131
149
  end
132
150
 
133
151
  protected
134
152
 
135
153
  class NullSessionHash < Rack::Session::Abstract::SessionHash #:nodoc:
136
- def initialize(env)
137
- super(nil, env)
154
+ def initialize(req)
155
+ super(nil, req)
138
156
  @data = {}
139
157
  @loaded = true
140
158
  end
@@ -148,14 +166,6 @@ module ActionController #:nodoc:
148
166
  end
149
167
 
150
168
  class NullCookieJar < ActionDispatch::Cookies::CookieJar #:nodoc:
151
- def self.build(request)
152
- key_generator = request.env[ActionDispatch::Cookies::GENERATOR_KEY]
153
- host = request.host
154
- secure = request.ssl?
155
-
156
- new(key_generator, host, secure, options_for_env({}))
157
- end
158
-
159
169
  def write(*)
160
170
  # nothing
161
171
  end
@@ -246,13 +256,24 @@ module ActionController #:nodoc:
246
256
 
247
257
  # Returns true or false if a request is verified. Checks:
248
258
  #
249
- # * is it a GET or HEAD request? Gets should be safe and idempotent
259
+ # * Is it a GET or HEAD request? Gets should be safe and idempotent
250
260
  # * Does the form_authenticity_token match the given token value from the params?
251
261
  # * Does the X-CSRF-Token header match the form_authenticity_token
252
262
  def verified_request?
253
263
  !protect_against_forgery? || request.get? || request.head? ||
254
- valid_authenticity_token?(session, form_authenticity_param) ||
255
- valid_authenticity_token?(session, request.headers['X-CSRF-Token'])
264
+ (valid_request_origin? && any_authenticity_token_valid?)
265
+ end
266
+
267
+ # Checks if any of the authenticity tokens from the request are valid.
268
+ def any_authenticity_token_valid?
269
+ request_authenticity_tokens.any? do |token|
270
+ valid_authenticity_token?(session, token)
271
+ end
272
+ end
273
+
274
+ # Possible authenticity tokens sent in the request.
275
+ def request_authenticity_tokens
276
+ [form_authenticity_param, request.x_csrf_token]
256
277
  end
257
278
 
258
279
  # Sets the token value for the current session.
@@ -330,5 +351,16 @@ module ActionController #:nodoc:
330
351
  def protect_against_forgery?
331
352
  allow_forgery_protection
332
353
  end
354
+
355
+ # Checks if the request originated from the same origin by looking at the
356
+ # Origin header.
357
+ def valid_request_origin?
358
+ if forgery_protection_origin_check
359
+ # We accept blank origin headers because some user agents don't send it.
360
+ request.origin.nil? || request.origin == request.base_url
361
+ else
362
+ true
363
+ end
364
+ end
333
365
  end
334
366
  end
@@ -7,10 +7,8 @@ module ActionController #:nodoc:
7
7
  include ActiveSupport::Rescuable
8
8
 
9
9
  def rescue_with_handler(exception)
10
- if (exception.respond_to?(:original_exception) &&
11
- (orig_exception = exception.original_exception) &&
12
- handler_for_rescue(orig_exception))
13
- exception = orig_exception
10
+ if exception.cause && handler_for_rescue(exception.cause)
11
+ exception = exception.cause
14
12
  end
15
13
  super(exception)
16
14
  end
@@ -110,9 +110,9 @@ module ActionController #:nodoc:
110
110
  # This means that, if you have <code>yield :title</code> in your layout
111
111
  # and you want to use streaming, you would have to render the whole template
112
112
  # (and eventually trigger all queries) before streaming the title and all
113
- # assets, which kills the purpose of streaming. For this reason Rails 3.1
114
- # introduces a new helper called +provide+ that does the same as +content_for+
115
- # but tells the layout to stop searching for other entries and continue rendering.
113
+ # assets, which kills the purpose of streaming. For this purpose, you can use
114
+ # a helper called +provide+ that does the same as +content_for+ but tells the
115
+ # layout to stop searching for other entries and continue rendering.
116
116
  #
117
117
  # For instance, the template above using +provide+ would be:
118
118
  #
@@ -199,7 +199,7 @@ module ActionController #:nodoc:
199
199
  def _process_options(options) #:nodoc:
200
200
  super
201
201
  if options[:stream]
202
- if env["HTTP_VERSION"] == "HTTP/1.0"
202
+ if request.version == "HTTP/1.0"
203
203
  options.delete(:stream)
204
204
  else
205
205
  headers["Cache-Control"] ||= "no-cache"
@@ -1,9 +1,10 @@
1
1
  require 'active_support/core_ext/hash/indifferent_access'
2
+ require 'active_support/core_ext/hash/transform_values'
2
3
  require 'active_support/core_ext/array/wrap'
3
4
  require 'active_support/core_ext/string/filters'
4
- require 'active_support/deprecation'
5
5
  require 'active_support/rescuable'
6
6
  require 'action_dispatch/http/upload'
7
+ require 'rack/test'
7
8
  require 'stringio'
8
9
  require 'set'
9
10
 
@@ -12,9 +13,9 @@ module ActionController
12
13
  #
13
14
  # params = ActionController::Parameters.new(a: {})
14
15
  # params.fetch(:b)
15
- # # => ActionController::ParameterMissing: param not found: b
16
+ # # => ActionController::ParameterMissing: param is missing or the value is empty: b
16
17
  # params.require(:a)
17
- # # => ActionController::ParameterMissing: param not found: a
18
+ # # => ActionController::ParameterMissing: param is missing or the value is empty: a
18
19
  class ParameterMissing < KeyError
19
20
  attr_reader :param # :nodoc:
20
21
 
@@ -98,17 +99,18 @@ module ActionController
98
99
  # environment they should only be set once at boot-time and never mutated at
99
100
  # runtime.
100
101
  #
101
- # <tt>ActionController::Parameters</tt> inherits from
102
- # <tt>ActiveSupport::HashWithIndifferentAccess</tt>, this means
103
- # that you can fetch values using either <tt>:key</tt> or <tt>"key"</tt>.
102
+ # You can fetch values of <tt>ActionController::Parameters</tt> using either
103
+ # <tt>:key</tt> or <tt>"key"</tt>.
104
104
  #
105
105
  # params = ActionController::Parameters.new(key: 'value')
106
106
  # params[:key] # => "value"
107
107
  # params["key"] # => "value"
108
- class Parameters < ActiveSupport::HashWithIndifferentAccess
108
+ class Parameters
109
109
  cattr_accessor :permit_all_parameters, instance_accessor: false
110
110
  cattr_accessor :action_on_unpermitted_parameters, instance_accessor: false
111
111
 
112
+ delegate :keys, :key?, :has_key?, :empty?, :inspect, to: :@parameters
113
+
112
114
  # By default, never raise an UnpermittedParameters exception if these
113
115
  # params are present. The default includes both 'controller' and 'action'
114
116
  # because they are added by Rails and should be of no concern. One way
@@ -120,7 +122,7 @@ module ActionController
120
122
  self.always_permitted_parameters = %w( controller action )
121
123
 
122
124
  def self.const_missing(const_name)
123
- super unless const_name == :NEVER_UNPERMITTED_PARAMS
125
+ return super unless const_name == :NEVER_UNPERMITTED_PARAMS
124
126
  ActiveSupport::Deprecation.warn(<<-MSG.squish)
125
127
  `ActionController::Parameters::NEVER_UNPERMITTED_PARAMS` has been deprecated.
126
128
  Use `ActionController::Parameters.always_permitted_parameters` instead.
@@ -145,13 +147,24 @@ module ActionController
145
147
  # params = ActionController::Parameters.new(name: 'Francesco')
146
148
  # params.permitted? # => true
147
149
  # Person.new(params) # => #<Person id: nil, name: "Francesco">
148
- def initialize(attributes = nil)
149
- super(attributes)
150
+ def initialize(parameters = {})
151
+ @parameters = parameters.with_indifferent_access
150
152
  @permitted = self.class.permit_all_parameters
151
153
  end
152
154
 
153
- # Returns a safe +Hash+ representation of this parameter with all
154
- # unpermitted keys removed.
155
+ # Returns true if another +Parameters+ object contains the same content and
156
+ # permitted flag, or other Hash-like object contains the same content. This
157
+ # override is in place so you can perform a comparison with `Hash`.
158
+ def ==(other_hash)
159
+ if other_hash.respond_to?(:permitted?)
160
+ super
161
+ else
162
+ @parameters == other_hash
163
+ end
164
+ end
165
+
166
+ # Returns a safe <tt>ActiveSupport::HashWithIndifferentAccess</tt>
167
+ # representation of this parameter with all unpermitted keys removed.
155
168
  #
156
169
  # params = ActionController::Parameters.new({
157
170
  # name: 'Senjougahara Hitagi',
@@ -163,28 +176,27 @@ module ActionController
163
176
  # safe_params.to_h # => {"name"=>"Senjougahara Hitagi"}
164
177
  def to_h
165
178
  if permitted?
166
- to_hash
179
+ convert_parameters_to_hashes(@parameters)
167
180
  else
168
181
  slice(*self.class.always_permitted_parameters).permit!.to_h
169
182
  end
170
183
  end
171
184
 
172
- # Returns an unsafe, unfiltered +Hash+ representation of this parameter.
185
+ # Returns an unsafe, unfiltered
186
+ # <tt>ActiveSupport::HashWithIndifferentAccess</tt> representation of this
187
+ # parameter.
173
188
  def to_unsafe_h
174
- to_hash
189
+ convert_parameters_to_hashes(@parameters)
175
190
  end
176
191
  alias_method :to_unsafe_hash, :to_unsafe_h
177
192
 
178
193
  # Convert all hashes in values into parameters, then yield each pair like
179
194
  # the same way as <tt>Hash#each_pair</tt>
180
195
  def each_pair(&block)
181
- super do |key, value|
182
- convert_hashes_to_parameters(key, value)
196
+ @parameters.each_pair do |key, value|
197
+ yield key, convert_hashes_to_parameters(key, value)
183
198
  end
184
-
185
- super
186
199
  end
187
-
188
200
  alias_method :each, :each_pair
189
201
 
190
202
  # Attribute that keeps track of converted arrays, if any, to avoid double
@@ -231,19 +243,58 @@ module ActionController
231
243
  self
232
244
  end
233
245
 
234
- # Ensures that a parameter is present. If it's present, returns
235
- # the parameter at the given +key+, otherwise raises an
236
- # <tt>ActionController::ParameterMissing</tt> error.
246
+ # This method accepts both a single key and an array of keys.
247
+ #
248
+ # When passed a single key, if it exists and its associated value is
249
+ # either present or the singleton +false+, returns said value:
237
250
  #
238
251
  # ActionController::Parameters.new(person: { name: 'Francesco' }).require(:person)
239
252
  # # => {"name"=>"Francesco"}
240
253
  #
254
+ # Otherwise raises <tt>ActionController::ParameterMissing</tt>:
255
+ #
256
+ # ActionController::Parameters.new.require(:person)
257
+ # # ActionController::ParameterMissing: param is missing or the value is empty: person
258
+ #
241
259
  # ActionController::Parameters.new(person: nil).require(:person)
242
- # # => ActionController::ParameterMissing: param not found: person
260
+ # # ActionController::ParameterMissing: param is missing or the value is empty: person
261
+ #
262
+ # ActionController::Parameters.new(person: "\t").require(:person)
263
+ # # ActionController::ParameterMissing: param is missing or the value is empty: person
243
264
  #
244
265
  # ActionController::Parameters.new(person: {}).require(:person)
245
- # # => ActionController::ParameterMissing: param not found: person
266
+ # # ActionController::ParameterMissing: param is missing or the value is empty: person
267
+ #
268
+ # When given an array of keys, the method tries to require each one of them
269
+ # in order. If it succeeds, an array with the respective return values is
270
+ # returned:
271
+ #
272
+ # params = ActionController::Parameters.new(user: { ... }, profile: { ... })
273
+ # user_params, profile_params = params.require(:user, :profile)
274
+ #
275
+ # Otherwise, the method reraises the first exception found:
276
+ #
277
+ # params = ActionController::Parameters.new(user: {}, profile: {})
278
+ # user_params, profile_params = params.require(:user, :profile)
279
+ # # ActionController::ParameterMissing: param is missing or the value is empty: user
280
+ #
281
+ # Technically this method can be used to fetch terminal values:
282
+ #
283
+ # # CAREFUL
284
+ # params = ActionController::Parameters.new(person: { name: 'Finn' })
285
+ # name = params.require(:person).require(:name) # CAREFUL
286
+ #
287
+ # but take into account that at some point those ones have to be permitted:
288
+ #
289
+ # def person_params
290
+ # params.require(:person).permit(:name).tap do |person_params|
291
+ # person_params.require(:name) # SAFER
292
+ # end
293
+ # end
294
+ #
295
+ # for example.
246
296
  def require(key)
297
+ return key.map { |k| require(k) } if key.is_a?(Array)
247
298
  value = self[key]
248
299
  if value.present? || value == false
249
300
  value
@@ -271,7 +322,7 @@ module ActionController
271
322
  #
272
323
  # params.permit(:name)
273
324
  #
274
- # +:name+ passes it is a key of +params+ whose associated value is of type
325
+ # +:name+ passes if it is a key of +params+ whose associated value is of type
275
326
  # +String+, +Symbol+, +NilClass+, +Numeric+, +TrueClass+, +FalseClass+,
276
327
  # +Date+, +Time+, +DateTime+, +StringIO+, +IO+,
277
328
  # +ActionDispatch::Http::UploadedFile+ or +Rack::Test::UploadedFile+.
@@ -348,7 +399,13 @@ module ActionController
348
399
  # params[:person] # => {"name"=>"Francesco"}
349
400
  # params[:none] # => nil
350
401
  def [](key)
351
- convert_hashes_to_parameters(key, super)
402
+ convert_hashes_to_parameters(key, @parameters[key])
403
+ end
404
+
405
+ # Assigns a value to a given +key+. The given key may still get filtered out
406
+ # when +permit+ is called.
407
+ def []=(key, value)
408
+ @parameters[key] = value
352
409
  end
353
410
 
354
411
  # Returns a parameter for the given +key+. If the +key+
@@ -359,13 +416,19 @@ module ActionController
359
416
  #
360
417
  # params = ActionController::Parameters.new(person: { name: 'Francesco' })
361
418
  # params.fetch(:person) # => {"name"=>"Francesco"}
362
- # params.fetch(:none) # => ActionController::ParameterMissing: param not found: none
419
+ # params.fetch(:none) # => ActionController::ParameterMissing: param is missing or the value is empty: none
363
420
  # params.fetch(:none, 'Francesco') # => "Francesco"
364
421
  # params.fetch(:none) { 'Francesco' } # => "Francesco"
365
- def fetch(key, *args)
366
- convert_hashes_to_parameters(key, super, false)
367
- rescue KeyError
368
- raise ActionController::ParameterMissing.new(key)
422
+ def fetch(key, *args, &block)
423
+ convert_value_to_parameters(
424
+ @parameters.fetch(key) {
425
+ if block_given?
426
+ yield
427
+ else
428
+ args.fetch(0) { raise ActionController::ParameterMissing.new(key) }
429
+ end
430
+ }
431
+ )
369
432
  end
370
433
 
371
434
  # Returns a new <tt>ActionController::Parameters</tt> instance that
@@ -376,7 +439,24 @@ module ActionController
376
439
  # params.slice(:a, :b) # => {"a"=>1, "b"=>2}
377
440
  # params.slice(:d) # => {}
378
441
  def slice(*keys)
379
- new_instance_with_inherited_permitted_status(super)
442
+ new_instance_with_inherited_permitted_status(@parameters.slice(*keys))
443
+ end
444
+
445
+ # Returns current <tt>ActionController::Parameters</tt> instance which
446
+ # contains only the given +keys+.
447
+ def slice!(*keys)
448
+ @parameters.slice!(*keys)
449
+ self
450
+ end
451
+
452
+ # Returns a new <tt>ActionController::Parameters</tt> instance that
453
+ # filters out the given +keys+.
454
+ #
455
+ # params = ActionController::Parameters.new(a: 1, b: 2, c: 3)
456
+ # params.except(:a, :b) # => {"c"=>3}
457
+ # params.except(:d) # => {"a"=>1,"b"=>2,"c"=>3}
458
+ def except(*keys)
459
+ new_instance_with_inherited_permitted_status(@parameters.except(*keys))
380
460
  end
381
461
 
382
462
  # Removes and returns the key/value pairs matching the given keys.
@@ -385,7 +465,7 @@ module ActionController
385
465
  # params.extract!(:a, :b) # => {"a"=>1, "b"=>2}
386
466
  # params # => {"c"=>3}
387
467
  def extract!(*keys)
388
- new_instance_with_inherited_permitted_status(super)
468
+ new_instance_with_inherited_permitted_status(@parameters.extract!(*keys))
389
469
  end
390
470
 
391
471
  # Returns a new <tt>ActionController::Parameters</tt> with the results of
@@ -394,36 +474,80 @@ module ActionController
394
474
  # params = ActionController::Parameters.new(a: 1, b: 2, c: 3)
395
475
  # params.transform_values { |x| x * 2 }
396
476
  # # => {"a"=>2, "b"=>4, "c"=>6}
397
- def transform_values
398
- if block_given?
399
- new_instance_with_inherited_permitted_status(super)
477
+ def transform_values(&block)
478
+ if block
479
+ new_instance_with_inherited_permitted_status(
480
+ @parameters.transform_values(&block)
481
+ )
400
482
  else
401
- super
483
+ @parameters.transform_values
402
484
  end
403
485
  end
404
486
 
405
- # This method is here only to make sure that the returned object has the
406
- # correct +permitted+ status. It should not matter since the parent of
407
- # this object is +HashWithIndifferentAccess+
408
- def transform_keys # :nodoc:
409
- if block_given?
410
- new_instance_with_inherited_permitted_status(super)
487
+ # Performs values transformation and returns the altered
488
+ # <tt>ActionController::Parameters</tt> instance.
489
+ def transform_values!(&block)
490
+ @parameters.transform_values!(&block)
491
+ self
492
+ end
493
+
494
+ # Returns a new <tt>ActionController::Parameters</tt> instance with the
495
+ # results of running +block+ once for every key. The values are unchanged.
496
+ def transform_keys(&block)
497
+ if block
498
+ new_instance_with_inherited_permitted_status(
499
+ @parameters.transform_keys(&block)
500
+ )
411
501
  else
412
- super
502
+ @parameters.transform_keys
413
503
  end
414
504
  end
415
505
 
506
+ # Performs keys transformation and returns the altered
507
+ # <tt>ActionController::Parameters</tt> instance.
508
+ def transform_keys!(&block)
509
+ @parameters.transform_keys!(&block)
510
+ self
511
+ end
512
+
416
513
  # Deletes and returns a key-value pair from +Parameters+ whose key is equal
417
514
  # to key. If the key is not found, returns the default value. If the
418
515
  # optional code block is given and the key is not found, pass in the key
419
516
  # and return the result of block.
420
517
  def delete(key, &block)
421
- convert_hashes_to_parameters(key, super, false)
518
+ convert_value_to_parameters(@parameters.delete(key))
519
+ end
520
+
521
+ # Returns a new instance of <tt>ActionController::Parameters</tt> with only
522
+ # items that the block evaluates to true.
523
+ def select(&block)
524
+ new_instance_with_inherited_permitted_status(@parameters.select(&block))
422
525
  end
423
526
 
424
527
  # Equivalent to Hash#keep_if, but returns nil if no changes were made.
425
528
  def select!(&block)
426
- convert_value_to_parameters(super)
529
+ @parameters.select!(&block)
530
+ self
531
+ end
532
+ alias_method :keep_if, :select!
533
+
534
+ # Returns a new instance of <tt>ActionController::Parameters</tt> with items
535
+ # that the block evaluates to true removed.
536
+ def reject(&block)
537
+ new_instance_with_inherited_permitted_status(@parameters.reject(&block))
538
+ end
539
+
540
+ # Removes items that the block evaluates to true and returns self.
541
+ def reject!(&block)
542
+ @parameters.reject!(&block)
543
+ self
544
+ end
545
+ alias_method :delete_if, :reject!
546
+
547
+ # Returns values that were assigned to the given +keys+. Note that all the
548
+ # +Hash+ objects will be converted to <tt>ActionController::Parameters</tt>.
549
+ def values_at(*keys)
550
+ convert_value_to_parameters(@parameters.values_at(*keys))
427
551
  end
428
552
 
429
553
  # Returns an exact copy of the <tt>ActionController::Parameters</tt>
@@ -440,11 +564,30 @@ module ActionController
440
564
  end
441
565
  end
442
566
 
567
+ # Returns a new <tt>ActionController::Parameters</tt> with all keys from
568
+ # +other_hash+ merges into current hash.
569
+ def merge(other_hash)
570
+ new_instance_with_inherited_permitted_status(
571
+ @parameters.merge(other_hash)
572
+ )
573
+ end
574
+
575
+ # This is required by ActiveModel attribute assignment, so that user can
576
+ # pass +Parameters+ to a mass assignment methods in a model. It should not
577
+ # matter as we are using +HashWithIndifferentAccess+ internally.
578
+ def stringify_keys # :nodoc:
579
+ dup
580
+ end
581
+
443
582
  protected
444
583
  def permitted=(new_permitted)
445
584
  @permitted = new_permitted
446
585
  end
447
586
 
587
+ def fields_for_style?
588
+ @parameters.all? { |k, v| k =~ /\A-?\d+\z/ && v.is_a?(Hash) }
589
+ end
590
+
448
591
  private
449
592
  def new_instance_with_inherited_permitted_status(hash)
450
593
  self.class.new(hash).tap do |new_instance|
@@ -452,40 +595,56 @@ module ActionController
452
595
  end
453
596
  end
454
597
 
455
- def convert_hashes_to_parameters(key, value, assign_if_converted=true)
598
+ def convert_parameters_to_hashes(value)
599
+ case value
600
+ when Array
601
+ value.map { |v| convert_parameters_to_hashes(v) }
602
+ when Hash
603
+ value.transform_values do |v|
604
+ convert_parameters_to_hashes(v)
605
+ end.with_indifferent_access
606
+ when Parameters
607
+ value.to_h
608
+ else
609
+ value
610
+ end
611
+ end
612
+
613
+ def convert_hashes_to_parameters(key, value)
456
614
  converted = convert_value_to_parameters(value)
457
- self[key] = converted if assign_if_converted && !converted.equal?(value)
615
+ @parameters[key] = converted unless converted.equal?(value)
458
616
  converted
459
617
  end
460
618
 
461
619
  def convert_value_to_parameters(value)
462
- if value.is_a?(Array) && !converted_arrays.member?(value)
620
+ case value
621
+ when Array
622
+ return value if converted_arrays.member?(value)
463
623
  converted = value.map { |_| convert_value_to_parameters(_) }
464
624
  converted_arrays << converted
465
625
  converted
466
- elsif value.is_a?(Parameters) || !value.is_a?(Hash)
467
- value
468
- else
626
+ when Hash
469
627
  self.class.new(value)
628
+ else
629
+ value
470
630
  end
471
631
  end
472
632
 
473
633
  def each_element(object)
474
- if object.is_a?(Array)
475
- object.map { |el| yield el }.compact
476
- elsif fields_for_style?(object)
477
- hash = object.class.new
478
- object.each { |k,v| hash[k] = yield v }
479
- hash
480
- else
481
- yield object
634
+ case object
635
+ when Array
636
+ object.grep(Parameters).map { |el| yield el }.compact
637
+ when Parameters
638
+ if object.fields_for_style?
639
+ hash = object.class.new
640
+ object.each { |k,v| hash[k] = yield v }
641
+ hash
642
+ else
643
+ yield object
644
+ end
482
645
  end
483
646
  end
484
647
 
485
- def fields_for_style?(object)
486
- object.is_a?(Hash) && object.all? { |k, v| k =~ /\A-?\d+\z/ && v.is_a?(Hash) }
487
- end
488
-
489
648
  def unpermitted_parameters!(params)
490
649
  unpermitted_keys = unpermitted_keys(params)
491
650
  if unpermitted_keys.any?
@@ -547,14 +706,8 @@ module ActionController
547
706
  end
548
707
 
549
708
  def array_of_permitted_scalars?(value)
550
- if value.is_a?(Array)
551
- value.all? {|element| permitted_scalar?(element)}
552
- end
553
- end
554
-
555
- def array_of_permitted_scalars_filter(params, key)
556
- if has_key?(key) && array_of_permitted_scalars?(self[key])
557
- params[key] = self[key]
709
+ if value.is_a?(Array) && value.all? {|element| permitted_scalar?(element)}
710
+ yield value
558
711
  end
559
712
  end
560
713
 
@@ -565,17 +718,17 @@ module ActionController
565
718
  # Slicing filters out non-declared keys.
566
719
  slice(*filter.keys).each do |key, value|
567
720
  next unless value
721
+ next unless has_key? key
568
722
 
569
723
  if filter[key] == EMPTY_ARRAY
570
724
  # Declaration { comment_ids: [] }.
571
- array_of_permitted_scalars_filter(params, key)
725
+ array_of_permitted_scalars?(self[key]) do |val|
726
+ params[key] = val
727
+ end
572
728
  else
573
729
  # Declaration { user: :name } or { user: [:name, :age, { address: ... }] }.
574
730
  params[key] = each_element(value) do |element|
575
- if element.is_a?(Hash)
576
- element = self.class.new(element) unless element.respond_to?(:permit)
577
- element.permit(*Array.wrap(filter[key]))
578
- end
731
+ element.permit(*Array.wrap(filter[key]))
579
732
  end
580
733
  end
581
734
  end