actionpack 7.0.8.1 → 7.2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +94 -500
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +2 -2
  5. data/lib/abstract_controller/asset_paths.rb +2 -0
  6. data/lib/abstract_controller/base.rb +119 -106
  7. data/lib/abstract_controller/caching/fragments.rb +51 -52
  8. data/lib/abstract_controller/caching.rb +2 -0
  9. data/lib/abstract_controller/callbacks.rb +94 -67
  10. data/lib/abstract_controller/collector.rb +6 -6
  11. data/lib/abstract_controller/deprecator.rb +9 -0
  12. data/lib/abstract_controller/error.rb +2 -0
  13. data/lib/abstract_controller/helpers.rb +121 -91
  14. data/lib/abstract_controller/logger.rb +2 -0
  15. data/lib/abstract_controller/railties/routes_helpers.rb +3 -16
  16. data/lib/abstract_controller/rendering.rb +14 -13
  17. data/lib/abstract_controller/translation.rb +12 -30
  18. data/lib/abstract_controller/url_for.rb +9 -5
  19. data/lib/abstract_controller.rb +8 -0
  20. data/lib/action_controller/api/api_rendering.rb +2 -0
  21. data/lib/action_controller/api.rb +78 -73
  22. data/lib/action_controller/base.rb +199 -141
  23. data/lib/action_controller/caching.rb +16 -11
  24. data/lib/action_controller/deprecator.rb +9 -0
  25. data/lib/action_controller/form_builder.rb +21 -16
  26. data/lib/action_controller/log_subscriber.rb +19 -5
  27. data/lib/action_controller/metal/allow_browser.rb +123 -0
  28. data/lib/action_controller/metal/basic_implicit_render.rb +2 -0
  29. data/lib/action_controller/metal/conditional_get.rb +187 -174
  30. data/lib/action_controller/metal/content_security_policy.rb +26 -25
  31. data/lib/action_controller/metal/cookies.rb +4 -2
  32. data/lib/action_controller/metal/data_streaming.rb +65 -54
  33. data/lib/action_controller/metal/default_headers.rb +6 -2
  34. data/lib/action_controller/metal/etag_with_flash.rb +4 -0
  35. data/lib/action_controller/metal/etag_with_template_digest.rb +18 -14
  36. data/lib/action_controller/metal/exceptions.rb +19 -9
  37. data/lib/action_controller/metal/flash.rb +12 -10
  38. data/lib/action_controller/metal/head.rb +20 -16
  39. data/lib/action_controller/metal/helpers.rb +64 -67
  40. data/lib/action_controller/metal/http_authentication.rb +214 -200
  41. data/lib/action_controller/metal/implicit_render.rb +21 -17
  42. data/lib/action_controller/metal/instrumentation.rb +22 -12
  43. data/lib/action_controller/metal/live.rb +125 -92
  44. data/lib/action_controller/metal/logging.rb +6 -4
  45. data/lib/action_controller/metal/mime_responds.rb +151 -142
  46. data/lib/action_controller/metal/parameter_encoding.rb +34 -32
  47. data/lib/action_controller/metal/params_wrapper.rb +58 -58
  48. data/lib/action_controller/metal/permissions_policy.rb +14 -13
  49. data/lib/action_controller/metal/rate_limiting.rb +62 -0
  50. data/lib/action_controller/metal/redirecting.rb +110 -84
  51. data/lib/action_controller/metal/renderers.rb +50 -49
  52. data/lib/action_controller/metal/rendering.rb +103 -82
  53. data/lib/action_controller/metal/request_forgery_protection.rb +279 -161
  54. data/lib/action_controller/metal/rescue.rb +12 -8
  55. data/lib/action_controller/metal/streaming.rb +174 -132
  56. data/lib/action_controller/metal/strong_parameters.rb +598 -473
  57. data/lib/action_controller/metal/testing.rb +2 -0
  58. data/lib/action_controller/metal/url_for.rb +23 -14
  59. data/lib/action_controller/metal.rb +145 -61
  60. data/lib/action_controller/railtie.rb +25 -9
  61. data/lib/action_controller/railties/helpers.rb +2 -0
  62. data/lib/action_controller/renderer.rb +105 -66
  63. data/lib/action_controller/template_assertions.rb +4 -2
  64. data/lib/action_controller/test_case.rb +157 -128
  65. data/lib/action_controller.rb +17 -3
  66. data/lib/action_dispatch/constants.rb +34 -0
  67. data/lib/action_dispatch/deprecator.rb +9 -0
  68. data/lib/action_dispatch/http/cache.rb +28 -29
  69. data/lib/action_dispatch/http/content_disposition.rb +2 -0
  70. data/lib/action_dispatch/http/content_security_policy.rb +69 -49
  71. data/lib/action_dispatch/http/filter_parameters.rb +27 -12
  72. data/lib/action_dispatch/http/filter_redirect.rb +22 -1
  73. data/lib/action_dispatch/http/headers.rb +23 -21
  74. data/lib/action_dispatch/http/mime_negotiation.rb +37 -48
  75. data/lib/action_dispatch/http/mime_type.rb +60 -30
  76. data/lib/action_dispatch/http/mime_types.rb +5 -1
  77. data/lib/action_dispatch/http/parameters.rb +12 -10
  78. data/lib/action_dispatch/http/permissions_policy.rb +32 -34
  79. data/lib/action_dispatch/http/rack_cache.rb +4 -0
  80. data/lib/action_dispatch/http/request.rb +132 -79
  81. data/lib/action_dispatch/http/response.rb +136 -103
  82. data/lib/action_dispatch/http/upload.rb +19 -15
  83. data/lib/action_dispatch/http/url.rb +75 -73
  84. data/lib/action_dispatch/journey/formatter.rb +19 -6
  85. data/lib/action_dispatch/journey/gtg/builder.rb +4 -3
  86. data/lib/action_dispatch/journey/gtg/simulator.rb +2 -0
  87. data/lib/action_dispatch/journey/gtg/transition_table.rb +10 -8
  88. data/lib/action_dispatch/journey/nfa/dot.rb +2 -0
  89. data/lib/action_dispatch/journey/nodes/node.rb +6 -5
  90. data/lib/action_dispatch/journey/parser.rb +4 -3
  91. data/lib/action_dispatch/journey/parser_extras.rb +2 -0
  92. data/lib/action_dispatch/journey/path/pattern.rb +18 -15
  93. data/lib/action_dispatch/journey/route.rb +12 -9
  94. data/lib/action_dispatch/journey/router/utils.rb +16 -15
  95. data/lib/action_dispatch/journey/router.rb +13 -10
  96. data/lib/action_dispatch/journey/routes.rb +6 -4
  97. data/lib/action_dispatch/journey/scanner.rb +4 -2
  98. data/lib/action_dispatch/journey/visitors.rb +2 -0
  99. data/lib/action_dispatch/journey.rb +2 -0
  100. data/lib/action_dispatch/log_subscriber.rb +25 -0
  101. data/lib/action_dispatch/middleware/actionable_exceptions.rb +7 -6
  102. data/lib/action_dispatch/middleware/assume_ssl.rb +27 -0
  103. data/lib/action_dispatch/middleware/callbacks.rb +4 -0
  104. data/lib/action_dispatch/middleware/cookies.rb +192 -194
  105. data/lib/action_dispatch/middleware/debug_exceptions.rb +36 -27
  106. data/lib/action_dispatch/middleware/debug_locks.rb +18 -13
  107. data/lib/action_dispatch/middleware/debug_view.rb +9 -2
  108. data/lib/action_dispatch/middleware/exception_wrapper.rb +181 -27
  109. data/lib/action_dispatch/middleware/executor.rb +9 -1
  110. data/lib/action_dispatch/middleware/flash.rb +65 -46
  111. data/lib/action_dispatch/middleware/host_authorization.rb +22 -17
  112. data/lib/action_dispatch/middleware/public_exceptions.rb +12 -8
  113. data/lib/action_dispatch/middleware/reloader.rb +9 -5
  114. data/lib/action_dispatch/middleware/remote_ip.rb +88 -83
  115. data/lib/action_dispatch/middleware/request_id.rb +15 -8
  116. data/lib/action_dispatch/middleware/server_timing.rb +8 -6
  117. data/lib/action_dispatch/middleware/session/abstract_store.rb +7 -0
  118. data/lib/action_dispatch/middleware/session/cache_store.rb +14 -7
  119. data/lib/action_dispatch/middleware/session/cookie_store.rb +32 -25
  120. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +9 -3
  121. data/lib/action_dispatch/middleware/show_exceptions.rb +42 -28
  122. data/lib/action_dispatch/middleware/ssl.rb +60 -45
  123. data/lib/action_dispatch/middleware/stack.rb +15 -9
  124. data/lib/action_dispatch/middleware/static.rb +40 -34
  125. data/lib/action_dispatch/middleware/templates/rescues/_actions.html.erb +2 -2
  126. data/lib/action_dispatch/middleware/templates/rescues/_message_and_suggestions.html.erb +4 -4
  127. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +8 -1
  128. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +7 -7
  129. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +2 -2
  130. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +17 -0
  131. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +16 -12
  132. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +1 -1
  133. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +3 -3
  134. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +4 -4
  135. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +1 -1
  136. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb +1 -1
  137. data/lib/action_dispatch/middleware/templates/routes/_route.html.erb +3 -0
  138. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +47 -38
  139. data/lib/action_dispatch/railtie.rb +12 -4
  140. data/lib/action_dispatch/request/session.rb +39 -27
  141. data/lib/action_dispatch/request/utils.rb +10 -3
  142. data/lib/action_dispatch/routing/endpoint.rb +2 -0
  143. data/lib/action_dispatch/routing/inspector.rb +59 -9
  144. data/lib/action_dispatch/routing/mapper.rb +686 -639
  145. data/lib/action_dispatch/routing/polymorphic_routes.rb +70 -61
  146. data/lib/action_dispatch/routing/redirection.rb +52 -38
  147. data/lib/action_dispatch/routing/route_set.rb +106 -62
  148. data/lib/action_dispatch/routing/routes_proxy.rb +16 -19
  149. data/lib/action_dispatch/routing/url_for.rb +131 -122
  150. data/lib/action_dispatch/routing.rb +152 -150
  151. data/lib/action_dispatch/system_test_case.rb +91 -81
  152. data/lib/action_dispatch/system_testing/browser.rb +27 -19
  153. data/lib/action_dispatch/system_testing/driver.rb +16 -22
  154. data/lib/action_dispatch/system_testing/server.rb +2 -0
  155. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +53 -31
  156. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +2 -0
  157. data/lib/action_dispatch/testing/assertion_response.rb +9 -7
  158. data/lib/action_dispatch/testing/assertions/response.rb +36 -26
  159. data/lib/action_dispatch/testing/assertions/routing.rb +203 -95
  160. data/lib/action_dispatch/testing/assertions.rb +5 -1
  161. data/lib/action_dispatch/testing/integration.rb +240 -229
  162. data/lib/action_dispatch/testing/request_encoder.rb +6 -1
  163. data/lib/action_dispatch/testing/test_helpers/page_dump_helper.rb +35 -0
  164. data/lib/action_dispatch/testing/test_process.rb +14 -9
  165. data/lib/action_dispatch/testing/test_request.rb +4 -2
  166. data/lib/action_dispatch/testing/test_response.rb +34 -19
  167. data/lib/action_dispatch.rb +52 -21
  168. data/lib/action_pack/gem_version.rb +5 -3
  169. data/lib/action_pack/version.rb +3 -1
  170. data/lib/action_pack.rb +18 -17
  171. metadata +91 -32
