actionpack 7.1.3.4 → 7.2.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 (158) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +70 -541
  3. data/lib/abstract_controller/asset_paths.rb +2 -0
  4. data/lib/abstract_controller/base.rb +102 -98
  5. data/lib/abstract_controller/caching/fragments.rb +50 -53
  6. data/lib/abstract_controller/caching.rb +2 -0
  7. data/lib/abstract_controller/callbacks.rb +66 -64
  8. data/lib/abstract_controller/collector.rb +6 -6
  9. data/lib/abstract_controller/deprecator.rb +2 -0
  10. data/lib/abstract_controller/error.rb +2 -0
  11. data/lib/abstract_controller/helpers.rb +70 -85
  12. data/lib/abstract_controller/logger.rb +2 -0
  13. data/lib/abstract_controller/railties/routes_helpers.rb +2 -0
  14. data/lib/abstract_controller/rendering.rb +13 -12
  15. data/lib/abstract_controller/translation.rb +11 -10
  16. data/lib/abstract_controller/url_for.rb +8 -6
  17. data/lib/abstract_controller.rb +2 -0
  18. data/lib/action_controller/api/api_rendering.rb +2 -0
  19. data/lib/action_controller/api.rb +74 -72
  20. data/lib/action_controller/base.rb +155 -117
  21. data/lib/action_controller/caching.rb +15 -12
  22. data/lib/action_controller/deprecator.rb +2 -0
  23. data/lib/action_controller/form_builder.rb +20 -17
  24. data/lib/action_controller/log_subscriber.rb +3 -1
  25. data/lib/action_controller/metal/allow_browser.rb +119 -0
  26. data/lib/action_controller/metal/basic_implicit_render.rb +2 -0
  27. data/lib/action_controller/metal/conditional_get.rb +188 -174
  28. data/lib/action_controller/metal/content_security_policy.rb +25 -24
  29. data/lib/action_controller/metal/cookies.rb +4 -2
  30. data/lib/action_controller/metal/data_streaming.rb +64 -55
  31. data/lib/action_controller/metal/default_headers.rb +5 -3
  32. data/lib/action_controller/metal/etag_with_flash.rb +3 -1
  33. data/lib/action_controller/metal/etag_with_template_digest.rb +17 -15
  34. data/lib/action_controller/metal/exceptions.rb +11 -9
  35. data/lib/action_controller/metal/flash.rb +12 -10
  36. data/lib/action_controller/metal/head.rb +12 -10
  37. data/lib/action_controller/metal/helpers.rb +63 -55
  38. data/lib/action_controller/metal/http_authentication.rb +209 -201
  39. data/lib/action_controller/metal/implicit_render.rb +17 -15
  40. data/lib/action_controller/metal/instrumentation.rb +15 -12
  41. data/lib/action_controller/metal/live.rb +113 -107
  42. data/lib/action_controller/metal/logging.rb +6 -4
  43. data/lib/action_controller/metal/mime_responds.rb +151 -142
  44. data/lib/action_controller/metal/parameter_encoding.rb +34 -32
  45. data/lib/action_controller/metal/params_wrapper.rb +57 -59
  46. data/lib/action_controller/metal/permissions_policy.rb +13 -12
  47. data/lib/action_controller/metal/rate_limiting.rb +62 -0
  48. data/lib/action_controller/metal/redirecting.rb +108 -82
  49. data/lib/action_controller/metal/renderers.rb +50 -49
  50. data/lib/action_controller/metal/rendering.rb +103 -75
  51. data/lib/action_controller/metal/request_forgery_protection.rb +162 -133
  52. data/lib/action_controller/metal/rescue.rb +11 -9
  53. data/lib/action_controller/metal/streaming.rb +138 -136
  54. data/lib/action_controller/metal/strong_parameters.rb +525 -480
  55. data/lib/action_controller/metal/testing.rb +2 -0
  56. data/lib/action_controller/metal/url_for.rb +17 -15
  57. data/lib/action_controller/metal.rb +58 -57
  58. data/lib/action_controller/railtie.rb +3 -0
  59. data/lib/action_controller/railties/helpers.rb +2 -0
  60. data/lib/action_controller/renderer.rb +42 -36
  61. data/lib/action_controller/template_assertions.rb +4 -2
  62. data/lib/action_controller/test_case.rb +146 -126
  63. data/lib/action_controller.rb +10 -3
  64. data/lib/action_dispatch/constants.rb +2 -0
  65. data/lib/action_dispatch/deprecator.rb +2 -0
  66. data/lib/action_dispatch/http/cache.rb +27 -26
  67. data/lib/action_dispatch/http/content_disposition.rb +2 -0
  68. data/lib/action_dispatch/http/content_security_policy.rb +44 -38
  69. data/lib/action_dispatch/http/filter_parameters.rb +9 -5
  70. data/lib/action_dispatch/http/filter_redirect.rb +15 -1
  71. data/lib/action_dispatch/http/headers.rb +22 -22
  72. data/lib/action_dispatch/http/mime_negotiation.rb +30 -41
  73. data/lib/action_dispatch/http/mime_type.rb +29 -22
  74. data/lib/action_dispatch/http/mime_types.rb +2 -0
  75. data/lib/action_dispatch/http/parameters.rb +11 -9
  76. data/lib/action_dispatch/http/permissions_policy.rb +27 -37
  77. data/lib/action_dispatch/http/rack_cache.rb +2 -0
  78. data/lib/action_dispatch/http/request.rb +71 -71
  79. data/lib/action_dispatch/http/response.rb +61 -61
  80. data/lib/action_dispatch/http/upload.rb +18 -16
  81. data/lib/action_dispatch/http/url.rb +75 -73
  82. data/lib/action_dispatch/journey/formatter.rb +13 -6
  83. data/lib/action_dispatch/journey/gtg/builder.rb +4 -3
  84. data/lib/action_dispatch/journey/gtg/simulator.rb +2 -0
  85. data/lib/action_dispatch/journey/gtg/transition_table.rb +10 -8
  86. data/lib/action_dispatch/journey/nfa/dot.rb +2 -0
  87. data/lib/action_dispatch/journey/nodes/node.rb +6 -5
  88. data/lib/action_dispatch/journey/parser.rb +4 -3
  89. data/lib/action_dispatch/journey/parser_extras.rb +2 -0
  90. data/lib/action_dispatch/journey/path/pattern.rb +4 -1
  91. data/lib/action_dispatch/journey/route.rb +9 -7
  92. data/lib/action_dispatch/journey/router/utils.rb +16 -15
  93. data/lib/action_dispatch/journey/router.rb +4 -2
  94. data/lib/action_dispatch/journey/routes.rb +4 -2
  95. data/lib/action_dispatch/journey/scanner.rb +4 -2
  96. data/lib/action_dispatch/journey/visitors.rb +2 -0
  97. data/lib/action_dispatch/journey.rb +2 -0
  98. data/lib/action_dispatch/log_subscriber.rb +2 -0
  99. data/lib/action_dispatch/middleware/actionable_exceptions.rb +2 -0
  100. data/lib/action_dispatch/middleware/assume_ssl.rb +8 -5
  101. data/lib/action_dispatch/middleware/callbacks.rb +3 -1
  102. data/lib/action_dispatch/middleware/cookies.rb +119 -104
  103. data/lib/action_dispatch/middleware/debug_exceptions.rb +13 -5
  104. data/lib/action_dispatch/middleware/debug_locks.rb +15 -13
  105. data/lib/action_dispatch/middleware/debug_view.rb +2 -0
  106. data/lib/action_dispatch/middleware/exception_wrapper.rb +6 -11
  107. data/lib/action_dispatch/middleware/executor.rb +8 -0
  108. data/lib/action_dispatch/middleware/flash.rb +63 -51
  109. data/lib/action_dispatch/middleware/host_authorization.rb +17 -15
  110. data/lib/action_dispatch/middleware/public_exceptions.rb +8 -6
  111. data/lib/action_dispatch/middleware/reloader.rb +5 -3
  112. data/lib/action_dispatch/middleware/remote_ip.rb +77 -72
  113. data/lib/action_dispatch/middleware/request_id.rb +14 -9
  114. data/lib/action_dispatch/middleware/server_timing.rb +4 -2
  115. data/lib/action_dispatch/middleware/session/abstract_store.rb +2 -0
  116. data/lib/action_dispatch/middleware/session/cache_store.rb +13 -8
  117. data/lib/action_dispatch/middleware/session/cookie_store.rb +27 -26
  118. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +7 -3
  119. data/lib/action_dispatch/middleware/show_exceptions.rb +31 -21
  120. data/lib/action_dispatch/middleware/ssl.rb +43 -40
  121. data/lib/action_dispatch/middleware/stack.rb +11 -10
  122. data/lib/action_dispatch/middleware/static.rb +33 -31
  123. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +1 -1
  124. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +1 -1
  125. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +1 -1
  126. data/lib/action_dispatch/railtie.rb +2 -4
  127. data/lib/action_dispatch/request/session.rb +23 -21
  128. data/lib/action_dispatch/request/utils.rb +2 -0
  129. data/lib/action_dispatch/routing/endpoint.rb +2 -0
  130. data/lib/action_dispatch/routing/inspector.rb +5 -3
  131. data/lib/action_dispatch/routing/mapper.rb +670 -635
  132. data/lib/action_dispatch/routing/polymorphic_routes.rb +69 -62
  133. data/lib/action_dispatch/routing/redirection.rb +37 -32
  134. data/lib/action_dispatch/routing/route_set.rb +59 -45
  135. data/lib/action_dispatch/routing/routes_proxy.rb +6 -4
  136. data/lib/action_dispatch/routing/url_for.rb +130 -125
  137. data/lib/action_dispatch/routing.rb +150 -148
  138. data/lib/action_dispatch/system_test_case.rb +91 -81
  139. data/lib/action_dispatch/system_testing/browser.rb +10 -3
  140. data/lib/action_dispatch/system_testing/driver.rb +3 -1
  141. data/lib/action_dispatch/system_testing/server.rb +2 -0
  142. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +32 -21
  143. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +2 -0
  144. data/lib/action_dispatch/testing/assertion_response.rb +8 -6
  145. data/lib/action_dispatch/testing/assertions/response.rb +26 -23
  146. data/lib/action_dispatch/testing/assertions/routing.rb +153 -84
  147. data/lib/action_dispatch/testing/assertions.rb +2 -0
  148. data/lib/action_dispatch/testing/integration.rb +223 -222
  149. data/lib/action_dispatch/testing/request_encoder.rb +2 -0
  150. data/lib/action_dispatch/testing/test_helpers/page_dump_helper.rb +35 -0
  151. data/lib/action_dispatch/testing/test_process.rb +12 -8
  152. data/lib/action_dispatch/testing/test_request.rb +3 -1
  153. data/lib/action_dispatch/testing/test_response.rb +27 -26
  154. data/lib/action_dispatch.rb +22 -28
  155. data/lib/action_pack/gem_version.rb +6 -4
  156. data/lib/action_pack/version.rb +3 -1
  157. data/lib/action_pack.rb +17 -16
  158. metadata +30 -13
