actionpack 4.2.10 → 7.2.0.rc1

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 (202) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +86 -600
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +13 -14
  5. data/lib/abstract_controller/asset_paths.rb +5 -1
  6. data/lib/abstract_controller/base.rb +166 -136
  7. data/lib/abstract_controller/caching/fragments.rb +149 -0
  8. data/lib/abstract_controller/caching.rb +68 -0
  9. data/lib/abstract_controller/callbacks.rb +126 -57
  10. data/lib/abstract_controller/collector.rb +13 -15
  11. data/lib/abstract_controller/deprecator.rb +9 -0
  12. data/lib/abstract_controller/error.rb +8 -0
  13. data/lib/abstract_controller/helpers.rb +181 -132
  14. data/lib/abstract_controller/logger.rb +5 -1
  15. data/lib/abstract_controller/railties/routes_helpers.rb +10 -3
  16. data/lib/abstract_controller/rendering.rb +56 -56
  17. data/lib/abstract_controller/translation.rb +29 -15
  18. data/lib/abstract_controller/url_for.rb +15 -11
  19. data/lib/abstract_controller.rb +21 -5
  20. data/lib/action_controller/api/api_rendering.rb +18 -0
  21. data/lib/action_controller/api.rb +154 -0
  22. data/lib/action_controller/base.rb +219 -155
  23. data/lib/action_controller/caching.rb +28 -68
  24. data/lib/action_controller/deprecator.rb +9 -0
  25. data/lib/action_controller/form_builder.rb +55 -0
  26. data/lib/action_controller/log_subscriber.rb +35 -22
  27. data/lib/action_controller/metal/allow_browser.rb +119 -0
  28. data/lib/action_controller/metal/basic_implicit_render.rb +17 -0
  29. data/lib/action_controller/metal/conditional_get.rb +259 -122
  30. data/lib/action_controller/metal/content_security_policy.rb +86 -0
  31. data/lib/action_controller/metal/cookies.rb +9 -5
  32. data/lib/action_controller/metal/data_streaming.rb +87 -104
  33. data/lib/action_controller/metal/default_headers.rb +21 -0
  34. data/lib/action_controller/metal/etag_with_flash.rb +22 -0
  35. data/lib/action_controller/metal/etag_with_template_digest.rb +35 -26
  36. data/lib/action_controller/metal/exceptions.rb +71 -24
  37. data/lib/action_controller/metal/flash.rb +26 -19
  38. data/lib/action_controller/metal/head.rb +45 -36
  39. data/lib/action_controller/metal/helpers.rb +80 -64
  40. data/lib/action_controller/metal/http_authentication.rb +297 -244
  41. data/lib/action_controller/metal/implicit_render.rb +57 -9
  42. data/lib/action_controller/metal/instrumentation.rb +76 -64
  43. data/lib/action_controller/metal/live.rb +238 -176
  44. data/lib/action_controller/metal/logging.rb +22 -0
  45. data/lib/action_controller/metal/mime_responds.rb +177 -166
  46. data/lib/action_controller/metal/parameter_encoding.rb +84 -0
  47. data/lib/action_controller/metal/params_wrapper.rb +145 -118
  48. data/lib/action_controller/metal/permissions_policy.rb +38 -0
  49. data/lib/action_controller/metal/rate_limiting.rb +62 -0
  50. data/lib/action_controller/metal/redirecting.rb +203 -64
  51. data/lib/action_controller/metal/renderers.rb +108 -65
  52. data/lib/action_controller/metal/rendering.rb +216 -56
  53. data/lib/action_controller/metal/request_forgery_protection.rb +496 -163
  54. data/lib/action_controller/metal/rescue.rb +19 -21
  55. data/lib/action_controller/metal/streaming.rb +179 -138
  56. data/lib/action_controller/metal/strong_parameters.rb +1058 -382
  57. data/lib/action_controller/metal/testing.rb +11 -17
  58. data/lib/action_controller/metal/url_for.rb +37 -21
  59. data/lib/action_controller/metal.rb +236 -138
  60. data/lib/action_controller/railtie.rb +89 -11
  61. data/lib/action_controller/railties/helpers.rb +5 -1
  62. data/lib/action_controller/renderer.rb +161 -0
  63. data/lib/action_controller/template_assertions.rb +13 -0
  64. data/lib/action_controller/test_case.rb +425 -497
  65. data/lib/action_controller.rb +44 -22
  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 +119 -63
  69. data/lib/action_dispatch/http/content_disposition.rb +47 -0
  70. data/lib/action_dispatch/http/content_security_policy.rb +364 -0
  71. data/lib/action_dispatch/http/filter_parameters.rb +36 -34
  72. data/lib/action_dispatch/http/filter_redirect.rb +24 -12
  73. data/lib/action_dispatch/http/headers.rb +66 -31
  74. data/lib/action_dispatch/http/mime_negotiation.rb +106 -75
  75. data/lib/action_dispatch/http/mime_type.rb +196 -136
  76. data/lib/action_dispatch/http/mime_types.rb +25 -7
  77. data/lib/action_dispatch/http/parameters.rb +97 -45
  78. data/lib/action_dispatch/http/permissions_policy.rb +187 -0
  79. data/lib/action_dispatch/http/rack_cache.rb +6 -0
  80. data/lib/action_dispatch/http/request.rb +299 -170
  81. data/lib/action_dispatch/http/response.rb +311 -160
  82. data/lib/action_dispatch/http/upload.rb +52 -23
  83. data/lib/action_dispatch/http/url.rb +201 -125
  84. data/lib/action_dispatch/journey/formatter.rb +110 -50
  85. data/lib/action_dispatch/journey/gtg/builder.rb +37 -50
  86. data/lib/action_dispatch/journey/gtg/simulator.rb +20 -17
  87. data/lib/action_dispatch/journey/gtg/transition_table.rb +96 -36
  88. data/lib/action_dispatch/journey/nfa/dot.rb +5 -14
  89. data/lib/action_dispatch/journey/nodes/node.rb +100 -20
  90. data/lib/action_dispatch/journey/parser.rb +19 -17
  91. data/lib/action_dispatch/journey/parser.y +4 -3
  92. data/lib/action_dispatch/journey/parser_extras.rb +14 -4
  93. data/lib/action_dispatch/journey/path/pattern.rb +79 -63
  94. data/lib/action_dispatch/journey/route.rb +108 -44
  95. data/lib/action_dispatch/journey/router/utils.rb +41 -29
  96. data/lib/action_dispatch/journey/router.rb +64 -57
  97. data/lib/action_dispatch/journey/routes.rb +23 -21
  98. data/lib/action_dispatch/journey/scanner.rb +28 -17
  99. data/lib/action_dispatch/journey/visitors.rb +100 -54
  100. data/lib/action_dispatch/journey/visualizer/fsm.js +49 -24
  101. data/lib/action_dispatch/journey/visualizer/index.html.erb +1 -1
  102. data/lib/action_dispatch/journey.rb +7 -5
  103. data/lib/action_dispatch/log_subscriber.rb +25 -0
  104. data/lib/action_dispatch/middleware/actionable_exceptions.rb +46 -0
  105. data/lib/action_dispatch/middleware/assume_ssl.rb +27 -0
  106. data/lib/action_dispatch/middleware/callbacks.rb +7 -6
  107. data/lib/action_dispatch/middleware/cookies.rb +471 -328
  108. data/lib/action_dispatch/middleware/debug_exceptions.rb +149 -66
  109. data/lib/action_dispatch/middleware/debug_locks.rb +129 -0
  110. data/lib/action_dispatch/middleware/debug_view.rb +73 -0
  111. data/lib/action_dispatch/middleware/exception_wrapper.rb +275 -73
  112. data/lib/action_dispatch/middleware/executor.rb +32 -0
  113. data/lib/action_dispatch/middleware/flash.rb +143 -101
  114. data/lib/action_dispatch/middleware/host_authorization.rb +171 -0
  115. data/lib/action_dispatch/middleware/public_exceptions.rb +36 -27
  116. data/lib/action_dispatch/middleware/reloader.rb +10 -92
  117. data/lib/action_dispatch/middleware/remote_ip.rb +133 -107
  118. data/lib/action_dispatch/middleware/request_id.rb +29 -15
  119. data/lib/action_dispatch/middleware/server_timing.rb +78 -0
  120. data/lib/action_dispatch/middleware/session/abstract_store.rb +49 -27
  121. data/lib/action_dispatch/middleware/session/cache_store.rb +33 -16
  122. data/lib/action_dispatch/middleware/session/cookie_store.rb +86 -80
  123. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +15 -3
  124. data/lib/action_dispatch/middleware/show_exceptions.rb +66 -36
  125. data/lib/action_dispatch/middleware/ssl.rb +134 -36
  126. data/lib/action_dispatch/middleware/stack.rb +109 -44
  127. data/lib/action_dispatch/middleware/static.rb +159 -90
  128. data/lib/action_dispatch/middleware/templates/rescues/_actions.html.erb +13 -0
  129. data/lib/action_dispatch/middleware/templates/rescues/_actions.text.erb +0 -0
  130. data/lib/action_dispatch/middleware/templates/rescues/_message_and_suggestions.html.erb +22 -0
  131. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +7 -24
  132. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb +1 -1
  133. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +36 -0
  134. data/lib/action_dispatch/middleware/templates/rescues/_source.text.erb +8 -0
  135. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +46 -36
  136. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +12 -0
  137. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb +9 -0
  138. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +26 -7
  139. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +3 -3
  140. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +24 -0
  141. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +16 -0
  142. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +139 -15
  143. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +23 -0
  144. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb +3 -0
  145. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +6 -6
  146. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +7 -7
  147. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +9 -9
  148. data/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb +1 -1
  149. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +4 -4
  150. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb +1 -1
  151. data/lib/action_dispatch/middleware/templates/routes/_route.html.erb +7 -4
  152. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +125 -93
  153. data/lib/action_dispatch/railtie.rb +44 -16
  154. data/lib/action_dispatch/request/session.rb +159 -69
  155. data/lib/action_dispatch/request/utils.rb +97 -23
  156. data/lib/action_dispatch/routing/endpoint.rb +11 -2
  157. data/lib/action_dispatch/routing/inspector.rb +195 -106
  158. data/lib/action_dispatch/routing/mapper.rb +1338 -955
  159. data/lib/action_dispatch/routing/polymorphic_routes.rb +234 -201
  160. data/lib/action_dispatch/routing/redirection.rb +78 -51
  161. data/lib/action_dispatch/routing/route_set.rb +460 -374
  162. data/lib/action_dispatch/routing/routes_proxy.rb +36 -12
  163. data/lib/action_dispatch/routing/url_for.rb +172 -124
  164. data/lib/action_dispatch/routing.rb +159 -158
  165. data/lib/action_dispatch/system_test_case.rb +206 -0
  166. data/lib/action_dispatch/system_testing/browser.rb +84 -0
  167. data/lib/action_dispatch/system_testing/driver.rb +85 -0
  168. data/lib/action_dispatch/system_testing/server.rb +33 -0
  169. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +164 -0
  170. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +23 -0
  171. data/lib/action_dispatch/testing/assertion_response.rb +48 -0
  172. data/lib/action_dispatch/testing/assertions/response.rb +71 -39
  173. data/lib/action_dispatch/testing/assertions/routing.rb +228 -103
  174. data/lib/action_dispatch/testing/assertions.rb +9 -6
  175. data/lib/action_dispatch/testing/integration.rb +486 -306
  176. data/lib/action_dispatch/testing/request_encoder.rb +60 -0
  177. data/lib/action_dispatch/testing/test_helpers/page_dump_helper.rb +35 -0
  178. data/lib/action_dispatch/testing/test_process.rb +35 -22
  179. data/lib/action_dispatch/testing/test_request.rb +29 -34
  180. data/lib/action_dispatch/testing/test_response.rb +48 -15
  181. data/lib/action_dispatch.rb +82 -40
  182. data/lib/action_pack/gem_version.rb +8 -4
  183. data/lib/action_pack/version.rb +6 -2
  184. data/lib/action_pack.rb +21 -18
  185. metadata +146 -56
  186. data/lib/action_controller/caching/fragments.rb +0 -103
  187. data/lib/action_controller/metal/force_ssl.rb +0 -97
  188. data/lib/action_controller/metal/hide_actions.rb +0 -40
  189. data/lib/action_controller/metal/rack_delegation.rb +0 -32
  190. data/lib/action_controller/middleware.rb +0 -39
  191. data/lib/action_controller/model_naming.rb +0 -12
  192. data/lib/action_dispatch/http/parameter_filter.rb +0 -72
  193. data/lib/action_dispatch/journey/backwards.rb +0 -5
  194. data/lib/action_dispatch/journey/nfa/builder.rb +0 -76
  195. data/lib/action_dispatch/journey/nfa/simulator.rb +0 -47
  196. data/lib/action_dispatch/journey/nfa/transition_table.rb +0 -163
  197. data/lib/action_dispatch/journey/router/strexp.rb +0 -27
  198. data/lib/action_dispatch/middleware/params_parser.rb +0 -60
  199. data/lib/action_dispatch/middleware/templates/rescues/_source.erb +0 -27
  200. data/lib/action_dispatch/testing/assertions/dom.rb +0 -3
  201. data/lib/action_dispatch/testing/assertions/selector.rb +0 -3
  202. data/lib/action_dispatch/testing/assertions/tag.rb +0 -3
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ module ActionController
6
+ # # Action Controller Form Builder
7
+ #
8
+ # Override the default form builder for all views rendered by this controller
9
+ # and any of its descendants. Accepts a subclass of
10
+ # ActionView::Helpers::FormBuilder.
11
+ #
12
+ # For example, given a form builder:
13
+ #
14
+ # class AdminFormBuilder < ActionView::Helpers::FormBuilder
15
+ # def special_field(name)
16
+ # end
17
+ # end
18
+ #
19
+ # The controller specifies a form builder as its default:
20
+ #
21
+ # class AdminAreaController < ApplicationController
22
+ # default_form_builder AdminFormBuilder
23
+ # end
24
+ #
25
+ # Then in the view any form using `form_for` will be an instance of the
26
+ # specified form builder:
27
+ #
28
+ # <%= form_for(@instance) do |builder| %>
29
+ # <%= builder.special_field(:name) %>
30
+ # <% end %>
31
+ module FormBuilder
32
+ extend ActiveSupport::Concern
33
+
34
+ included do
35
+ class_attribute :_default_form_builder, instance_accessor: false
36
+ end
37
+
38
+ module ClassMethods
39
+ # Set the form builder to be used as the default for all forms in the views
40
+ # rendered by this controller and its subclasses.
41
+ #
42
+ # #### Parameters
43
+ # * `builder` - Default form builder, an instance of
44
+ # ActionView::Helpers::FormBuilder
45
+ def default_form_builder(builder)
46
+ self._default_form_builder = builder
47
+ end
48
+ end
49
+
50
+ # Default form builder for the controller
51
+ def default_form_builder
52
+ self.class._default_form_builder
53
+ end
54
+ end
55
+ end
@@ -1,3 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
1
5
  module ActionController