@@ -1,11 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # :markup: markdown
4
+
3
5
  require "active_support/core_ext/hash/slice"
4
6
  require "active_support/core_ext/hash/except"
5
7
  require "active_support/core_ext/module/anonymous"
6
8
  require "action_dispatch/http/mime_type"
7
9
 
8
10
  module ActionController
11
+ # # Action Controller Params Wrapper
12
+ #
9
13
  # Wraps the parameters hash into a nested hash. This will allow clients to
10
14
  # submit requests without having to specify any root elements.
11
15
  #
@@ -22,8 +26,8 @@ module ActionController
22
26
  # wrap_parameters format: [:json, :xml, :url_encoded_form, :multipart_form]
23
27
  # end
24
28
  #
25
- # If you enable +ParamsWrapper+ for +:json+ format, instead of having to
26
- # send JSON parameters like this:
29
+ # If you enable `ParamsWrapper` for `:json` format, instead of having to send
30
+ # JSON parameters like this:
27
31
  #
28
32
  # {"user": {"name": "Konata"}}
29
33
  #
@@ -32,45 +36,44 @@ module ActionController
32
36
  # {"name": "Konata"}
33
37
  #
34
38
  # And it will be wrapped into a nested hash with the key name matching the
35
- # controller's name. For example, if you're posting to +UsersController+,
36
- # your new +params+ hash will look like this:
39
+ # controller's name. For example, if you're posting to `UsersController`, your
40
+ # new `params` hash will look like this:
37
41
  #