@@ -1,12 +1,14 @@
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
9
- # = Action Controller Params Wrapper
11
+ # # Action Controller Params Wrapper
10
12
  #
11
13
  # Wraps the parameters hash into a nested hash. This will allow clients to
12
14
  # submit requests without having to specify any root elements.
@@ -24,8 +26,8 @@ module ActionController
24
26
  # wrap_parameters format: [:json, :xml, :url_encoded_form, :multipart_form]
25
27
  # end
26
28
  #
27
- # If you enable +ParamsWrapper+ for +:json+ format, instead of having to
28
- # send JSON parameters like this:
29
+ # If you enable `ParamsWrapper` for `:json` format, instead of having to send
30
+ # JSON parameters like this:
29
31
  #
30
32
  # {"user": {"name": "Konata"}}
31
33
  #
@@ -34,45 +36,44 @@ module ActionController
34
36
  # {"name": "Konata"}
35
37
  #
36
38
  # And it will be wrapped into a nested hash with the key name matching the
37
- # controller's name. For example, if you're posting to +UsersController+,
38
- # 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:
39
41
  #
40
42
  # {"name" => "Konata", "user" => {"name" => "Konata"}}
41
43
  #
42
- # You can also specify the key in which the parameters should be wrapped to,
43
- # and also the list of attributes it should wrap by using either +:include+ or
44
- # +: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:
45
47
  #