2
6
  class LogSubscriber < ActiveSupport::LogSubscriber
3
7
  INTERNAL_PARAMS = %w(controller action format _method only_path)
@@ -6,71 +10,80 @@ module ActionController
6
10
  return unless logger.info?
7
11
 
8
12
  payload = event.payload
9
- params = payload[:params].except(*INTERNAL_PARAMS)
13
+ params = {}
14
+ payload[:params].each_pair do |k, v|
15
+ params[k] = v unless INTERNAL_PARAMS.include?(k)
16
+ end
10
17
  format = payload[:format]
11
18
  format = format.to_s.upcase if format.is_a?(Symbol)
19
+ format = "*/*" if format.nil?
12
20
 
13
21
  info "Processing by #{payload[:controller]}##{payload[:action]} as #{format}"
14
22
  info " Parameters: #{params.inspect}" unless params.empty?
15
23
  end
24
+ subscribe_log_level :start_processing, :info
16
25
 
17
26
  def process_action(event)
18
27
  info do
19
- payload = event.payload
28
+ payload = event.payload
20
29
  additions = ActionController::Base.log_process_action(payload)
21
-
22
30
  status = payload[:status]
23
- if status.nil? && payload[:exception].present?
24
- exception_class_name = payload[:exception].first
31
+
32
+ if status.nil? && (exception_class_name = payload[:exception]&.first)
25
33
  status = ActionDispatch::ExceptionWrapper.status_code_for_exception(exception_class_name)