38
42
  # {"name" => "Konata", "user" => {"name" => "Konata"}}
39
43
  #
40
- # You can also specify the key in which the parameters should be wrapped to,
41
- # and also the list of attributes it should wrap by using either +:include+ or
42
- # +:exclude+ options like this:
44
+ # You can also specify the key in which the parameters should be wrapped to, and
45
+ # also the list of attributes it should wrap by using either `:include` or
46
+ # `:exclude` options like this:
43
47
  #
44
48
  # class UsersController < ApplicationController
45
49
  # wrap_parameters :person, include: [:username, :password]
46
50
  # end
47
51
  #
48
- # On Active Record models with no +:include+ or +:exclude+ option set,
49
- # it will only wrap the parameters returned by the class method
50
- # <tt>attribute_names</tt>.
52
+ # On Active Record models with no `:include` or `:exclude` option set, it will
53
+ # only wrap the parameters returned by the class method `attribute_names`.
51
54
  #
52
- # If you're going to pass the parameters to an +ActiveModel+ object (such as
53
- # <tt>User.new(params[:user])</tt>), you might consider passing the model class to
54
- # the method instead. The +ParamsWrapper+ will actually try to determine the
55
- # list of attribute names from the model and only wrap those attributes:
55
+ # If you're going to pass the parameters to an `ActiveModel` object (such as
56
+ # `User.new(params[:user])`), you might consider passing the model class to the
57
+ # method instead. The `ParamsWrapper` will actually try to determine the list of
58
+ # attribute names from the model and only wrap those attributes:
56
59
  #
57
60
  # class UsersController < ApplicationController
58
61
  # wrap_parameters Person
59
62
  # end
60
63
  #
61
- # You still could pass +:include+ and +:exclude+ to set the list of attributes
64
+ # You still could pass `:include` and `:exclude` to set the list of attributes
62
65
  # you want to wrap.
63
66
  #
64
67
  # By default, if you don't specify the key in which the parameters would be
65
- # wrapped to, +ParamsWrapper+ will actually try to determine if there's
66
- # a model related to it or not. This controller, for example:
68
+ # wrapped to, `ParamsWrapper` will actually try to determine if there's a model
69
+ # related to it or not. This controller, for example:
67
70
  #
68
71
  # class Admin::UsersController < ApplicationController
69
72
  # end
70
73
  #
71
- # will try to check if <tt>Admin::User</tt> or +User+ model exists, and use it to
72
- # determine the wrapper key respectively. If both models don't exist,
73
- # it will then fallback to use +user+ as the key.
74
+ # will try to check if `Admin::User` or `User` model exists, and use it to
75
+ # determine the wrapper key respectively. If both models don't exist, it will
76
+ # then fall back to use `user` as the key.
74
77
  #