46
48
  # class UsersController < ApplicationController
47
49
  # wrap_parameters :person, include: [:username, :password]
48
50
  # end
49
51
  #
50
- # On Active Record models with no +:include+ or +:exclude+ option set,
51
- # it will only wrap the parameters returned by the class method
52
- # <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`.
53
54
  #
54
- # If you're going to pass the parameters to an +ActiveModel+ object (such as
55
- # <tt>User.new(params[:user])</tt>), you might consider passing the model class to
56
- # the method instead. The +ParamsWrapper+ will actually try to determine the
57
- # 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:
58
59
  #
59
60
  # class UsersController < ApplicationController
60
61
  # wrap_parameters Person
61
62
  # end
62
63
  #
63
- # 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
64
65
  # you want to wrap.
65
66
  #
66
67
  # By default, if you don't specify the key in which the parameters would be
67
- # wrapped to, +ParamsWrapper+ will actually try to determine if there's
68
- # 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:
69
70
  #
70
71
  # class Admin::UsersController < ApplicationController
71
72
  # end
72
73
  #
73
- # will try to check if +Admin::User+ or +User+ model exists, and use it to
74
- # determine the wrapper key respectively. If both models don't exist,
75
- # it will then fall back 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.
76
77
  #
77
78
  # To disable this functionality for a controller:
78
79
  #
@@ -84,11 +85,7 @@ module ActionController
84
85
 
85
86
  EXCLUDE_PARAMETERS = %w(authenticity_token _method utf8)
86
87
 
87
- require "mutex_m"
88
-
89
88
  class Options < Struct.new(:name, :format, :include, :exclude, :klass, :model) # :nodoc:
90
- include Mutex_m
91
-
92
89
  def self.from_hash(hash)
93
90
  name = hash[:name]