26
34
  end
27
- message = "Completed #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]} in #{event.duration.round}ms"
28
- message << " (#{additions.join(" | ")})" unless additions.blank?
35
+
36
+ additions << "GC: #{event.gc_time.round(1)}ms"
37
+
38
+ message = +"Completed #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]} in #{event.duration.round}ms" \
39
+ " (#{additions.join(" | ")})"
40
+ message << "\n\n" if defined?(Rails.env) && Rails.env.development?
41
+
29
42
  message
30
43
  end
31
44
  end
45
+ subscribe_log_level :process_action, :info
32
46
 
33
47
  def halted_callback(event)
34
48
  info { "Filter chain halted as #{event.payload[:filter].inspect} rendered or redirected" }
35
49
  end
50
+ subscribe_log_level :halted_callback, :info
36
51
 
37
52
  def send_file(event)
38
53
  info { "Sent file #{event.payload[:path]} (#{event.duration.round(1)}ms)" }
39
54
  end
55
+ subscribe_log_level :send_file, :info
40
56
 
41
57
  def redirect_to(event)
42
58
  info { "Redirected to #{event.payload[:location]}" }
43
59
  end
60
+ subscribe_log_level :redirect_to, :info
44
61
 
45
62
  def send_data(event)
46
63
  info { "Sent data #{event.payload[:filename]} (#{event.duration.round(1)}ms)" }