75
78
  # To disable this functionality for a controller:
76
79
  #
@@ -82,11 +85,7 @@ module ActionController
82
85
 
83
86
  EXCLUDE_PARAMETERS = %w(authenticity_token _method utf8)
84
87
 
85
- require "mutex_m"
86
-
87
88
  class Options < Struct.new(:name, :format, :include, :exclude, :klass, :model) # :nodoc:
88
- include Mutex_m
89
-
90
89
  def self.from_hash(hash)
91
90
  name = hash[:name]
92
91
  format = Array(hash[:format])
@@ -97,6 +96,7 @@ module ActionController
97
96
 
98
97
  def initialize(name, format, include, exclude, klass, model) # :nodoc:
99
98
  super
99
+ @mutex = Mutex.new
100
100
  @include_set = include
101
101
  @name_set = name
102
102
  end
@@ -109,7 +109,7 @@ module ActionController
109
109
  return super if @include_set
110
110
 
111
111
  m = model
112
- synchronize do
112
+ @mutex.synchronize do
113
113
  return super if @include_set
114
114
 
115
115
  @include_set = true
@@ -142,7 +142,7 @@ module ActionController
142
142
  return super if @name_set
143
143
 
144
144
  m = model
145
- synchronize do
145
+ @mutex.synchronize do
146
146
  return super if @name_set
147
147
 
148
148
  @name_set = true
@@ -155,13 +155,13 @@ module ActionController
155
155
  end
156
156
 
157
157
  private
158
- # Determine the wrapper model from the controller's name. By convention,
159
- # this could be done by trying to find the defined model that has the
160
- # same singular name as the controller. For example, +UsersController+
161
- # will try to find if the +User+ model exists.
158
+ # Determine the wrapper model from the controller's name. By convention, this
159
+ # could be done by trying to find the defined model that has the same singular
160
+ # name as the controller. For example, `UsersController` will try to find if the
161
+ # `User` model exists.
162
162
  #
163
- # This method also does namespace lookup. Foo::Bar::UsersController will
164
- # try to find Foo::Bar::User, Foo::User and finally User.
163
+ # This method also does namespace lookup. Foo::Bar::UsersController will try to
164
+ # find Foo::Bar::User, Foo::User and finally User.
165
165
  def _default_wrap_model
166
166
  return nil if klass.anonymous?
167
167
  model_name = klass.name.delete_suffix("Controller").classify
@@ -190,33 +190,34 @@ module ActionController
190
190
  self._wrapper_options = Options.from_hash(options)
191
191
  end
192
192
 
193
- # Sets the name of the wrapper key, or the model which +ParamsWrapper+
194
- # would use to determine the attribute names from.
193
+ # Sets the name of the wrapper key, or the model which `ParamsWrapper` would use
194
+ # to determine the attribute names from.
195
+ #
196
+ # #### Examples
197
+ # wrap_parameters format: :xml
198
+ # # enables the parameter wrapper for XML format
195
199
  #
196
- # ==== Examples
197
- # wrap_parameters format: :xml
198
- # # enables the parameter wrapper for XML format
200
+ # wrap_parameters :person
201
+ # # wraps parameters into +params[:person]+ hash
199
202
  #
200
- # wrap_parameters :person
201
- # # wraps parameters into +params[:person]+ hash
203
+ # wrap_parameters Person
204
+ # # wraps parameters by determining the wrapper key from Person class
205
+ # # (+person+, in this case) and the list of attribute names
202
206
  #
203
- # wrap_parameters Person
204
- # # wraps parameters by determining the wrapper key from Person class
205
- # # (+person+, in this case) and the list of attribute names
207
+ # wrap_parameters include: [:username, :title]
208
+ # # wraps only +:username+ and +:title+ attributes from parameters.
206
209
  #
207
- # wrap_parameters include: [:username, :title]
208
- # # wraps only +:username+ and +:title+ attributes from parameters.
210
+ # wrap_parameters false
211
+ # # disables parameters wrapping for this controller altogether.
209
212
  #
210
- # wrap_parameters false
211
- # # disables parameters wrapping for this controller altogether.
213
+ # #### Options
214
+ # * `:format` - The list of formats in which the parameters wrapper will be
215
+ # enabled.
216
+ # * `:include` - The list of attribute names which parameters wrapper will
217
+ # wrap into a nested hash.
218
+ # * `:exclude` - The list of attribute names which parameters wrapper will
219
+ # exclude from a nested hash.
212
220
  #
213
- # ==== Options
214
- # * <tt>:format</tt> - The list of formats in which the parameters wrapper
215
- # will be enabled.
216
- # * <tt>:include</tt> - The list of attribute names which parameters wrapper
217
- # will wrap into a nested hash.
218
- # * <tt>:exclude</tt> - The list of attribute names which parameters wrapper
219
- # will exclude from a nested hash.
220
221
  def wrap_parameters(name_or_model_or_options, options = {})
221
222
  model = nil
222
223
 
@@ -238,9 +239,8 @@ module ActionController
238
239
  self._wrapper_options = opts
239
240
  end
240
241
 
241
- # Sets the default wrapper key or model which will be used to determine
242
- # wrapper key and attribute names. Called automatically when the
243
- # module is inherited.
242
+ # Sets the default wrapper key or model which will be used to determine wrapper
243
+ # key and attribute names. Called automatically when the module is inherited.
244
244
  def inherited(klass)