94
91
  format = Array(hash[:format])
@@ -99,6 +96,7 @@ module ActionController
99
96
 
100
97
  def initialize(name, format, include, exclude, klass, model) # :nodoc:
101
98
  super
99
+ @mutex = Mutex.new
102
100
  @include_set = include
103
101
  @name_set = name
104
102
  end
@@ -111,7 +109,7 @@ module ActionController
111
109
  return super if @include_set
112
110
 
113
111
  m = model
114
- synchronize do
112
+ @mutex.synchronize do
115
113
  return super if @include_set
116
114
 
117
115
  @include_set = true
@@ -144,7 +142,7 @@ module ActionController
144
142
  return super if @name_set
145
143
 
146
144
  m = model
147
- synchronize do
145
+ @mutex.synchronize do
148
146
  return super if @name_set
149
147
 
150
148
  @name_set = true
@@ -157,13 +155,13 @@ module ActionController
157
155
  end
158
156
 
159
157
  private
160
- # Determine the wrapper model from the controller's name. By convention,
161
- # this could be done by trying to find the defined model that has the
162
- # same singular name as the controller. For example, +UsersController+
163
- # 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.
164
162
  #
165
- # This method also does namespace lookup. Foo::Bar::UsersController will
166
- # 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.
167
165
  def _default_wrap_model
168
166
  return nil if klass.anonymous?
169
167
  model_name = klass.name.delete_suffix("Controller").classify
@@ -192,33 +190,34 @@ module ActionController
192
190
  self._wrapper_options = Options.from_hash(options)
193
191
  end
194
192
 
195
- # Sets the name of the wrapper key, or the model which +ParamsWrapper+
196
- # 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
197
199
  #
198
- # ==== Examples
199
- # wrap_parameters format: :xml
200
- # # enables the parameter wrapper for XML format
200
+ # wrap_parameters :person
201
+ # # wraps parameters into +params[:person]+ hash
201
202
  #
202
- # wrap_parameters :person
203
- # # 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
204
206
  #
205
- # wrap_parameters Person
206
- # # wraps parameters by determining the wrapper key from Person class
207
- # # (+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.
208
209
  #
209
- # wrap_parameters include: [:username, :title]
210
- # # wraps only +:username+ and +:title+ attributes from parameters.
210
+ # wrap_parameters false
211
+ # # disables parameters wrapping for this controller altogether.
211
212
  #
212
- # wrap_parameters false
213
- # # 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.
214
220
  #
215
- # ==== Options
216
- # * <tt>:format</tt> - The list of formats in which the parameters wrapper
217
- # will be enabled.
218
- # * <tt>:include</tt> - The list of attribute names which parameters wrapper
219
- # will wrap into a nested hash.
220
- # * <tt>:exclude</tt> - The list of attribute names which parameters wrapper
221
- # will exclude from a nested hash.
222
221
  def wrap_parameters(name_or_model_or_options, options = {})
223
222
  model = nil
224
223
 
@@ -240,9 +239,8 @@ module ActionController
240
239
  self._wrapper_options = opts
241
240
  end
242
241
 
243
- # Sets the default wrapper key or model which will be used to determine
244
- # wrapper key and attribute names. Called automatically when the
245
- # 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.
246
244
  def inherited(klass)
247
245
  if klass._wrapper_options.format.any?
248
246
  params = klass._wrapper_options.dup
@@ -254,8 +252,8 @@ module ActionController
254
252
  end
255
253
 
256
254
  private
257
- # Performs parameters wrapping upon the request. Called automatically
258
- # by the metal call stack.
255
+ # Performs parameters wrapping upon the request. Called automatically by the
256
+ # metal call stack.
259
257
  def process_action(*)
260
258
  _perform_parameter_wrapping if _wrapper_enabled?
261
259
  super
@@ -1,27 +1,28 @@
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
@@ -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,5 +1,7 @@
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
@@ -9,78 +11,95 @@ module ActionController
9
11
 
10
12
  class UnsafeRedirectError < StandardError; end
11
13
 
12
- ILLEGAL_HEADER_VALUE_REGEX = /[\x00-\x08\x0A-\x1F]/.freeze
14
+ ILLEGAL_HEADER_VALUE_REGEX = /[\x00-\x08\x0A-\x1F]/
13
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:
22
+ #
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`.
19
33
  #
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+.
25
34
  #
26
- # === Examples
35
+ # ### Examples
27
36
  #
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) }
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) }
34
43
  #
35
- # The redirection happens as a <tt>302 Found</tt> header unless otherwise specified using the <tt>:status</tt> option:
44
+ # The redirection happens as a `302 Found` header unless otherwise specified
45
+ # using the `:status` option:
36
46
  #
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
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
41
51
  #
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.
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
@@ -96,50 +115,53 @@ module ActionController
96
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 fall back 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"