47
64
  end
65
+ subscribe_log_level :send_data, :info
48
66
 
49
67
  def unpermitted_parameters(event)
50
68
  debug do
51
69
  unpermitted_keys = event.payload[:keys]
52
- "Unpermitted parameter#{'s' if unpermitted_keys.size > 1}: #{unpermitted_keys.join(", ")}"
53
- end
54
- end
55
-
56
- def deep_munge(event)
57
- debug do
58
- "Value for params[:#{event.payload[:keys].join('][:')}] was set "\
59
- "to nil, because it was one of [], [null] or [null, null, ...]. "\
60
- "Go to http://guides.rubyonrails.org/security.html#unsafe-query-generation "\
61
- "for more information."\
70
+ display_unpermitted_keys = unpermitted_keys.map { |e| ":#{e}" }.join(", ")
71
+ context = event.payload[:context].map { |k, v| "#{k}: #{v}" }.join(", ")
72
+ color("Unpermitted parameter#{'s' if unpermitted_keys.size > 1}: #{display_unpermitted_keys}. Context: { #{context} }", RED)
62
73
  end
63
74
  end
75
+ subscribe_log_level :unpermitted_parameters, :debug
64
76
 
65
- %w(write_fragment read_fragment exist_fragment?
66
- expire_fragment expire_page write_page).each do |method|
77
+ %w(write_fragment read_fragment exist_fragment? expire_fragment).each do |method|
67
78
  class_eval <<-METHOD, __FILE__, __LINE__ + 1