245
245
  if klass._wrapper_options.format.any?
246
246
  params = klass._wrapper_options.dup
@@ -252,8 +252,8 @@ module ActionController
252
252
  end
253
253
 
254
254
  private
255
- # Performs parameters wrapping upon the request. Called automatically
256
- # by the metal call stack.
255
+ # Performs parameters wrapping upon the request. Called automatically by the
256
+ # metal call stack.
257
257
  def process_action(*)
258
258
  _perform_parameter_wrapping if _wrapper_enabled?
259
259
  super
@@ -1,33 +1,34 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # :markup: markdown
4
+
3
5
  module ActionController # :nodoc:
4
6
  module PermissionsPolicy
5
7
  extend ActiveSupport::Concern
6
8
 
7
9
  module ClassMethods
8
- # Overrides parts of the globally configured +Feature-Policy+
9
- # header:
10
+ # Overrides parts of the globally configured `Feature-Policy` header:
10
11
  #
11
- # class PagesController < ApplicationController
12
- # permissions_policy do |policy|
13
- # policy.geolocation "https://example.com"
12
+ # class PagesController < ApplicationController
13
+ # permissions_policy do |policy|
14
+ # policy.geolocation "https://example.com"
15
+ # end
14
16
  # end
15
- # end
16
17
  #
17
- # Options can be passed similar to +before_action+. For example, pass
18
- # <tt>only: :index</tt> to override the header on the index action only:
18
+ # Options can be passed similar to `before_action`. For example, pass `only:
19
+ # :index` to override the header on the index action only:
19
20
  #
20
- # class PagesController < ApplicationController
21
- # permissions_policy(only: :index) do |policy|
22
- # policy.camera :self
21
+ # class PagesController < ApplicationController
22
+ # permissions_policy(only: :index) do |policy|
23
+ # policy.camera :self
24
+ # end
23
25
  # end
24
- # end
25
26
  #
26
27
  def permissions_policy(**options, &block)
27
28
  before_action(options) do
28
29
  if block_given?
29
30
  policy = request.permissions_policy.clone
30
- yield policy
31
+ instance_exec(policy, &block)
31
32
  request.permissions_policy = policy
32
33
  end
33
34
  end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ module ActionController # :nodoc:
6
+ module RateLimiting
7
+ extend ActiveSupport::Concern
8
+
9
+ module ClassMethods
10
+ # Applies a rate limit to all actions or those specified by the normal
11
+ # `before_action` filters with `only:` and `except:`.
12
+ #
13
+ # The maximum number of requests allowed is specified `to:` and constrained to
14
+ # the window of time given by `within:`.
15
+ #
16
+ # Rate limits are by default unique to the ip address making the request, but
17
+ # you can provide your own identity function by passing a callable in the `by:`
18
+ # parameter. It's evaluated within the context of the controller processing the
19
+ # request.
20
+ #
21
+ # Requests that exceed the rate limit are refused with a `429 Too Many Requests`
22
+ # response. You can specialize this by passing a callable in the `with:`
23
+ # parameter. It's evaluated within the context of the controller processing the
24
+ # request.
25
+ #
26
+ # Rate limiting relies on a backing `ActiveSupport::Cache` store and defaults to
27
+ # `config.action_controller.cache_store`, which itself defaults to the global
28
+ # `config.cache_store`. If you don't want to store rate limits in the same
29
+ # datastore as your general caches, you can pass a custom store in the `store`
30
+ # parameter.
31
+ #
32
+ # Examples:
33
+ #
34
+ # class SessionsController < ApplicationController
35
+ # rate_limit to: 10, within: 3.minutes, only: :create
36
+ # end
37
+ #
38
+ # class SignupsController < ApplicationController
39
+ # rate_limit to: 1000, within: 10.seconds,
40
+ # by: -> { request.domain }, with: -> { redirect_to busy_controller_url, alert: "Too many signups on domain!" }, only: :new
41
+ # end
42
+ #
43
+ # class APIController < ApplicationController
44
+ # RATE_LIMIT_STORE = ActiveSupport::Cache::RedisCacheStore.new(url: ENV["REDIS_URL"])
45
+ # rate_limit to: 10, within: 3.minutes, store: RATE_LIMIT_STORE
46
+ # end
47
+ def rate_limit(to:, within:, by: -> { request.remote_ip }, with: -> { head :too_many_requests }, store: cache_store, **options)
48
+ before_action -> { rate_limiting(to: to, within: within, by: by, with: with, store: store) }, **options
49
+ end
50
+ end
51
+
52
+ private
53
+ def rate_limiting(to:, within:, by:, with:, store:)
54
+ count = store.increment("rate-limit:#{controller_path}:#{instance_exec(&by)}", 1, expires_in: within)
55
+ if count && count > to
56
+ ActiveSupport::Notifications.instrument("rate_limit.action_controller", request: request) do
57
+ instance_exec(&with)
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -1,86 +1,105 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # :markup: markdown
4
+
3
5
  module ActionController
4
6
  module Redirecting
5
7
  extend ActiveSupport::Concern
6
8
 
7
- ILLEGAL_HEADER_VALUE_REGEX = /[\x00-\x08\x0A-\x1F]/.freeze
8
-
9
9
  include AbstractController::Logger
10
10
  include ActionController::UrlFor
11
11
 
