actionpack 7.1.3 → 7.2.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (158) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +82 -501
  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 +15 -7
  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 +198 -126
  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 +123 -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 +210 -205
  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 +86 -60
  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 +18 -9
  70. data/lib/action_dispatch/http/filter_redirect.rb +22 -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 +31 -24
  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 +20 -44
  77. data/lib/action_dispatch/http/rack_cache.rb +2 -0
  78. data/lib/action_dispatch/http/request.rb +94 -75
  79. data/lib/action_dispatch/http/response.rb +73 -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 +671 -636
  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 +39 -16
@@ -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"