79
+ # frozen_string_literal: true
68
80
  def #{method}(event)
69
- return unless logger.info?
70
- key_or_path = event.payload[:key] || event.payload[:path]
81
+ return unless ActionController::Base.enable_fragment_cache_logging
82
+ key = ActiveSupport::Cache.expand_cache_key(event.payload[:key] || event.payload[:path])
71
83
  human_name = #{method.to_s.humanize.inspect}
72
- info("\#{human_name} \#{key_or_path} (\#{event.duration.round(1)}ms)")
84
+ info("\#{human_name} \#{key} (\#{event.duration.round(1)}ms)")
73
85
  end
86
+ subscribe_log_level :#{method}, :info
74
87
  METHOD
75
88
  end
76
89
 
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ module ActionController # :nodoc:
6
+ module AllowBrowser
7
+ extend ActiveSupport::Concern
8
+
9
+ module ClassMethods
10
+ # Specify the browser versions that will be allowed to access all actions (or
11
+ # some, as limited by `only:` or `except:`). Only browsers matched in the hash
12
+ # or named set passed to `versions:` will be blocked if they're below the
13
+ # versions specified. This means that all other browsers, as well as agents that
14
+ # aren't reporting a user-agent header, will be allowed access.
15
+ #
16
+ # A browser that's blocked will by default be served the file in
17
+ # public/406-unsupported-browser.html with a HTTP status code of "406 Not
18
+ # Acceptable".
19
+ #
20
+ # In addition to specifically named browser versions, you can also pass
21
+ # `:modern` as the set to restrict support to browsers natively supporting webp
22
+ # images, web push, badges, import maps, CSS nesting, and CSS :has. This
23
+ # includes Safari 17.2+, Chrome 120+, Firefox 121+, Opera 106+.
24
+ #
25
+ # You can use https://caniuse.com to check for browser versions supporting the
26
+ # features you use.
27
+ #
28
+ # You can use `ActiveSupport::Notifications` to subscribe to events of browsers
29
+ # being blocked using the `browser_block.action_controller` event name.
30
+ #
31
+ # Examples:
32
+ #
33
+ # class ApplicationController < ActionController::Base
34
+ # # Allow only browsers natively supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has
35
+ # allow_browser versions: :modern
36
+ # end
37
+ #
38
+ # class ApplicationController < ActionController::Base
39
+ # # All versions of Chrome and Opera will be allowed, but no versions of "internet explorer" (ie). Safari needs to be 16.4+ and Firefox 121+.
40
+ # allow_browser versions: { safari: 16.4, firefox: 121, ie: false }
41
+ # end
42
+ #
43
+ # class MessagesController < ApplicationController
44
+ # # In addition to the browsers blocked by ApplicationController, also block Opera below 104 and Chrome below 119 for the show action.
45
+ # allow_browser versions: { opera: 104, chrome: 119 }, only: :show
46
+ # end
47
+ def allow_browser(versions:, block: -> { render file: Rails.root.join("public/406-unsupported-browser.html"), layout: false, status: :not_acceptable }, **options)
48
+ before_action -> { allow_browser(versions: versions, block: block) }, **options
49
+ end
50
+ end
51
+
52
+ private
53
+ def allow_browser(versions:, block:)
54
+ require "useragent"
55
+
56
+ if BrowserBlocker.new(request, versions: versions).blocked?
57
+ ActiveSupport::Notifications.instrument("browser_block.action_controller", request: request, versions: versions) do
58
+ instance_exec(&block)
59
+ end
60
+ end
61
+ end
62
+
63
+ class BrowserBlocker
64
+ SETS = {
65
+ modern: { safari: 17.2, chrome: 120, firefox: 121, opera: 106, ie: false }
66
+ }
67
+
68
+ attr_reader :request, :versions
69
+
70
+ def initialize(request, versions:)
71
+ @request, @versions = request, versions
72
+ end
73
+
74
+ def blocked?
75
+ user_agent_version_reported? && unsupported_browser?
76
+ end
77
+
78
+ private
79
+ def parsed_user_agent
80
+ @parsed_user_agent ||= UserAgent.parse(request.user_agent)
81
+ end
82
+
83
+ def user_agent_version_reported?
84
+ request.user_agent.present? && parsed_user_agent.version.to_s.present?
85
+ end
86
+
87
+ def unsupported_browser?
88
+ version_guarded_browser? && version_below_minimum_required?
89
+ end
90
+
91
+ def version_guarded_browser?
92
+ minimum_browser_version_for_browser != nil
93
+ end
94
+
95
+ def version_below_minimum_required?
96
+ if minimum_browser_version_for_browser
97
+ parsed_user_agent.version < UserAgent::Version.new(minimum_browser_version_for_browser.to_s)
98
+ else
99
+ true
100
+ end
101
+ end
102
+
103
+ def minimum_browser_version_for_browser
104
+ expanded_versions[normalized_browser_name]
105
+ end
106
+
107
+ def expanded_versions
108
+ @expanded_versions ||= (SETS[versions] || versions).with_indifferent_access
109
+ end
110
+
111
+ def normalized_browser_name
112
+ case name = parsed_user_agent.browser.downcase
113
+ when "internet explorer" then "ie"
114
+ else name
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ module ActionController
6
+ module BasicImplicitRender # :nodoc:
7
+ def send_action(method, *args)
8
+ ret = super
9
+ default_render unless performed?
10
+ ret
11
+ end
12
+
13
+ def default_render
14
+ head :no_content
15
+ end
16
+ end
17
+ end