12
12
  class UnsafeRedirectError < StandardError; end
13
13
 
14
+ ILLEGAL_HEADER_VALUE_REGEX = /[\x00-\x08\x0A-\x1F]/
15
+
14
16
  included do
15
17
  mattr_accessor :raise_on_open_redirects, default: false
16
18
  end
17
19
 
18
- # Redirects the browser to the target specified in +options+. This parameter can be any one of:
20
+ # Redirects the browser to the target specified in `options`. This parameter can
21
+ # be any one of:
19
22
  #
20
- # * <tt>Hash</tt> - The URL will be generated by calling url_for with the +options+.
21
- # * <tt>Record</tt> - The URL will be generated by calling url_for with the +options+, which will reference a named URL for that record.
22
- # * <tt>String</tt> starting with <tt>protocol://</tt> (like <tt>http://</tt>) or a protocol relative reference (like <tt>//</tt>) - Is passed straight through as the target for redirection.
23
- # * <tt>String</tt> not containing a protocol - The current protocol and host is prepended to the string.
24
- # * <tt>Proc</tt> - A block that will be executed in the controller's context. Should return any option accepted by +redirect_to+.
23
+ # * `Hash` - The URL will be generated by calling url_for with the `options`.
24
+ # * `Record` - The URL will be generated by calling url_for with the
25
+ # `options`, which will reference a named URL for that record.
26
+ # * `String` starting with `protocol://` (like `http://`) or a protocol
27
+ # relative reference (like `//`) - Is passed straight through as the target
28
+ # for redirection.
29
+ # * `String` not containing a protocol - The current protocol and host is
30
+ # prepended to the string.
31
+ # * `Proc` - A block that will be executed in the controller's context. Should
32
+ # return any option accepted by `redirect_to`.
25
33
  #
26
- # === Examples:
27
34
  #
28
- # redirect_to action: "show", id: 5
29
- # redirect_to @post
30
- # redirect_to "http://www.rubyonrails.org"
31
- # redirect_to "/images/screenshot.jpg"
32
- # redirect_to posts_url
33
- # redirect_to proc { edit_post_url(@post) }
35
+ # ### Examples
34
36
  #
35
- # The redirection happens as a <tt>302 Found</tt> header unless otherwise specified using the <tt>:status</tt> option:
37
+ # redirect_to action: "show", id: 5
38
+ # redirect_to @post
39
+ # redirect_to "http://www.rubyonrails.org"
40
+ # redirect_to "/images/screenshot.jpg"
41
+ # redirect_to posts_url
42
+ # redirect_to proc { edit_post_url(@post) }
36
43
  #
37
- # redirect_to post_url(@post), status: :found
38
- # redirect_to action: 'atom', status: :moved_permanently
39
- # redirect_to post_url(@post), status: 301
40
- # redirect_to action: 'atom', status: 302
44
+ # The redirection happens as a `302 Found` header unless otherwise specified
45
+ # using the `:status` option:
41
46
  #
42
- # The status code can either be a standard {HTTP Status code}[https://www.iana.org/assignments/http-status-codes] as an
43
- # integer, or a symbol representing the downcased, underscored and symbolized description.
44
- # Note that the status code must be a 3xx HTTP code, or redirection will not occur.
47
+ # redirect_to post_url(@post), status: :found
48
+ # redirect_to action: 'atom', status: :moved_permanently
49
+ # redirect_to post_url(@post), status: 301
50
+ # redirect_to action: 'atom', status: 302
51
+ #
52
+ # The status code can either be a standard [HTTP Status
53
+ # code](https://www.iana.org/assignments/http-status-codes) as an integer, or a
54
+ # symbol representing the downcased, underscored and symbolized description.
55
+ # Note that the status code must be a 3xx HTTP code, or redirection will not
56
+ # occur.
45
57
  #
46
58
  # If you are using XHR requests other than GET or POST and redirecting after the
47
59
  # request then some browsers will follow the redirect using the original request
48
60
  # method. This may lead to undesirable behavior such as a double DELETE. To work
49
- # around this you can return a <tt>303 See Other</tt> status code which will be
61
+ # around this you can return a `303 See Other` status code which will be
50
62
  # followed using a GET request.
51
63
  #
52
- # redirect_to posts_url, status: :see_other
53
- # redirect_to action: 'index', status: 303
64
+ # redirect_to posts_url, status: :see_other
65
+ # redirect_to action: 'index', status: 303
54
66
  #
55
- # It is also possible to assign a flash message as part of the redirection. There are two special accessors for the commonly used flash names
56
- # +alert+ and +notice+ as well as a general purpose +flash+ bucket.
67
+ # It is also possible to assign a flash message as part of the redirection.
68
+ # There are two special accessors for the commonly used flash names `alert` and
69
+ # `notice` as well as a general purpose `flash` bucket.
57
70
  #
58
- # redirect_to post_url(@post), alert: "Watch it, mister!"
59
- # redirect_to post_url(@post), status: :found, notice: "Pay attention to the road"
60
- # redirect_to post_url(@post), status: 301, flash: { updated_post_id: @post.id }
61
- # redirect_to({ action: 'atom' }, alert: "Something serious happened")
71
+ # redirect_to post_url(@post), alert: "Watch it, mister!"
72
+ # redirect_to post_url(@post), status: :found, notice: "Pay attention to the road"
73
+ # redirect_to post_url(@post), status: 301, flash: { updated_post_id: @post.id }
74
+ # redirect_to({ action: 'atom' }, alert: "Something serious happened")
62
75
  #
63
- # Statements after +redirect_to+ in our controller get executed, so +redirect_to+ doesn't stop the execution of the function.
64
- # To terminate the execution of the function immediately after the +redirect_to+, use return.
76
+ # Statements after `redirect_to` in our controller get executed, so
77
+ # `redirect_to` doesn't stop the execution of the function. To terminate the
78
+ # execution of the function immediately after the `redirect_to`, use return.
65
79
  #
66
- # redirect_to post_url(@post) and return
80
+ # redirect_to post_url(@post) and return
67
81
  #
68
- # === Open Redirect protection
82
+ # ### Open Redirect protection
69
83
  #
70
- # By default, Rails protects against redirecting to external hosts for your app's safety, so called open redirects.
71
- # Note: this was a new default in Rails 7.0, after upgrading opt-in by uncommenting the line with +raise_on_open_redirects+ in <tt>config/initializers/new_framework_defaults_7_0.rb</tt>
84
+ # By default, Rails protects against redirecting to external hosts for your
85
+ # app's safety, so called open redirects. Note: this was a new default in Rails
86
+ # 7.0, after upgrading opt-in by uncommenting the line with
87
+ # `raise_on_open_redirects` in
88
+ # `config/initializers/new_framework_defaults_7_0.rb`
72
89
  #
73
90
  # Here #redirect_to automatically validates the potentially-unsafe URL:
74
91
  #
75
- # redirect_to params[:redirect_url]
92
+ # redirect_to params[:redirect_url]
76
93
  #
77
94
  # Raises UnsafeRedirectError in the case of an unsafe redirect.
78
95
  #
79
- # To allow any external redirects pass <tt>allow_other_host: true</tt>, though using a user-provided param in that case is unsafe.
96
+ # To allow any external redirects pass `allow_other_host: true`, though using a
97
+ # user-provided param in that case is unsafe.
80
98
  #
81
- # redirect_to "https://rubyonrails.org", allow_other_host: true
99
+ # redirect_to "https://rubyonrails.org", allow_other_host: true
82
100
  #
83
- # See #url_from for more information on what an internal and safe URL is, or how to fall back to an alternate redirect URL in the unsafe case.
101
+ # See #url_from for more information on what an internal and safe URL is, or how
102
+ # to fall back to an alternate redirect URL in the unsafe case.
84
103
  def redirect_to(options = {}, response_options = {})
85
104
  raise ActionControllerError.new("Cannot redirect to nil!") unless options
86
105
  raise AbstractController::DoubleRenderError if response_body
@@ -93,53 +112,56 @@ module ActionController
93
112
  _ensure_url_is_http_header_safe(redirect_to_location)
94
113
 
95
114
  self.location = _enforce_open_redirect_protection(redirect_to_location, allow_other_host: allow_other_host)
96
- self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(response.location)}\">redirected</a>.</body></html>"
115
+ self.response_body = ""
97
116
  end
98
117
 
99
- # Soft deprecated alias for #redirect_back_or_to where the +fallback_location+ location is supplied as a keyword argument instead
100
- # of the first positional argument.
118
+ # Soft deprecated alias for #redirect_back_or_to where the `fallback_location`
119
+ # location is supplied as a keyword argument instead of the first positional
120
+ # argument.
101
121
  def redirect_back(fallback_location:, allow_other_host: _allow_other_host, **args)
102
122
  redirect_back_or_to fallback_location, allow_other_host: allow_other_host, **args
103
123
  end
104
124
 
105
- # Redirects the browser to the page that issued the request (the referrer)
106
- # if possible, otherwise redirects to the provided default fallback
107
- # location.
125
+ # Redirects the browser to the page that issued the request (the referrer) if
126
+ # possible, otherwise redirects to the provided default fallback location.
127
+ #
128
+ # The referrer information is pulled from the HTTP `Referer` (sic) header on the
129
+ # request. This is an optional header and its presence on the request is subject
130
+ # to browser security settings and user preferences. If the request is missing
131
+ # this header, the `fallback_location` will be used.
108
132
  #
109
- # The referrer information is pulled from the HTTP +Referer+ (sic) header on
110
- # the request. This is an optional header and its presence on the request is
111
- # subject to browser security settings and user preferences. If the request
112
- # is missing this header, the <tt>fallback_location</tt> will be used.
133
+ # redirect_back_or_to({ action: "show", id: 5 })
134
+ # redirect_back_or_to @post
135
+ # redirect_back_or_to "http://www.rubyonrails.org"
136
+ # redirect_back_or_to "/images/screenshot.jpg"
137
+ # redirect_back_or_to posts_url
138
+ # redirect_back_or_to proc { edit_post_url(@post) }
139
+ # redirect_back_or_to '/', allow_other_host: false
113
140
  #
114
- # redirect_back_or_to({ action: "show", id: 5 })
115
- # redirect_back_or_to @post
116
- # redirect_back_or_to "http://www.rubyonrails.org"
117
- # redirect_back_or_to "/images/screenshot.jpg"
118
- # redirect_back_or_to posts_url
119
- # redirect_back_or_to proc { edit_post_url(@post) }
120
- # redirect_back_or_to '/', allow_other_host: false
141
+ # #### Options
142
+ # * `:allow_other_host` - Allow or disallow redirection to the host that is
143
+ # different to the current host, defaults to true.
121
144
  #
122
- # ==== Options
123
- # * <tt>:allow_other_host</tt> - Allow or disallow redirection to the host that is different to the current host, defaults to true.
124
145
  #
125
- # All other options that can be passed to #redirect_to are accepted as
126
- # options, and the behavior is identical.
146
+ # All other options that can be passed to #redirect_to are accepted as options,
147
+ # and the behavior is identical.
127
148
  def redirect_back_or_to(fallback_location, allow_other_host: _allow_other_host, **options)
128
149
  if request.referer && (allow_other_host || _url_host_allowed?(request.referer))
129
150
  redirect_to request.referer, allow_other_host: allow_other_host, **options
130
151
  else
131
- # The method level `allow_other_host` doesn't apply in the fallback case, omit and let the `redirect_to` handling take over.
152
+ # The method level `allow_other_host` doesn't apply in the fallback case, omit
153
+ # and let the `redirect_to` handling take over.
132
154
  redirect_to fallback_location, **options
133
155
  end
134
156
  end
135
157
 
136
158
  def _compute_redirect_to_location(request, options) # :nodoc:
137
159
  case options
138
- # The scheme name consist of a letter followed by any combination of
139
- # letters, digits, and the plus ("+"), period ("."), or hyphen ("-")
140
- # characters; and is terminated by a colon (":").
141
- # See https://tools.ietf.org/html/rfc3986#section-3.1
142
- # The protocol relative scheme starts with a double slash "//".
160
+ # The scheme name consist of a letter followed by any combination of letters,
161
+ # digits, and the plus ("+"), period ("."), or hyphen ("-") characters; and is
162
+ # terminated by a colon (":"). See
163
+ # https://tools.ietf.org/html/rfc3986#section-3.1 The protocol relative scheme
164
+ # starts with a double slash "//".
143
165
  when /\A([a-z][a-z\d\-+.]*:|\/\/).*/i
144
166
  options.to_str
145
167
  when String
@@ -153,25 +175,30 @@ module ActionController
153
175
  module_function :_compute_redirect_to_location
154
176
  public :_compute_redirect_to_location
155
177
 
156
- # Verifies the passed +location+ is an internal URL that's safe to redirect to and returns it, or nil if not.
157
- # Useful to wrap a params provided redirect URL and fallback to an alternate URL to redirect to:
178
+ # Verifies the passed `location` is an internal URL that's safe to redirect to
179
+ # and returns it, or nil if not. Useful to wrap a params provided redirect URL
180
+ # and fall back to an alternate URL to redirect to:
158
181
  #
159
- # redirect_to url_from(params[:redirect_url]) || root_url
182
+ # redirect_to url_from(params[:redirect_url]) || root_url
160
183
  #
161
- # The +location+ is considered internal, and safe, if it's on the same host as <tt>request.host</tt>:
184
+ # The `location` is considered internal, and safe, if it's on the same host as
185
+ # `request.host`:
162
186
  #
163
- # # If request.host is example.com:
164
- # url_from("https://example.com/profile") # => "https://example.com/profile"
165
- # url_from("http://example.com/profile") # => "http://example.com/profile"
166
- # url_from("http://evil.com/profile") # => nil
187
+ # # If request.host is example.com:
188
+ # url_from("https://example.com/profile") # => "https://example.com/profile"
189
+ # url_from("http://example.com/profile") # => "http://example.com/profile"
190
+ # url_from("http://evil.com/profile") # => nil
167
191
  #
168
192
  # Subdomains are considered part of the host:
169
193
  #
170
- # # If request.host is on https://example.com or https://app.example.com, you'd get:
171
- # url_from("https://dev.example.com/profile") # => nil
194
+ # # If request.host is on https://example.com or https://app.example.com, you'd get:
195
+ # url_from("https://dev.example.com/profile") # => nil
172
196
  #
173
- # NOTE: there's a similarity with {url_for}[rdoc-ref:ActionDispatch::Routing::UrlFor#url_for], which generates an internal URL from various options from within the app, e.g. <tt>url_for(@post)</tt>.
174
- # However, #url_from is meant to take an external parameter to verify as in <tt>url_from(params[:redirect_url])</tt>.
197
+ # NOTE: there's a similarity with
198
+ # [url_for](rdoc-ref:ActionDispatch::Routing::UrlFor#url_for), which generates
199
+ # an internal URL from various options from within the app, e.g.
200
+ # `url_for(@post)`. However, #url_from is meant to take an external parameter to
201
+ # verify as in `url_from(params[:redirect_url])`.
175
202
  def url_from(location)
176
203
  location = location.presence
177
204
  location if location && _url_host_allowed?(location)
@@ -212,9 +239,8 @@ module ActionController
212
239
  end
213
240
 
214
241
  def _ensure_url_is_http_header_safe(url)
215
- # Attempt to comply with the set of valid token characters
216
- # defined for an HTTP header value in
217
- # https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6
242
+ # Attempt to comply with the set of valid token characters defined for an HTTP
243
+ # header value in https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6
218
244
  if url.match?(ILLEGAL_HEADER_VALUE_REGEX)
219
245
  msg = "The redirect URL #{url} contains one or more illegal HTTP header field character. " \
220
246
  "Set of legal characters defined in https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6"