actionpack 7.2.3 → 8.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +394 -119
  3. data/lib/abstract_controller/asset_paths.rb +4 -2
  4. data/lib/abstract_controller/base.rb +11 -5
  5. data/lib/abstract_controller/caching.rb +6 -3
  6. data/lib/abstract_controller/callbacks.rb +6 -0
  7. data/lib/abstract_controller/logger.rb +2 -1
  8. data/lib/abstract_controller/rendering.rb +0 -1
  9. data/lib/action_controller/api.rb +1 -0
  10. data/lib/action_controller/base.rb +3 -2
  11. data/lib/action_controller/caching.rb +1 -2
  12. data/lib/action_controller/form_builder.rb +4 -4
  13. data/lib/action_controller/log_subscriber.rb +22 -3
  14. data/lib/action_controller/metal/allow_browser.rb +12 -2
  15. data/lib/action_controller/metal/conditional_get.rb +30 -1
  16. data/lib/action_controller/metal/data_streaming.rb +5 -5
  17. data/lib/action_controller/metal/exceptions.rb +5 -0
  18. data/lib/action_controller/metal/flash.rb +1 -4
  19. data/lib/action_controller/metal/head.rb +3 -1
  20. data/lib/action_controller/metal/instrumentation.rb +1 -2
  21. data/lib/action_controller/metal/live.rb +65 -25
  22. data/lib/action_controller/metal/permissions_policy.rb +9 -0
  23. data/lib/action_controller/metal/rate_limiting.rb +39 -9
  24. data/lib/action_controller/metal/redirecting.rb +105 -13
  25. data/lib/action_controller/metal/renderers.rb +29 -9
  26. data/lib/action_controller/metal/rendering.rb +7 -1
  27. data/lib/action_controller/metal/request_forgery_protection.rb +18 -10
  28. data/lib/action_controller/metal/rescue.rb +9 -0
  29. data/lib/action_controller/metal/streaming.rb +5 -84
  30. data/lib/action_controller/metal/strong_parameters.rb +277 -89
  31. data/lib/action_controller/railtie.rb +33 -15
  32. data/lib/action_controller/structured_event_subscriber.rb +116 -0
  33. data/lib/action_controller/test_case.rb +12 -2
  34. data/lib/action_dispatch/http/cache.rb +138 -11
  35. data/lib/action_dispatch/http/content_security_policy.rb +14 -1
  36. data/lib/action_dispatch/http/filter_parameters.rb +5 -3
  37. data/lib/action_dispatch/http/mime_negotiation.rb +55 -1
  38. data/lib/action_dispatch/http/mime_types.rb +1 -0
  39. data/lib/action_dispatch/http/param_builder.rb +187 -0
  40. data/lib/action_dispatch/http/param_error.rb +26 -0
  41. data/lib/action_dispatch/http/parameters.rb +3 -3
  42. data/lib/action_dispatch/http/permissions_policy.rb +6 -0
  43. data/lib/action_dispatch/http/query_parser.rb +55 -0
  44. data/lib/action_dispatch/http/request.rb +70 -21
  45. data/lib/action_dispatch/http/response.rb +50 -16
  46. data/lib/action_dispatch/http/url.rb +110 -14
  47. data/lib/action_dispatch/journey/gtg/simulator.rb +33 -12
  48. data/lib/action_dispatch/journey/gtg/transition_table.rb +33 -41
  49. data/lib/action_dispatch/journey/nodes/node.rb +2 -1
  50. data/lib/action_dispatch/journey/parser.rb +99 -196
  51. data/lib/action_dispatch/journey/route.rb +45 -31
  52. data/lib/action_dispatch/journey/router/utils.rb +8 -14
  53. data/lib/action_dispatch/journey/router.rb +59 -81
  54. data/lib/action_dispatch/journey/routes.rb +7 -0
  55. data/lib/action_dispatch/journey/scanner.rb +44 -42
  56. data/lib/action_dispatch/journey/visitors.rb +55 -23
  57. data/lib/action_dispatch/journey/visualizer/fsm.js +4 -6
  58. data/lib/action_dispatch/log_subscriber.rb +7 -3
  59. data/lib/action_dispatch/middleware/cookies.rb +8 -4
  60. data/lib/action_dispatch/middleware/debug_exceptions.rb +24 -5
  61. data/lib/action_dispatch/middleware/debug_view.rb +11 -5
  62. data/lib/action_dispatch/middleware/exception_wrapper.rb +11 -11
  63. data/lib/action_dispatch/middleware/executor.rb +12 -2
  64. data/lib/action_dispatch/middleware/public_exceptions.rb +1 -5
  65. data/lib/action_dispatch/middleware/remote_ip.rb +11 -5
  66. data/lib/action_dispatch/middleware/request_id.rb +2 -1
  67. data/lib/action_dispatch/middleware/session/cache_store.rb +17 -0
  68. data/lib/action_dispatch/middleware/ssl.rb +13 -3
  69. data/lib/action_dispatch/middleware/templates/rescues/_copy_button.html.erb +1 -0
  70. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +3 -5
  71. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +9 -5
  72. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +1 -0
  73. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +1 -0
  74. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +4 -0
  75. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +3 -0
  76. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +50 -0
  77. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +1 -0
  78. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +1 -0
  79. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +1 -0
  80. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +1 -0
  81. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +1 -0
  82. data/lib/action_dispatch/railtie.rb +21 -0
  83. data/lib/action_dispatch/request/session.rb +1 -0
  84. data/lib/action_dispatch/request/utils.rb +9 -3
  85. data/lib/action_dispatch/routing/inspector.rb +80 -57
  86. data/lib/action_dispatch/routing/mapper.rb +404 -223
  87. data/lib/action_dispatch/routing/polymorphic_routes.rb +2 -2
  88. data/lib/action_dispatch/routing/redirection.rb +10 -7
  89. data/lib/action_dispatch/routing/route_set.rb +21 -12
  90. data/lib/action_dispatch/routing/routes_proxy.rb +1 -0
  91. data/lib/action_dispatch/structured_event_subscriber.rb +20 -0
  92. data/lib/action_dispatch/system_test_case.rb +3 -3
  93. data/lib/action_dispatch/system_testing/browser.rb +12 -21
  94. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +2 -2
  95. data/lib/action_dispatch/testing/assertions/response.rb +26 -2
  96. data/lib/action_dispatch/testing/assertions/routing.rb +27 -15
  97. data/lib/action_dispatch/testing/integration.rb +18 -7
  98. data/lib/action_dispatch.rb +14 -4
  99. data/lib/action_pack/gem_version.rb +2 -2
  100. metadata +18 -48
  101. data/lib/action_dispatch/journey/parser.y +0 -50
  102. data/lib/action_dispatch/journey/parser_extras.rb +0 -33
@@ -25,11 +25,7 @@ module ActionDispatch
25
25
  def call(env)
26
26
  request = ActionDispatch::Request.new(env)
27
27
  status = request.path_info[1..-1].to_i
28
- begin
29
- content_type = request.formats.first
30
- rescue ActionDispatch::Http::MimeNegotiation::InvalidType
31
- content_type = Mime[:text]
32
- end
28
+ content_type = request.formats.first
33
29
  body = { status: status, error: Rack::Utils::HTTP_STATUS_CODES.fetch(status, Rack::Utils::HTTP_STATUS_CODES[500]) }
34
30
 
35
31
  if env["action_dispatch.original_request_method"] == "HEAD"
@@ -44,6 +44,8 @@ module ActionDispatch
44
44
  "10.0.0.0/8", # private IPv4 range 10.x.x.x
45
45
  "172.16.0.0/12", # private IPv4 range 172.16.0.0 .. 172.31.255.255
46
46
  "192.168.0.0/16", # private IPv4 range 192.168.x.x
47
+ "169.254.0.0/16", # link-local IPv4 range 169.254.x.x
48
+ "fe80::/10", # link-local IPv6 range fe80::/10
47
49
  ].map { |proxy| IPAddr.new(proxy) }
48
50
 
49
51
  attr_reader :check_ip, :proxies
@@ -126,11 +128,11 @@ module ActionDispatch
126
128
  # left, which was presumably set by one of those proxies.
127
129
  def calculate_ip
128
130
  # Set by the Rack web server, this is a single value.
129
- remote_addr = ips_from(@req.remote_addr).last
131
+ remote_addr = sanitize_ips(ips_from(@req.remote_addr)).last
130
132
 
131
133
  # Could be a CSV list and/or repeated headers that were concatenated.
132
- client_ips = ips_from(@req.client_ip).reverse!
133
- forwarded_ips = ips_from(@req.x_forwarded_for).reverse!
134
+ client_ips = sanitize_ips(ips_from(@req.client_ip)).reverse!
135
+ forwarded_ips = sanitize_ips(@req.forwarded_for || []).reverse!
134
136
 
135
137
  # `Client-Ip` and `X-Forwarded-For` should not, generally, both be set. If they
136
138
  # are both set, it means that either:
@@ -150,7 +152,8 @@ module ActionDispatch
150
152
  # We don't know which came from the proxy, and which from the user
151
153
  raise IpSpoofAttackError, "IP spoofing attack?! " \
152
154
  "HTTP_CLIENT_IP=#{@req.client_ip.inspect} " \
153
- "HTTP_X_FORWARDED_FOR=#{@req.x_forwarded_for.inspect}"
155
+ "HTTP_X_FORWARDED_FOR=#{@req.x_forwarded_for.inspect}" \
156
+ " HTTP_FORWARDED=" + @req.forwarded_for.map { "for=#{_1}" }.join(", ").inspect if @req.forwarded_for.any?
154
157
  end
155
158
 
156
159
  # We assume these things about the IP headers:
@@ -176,7 +179,10 @@ module ActionDispatch
176
179
  def ips_from(header) # :doc:
177
180
  return [] unless header
178
181
  # Split the comma-separated list into an array of strings.
179
- ips = header.strip.split(/[,\s]+/)
182
+ header.strip.split(/[,\s]+/)
183
+ end
184
+
185
+ def sanitize_ips(ips) # :doc:
180
186
  ips.select! do |ip|
181
187
  # Only return IPs that are valid according to the IPAddr#new method.
182
188
  range = IPAddr.new(ip).to_range
@@ -25,11 +25,12 @@ module ActionDispatch
25
25
  def initialize(app, header:)
26
26
  @app = app
27
27
  @header = header
28
+ @env_header = "HTTP_#{header.upcase.tr("-", "_")}"
28
29
  end
29
30
 
30
31
  def call(env)
31
32
  req = ActionDispatch::Request.new env
32
- req.request_id = make_request_id(req.headers[@header])
33
+ req.request_id = make_request_id(req.get_header(@env_header))
33
34
  @app.call(env).tap { |_status, headers, _body| headers[@header] = req.request_id }
34
35
  end
35
36
 
@@ -18,11 +18,16 @@ module ActionDispatch
18
18
  # * `expire_after` - The length of time a session will be stored before
19
19
  # automatically expiring. By default, the `:expires_in` option of the cache
20
20
  # is used.
21
+ # * `check_collisions` - Check if newly generated session ids aren't already in use.
22
+ # If for some reason 128 bits of randomness aren't considered secure enough to avoid
23
+ # collisions, this option can be enabled to ensure newly generated ids aren't in use.
24
+ # By default, it is set to `false` to avoid additional cache write operations.
21
25
  #
22
26
  class CacheStore < AbstractSecureStore
23
27
  def initialize(app, options = {})
24
28
  @cache = options[:cache] || Rails.cache
25
29
  options[:expire_after] ||= @cache.options[:expires_in]
30
+ @check_collisions = options[:check_collisions] || false
26
31
  super
27
32
  end
28
33
 
@@ -61,6 +66,18 @@ module ActionDispatch
61
66
  def get_session_with_fallback(sid)
62
67
  @cache.read(cache_key(sid.private_id)) || @cache.read(cache_key(sid.public_id))
63
68
  end
69
+
70
+ def generate_sid
71
+ if @check_collisions
72
+ loop do
73
+ sid = super
74
+ key = cache_key(sid.private_id)
75
+ break sid if @cache.write(key, {}, unless_exist: true, expires_in: default_options[:expire_after])
76
+ end
77
+ else
78
+ super
79
+ end
80
+ end
64
81
  end
65
82
  end
66
83
  end
@@ -11,9 +11,11 @@ module ActionDispatch
11
11
  #
12
12
  # 1. **TLS redirect**: Permanently redirects `http://` requests to `https://`
13
13
  # with the same URL host, path, etc. Enabled by default. Set
14
- # `config.ssl_options` to modify the destination URL (e.g. `redirect: {
15
- # host: "secure.widgets.com", port: 8080 }`), or set `redirect: false` to
16
- # disable this feature.
14
+ # `config.ssl_options` to modify the destination URL:
15
+ #
16
+ # config.ssl_options = { redirect: { host: "secure.widgets.com", port: 8080 }`
17
+ #
18
+ # Or set `redirect: false` to disable redirection.
17
19
  #
18
20
  # Requests can opt-out of redirection with `exclude`:
19
21
  #
@@ -21,6 +23,14 @@ module ActionDispatch
21
23
  #
22
24
  # Cookies will not be flagged as secure for excluded requests.
23
25
  #
26
+ # When proxying through a load balancer that terminates SSL, the forwarded
27
+ # request will appear as though it's HTTP instead of HTTPS to the application.
28
+ # This makes redirects and cookie security target HTTP instead of HTTPS.
29
+ # To make the server assume that the proxy already terminated SSL, and
30
+ # that the request really is HTTPS, set `config.assume_ssl` to `true`:
31
+ #
32
+ # config.assume_ssl = true
33
+ #
24
34
  # 2. **Secure cookies**: Sets the `secure` flag on cookies to tell browsers
25
35
  # they must not be sent along with `http://` requests. Enabled by default.
26
36
  # Set `config.ssl_options` with `secure_cookies: false` to disable this
@@ -0,0 +1 @@
1
+ <button onclick="copyAsText.bind(this)()">Copy as text</button>
@@ -11,8 +11,9 @@
11
11
  <tr>
12
12
  <td>
13
13
  <pre class="line_numbers">
14
- <% source_extract[:code].each_key do |line_number| %>
15
- <span><%= line_number -%></span>
14
+ <% source_extract[:code].each_key do |line| %>
15
+ <% file_url = editor_url(source_extract[:trace], line: line) %>
16
+ <span><%= link_to_if file_url, line, file_url -%></span>
16
17
  <% end %>
17
18
  </pre>
18
19
  </td>
@@ -28,9 +29,6 @@
28
29
  </tr>
29
30
  </table>
30
31
  </div>
31
- <%- unless self.error_highlight_available? -%>
32
- <p class="error_highlight_tip">Tip: You may want to add <code>gem "error_highlight", "&gt;= 0.4.0"</code> into your Gemfile, which will display the fine-grained error location.</p>
33
- <%- end -%>
34
32
  </div>
35
33
  <% end %>
36
34
  <% end %>
@@ -13,13 +13,17 @@
13
13
  <% end %>
14
14
 
15
15
  <% traces.each do |name, trace| %>
16
- <div id="<%= "#{name.gsub(/\s/, '-')}-#{error_index}" %>" style="display: <%= (name == trace_to_show) ? 'block' : 'none' %>;">
16
+ <div id="<%= "#{name.gsub(/\s/, '-')}-#{error_index}" %>" class="trace-container" style="display: <%= (name == trace_to_show) ? 'block' : 'none' %>;">
17
17
  <code class="traces">
18
18
  <% trace.each do |frame| %>
19
- <a class="trace-frames trace-frames-<%= error_index %>" data-exception-object-id="<%= frame[:exception_object_id] %>" data-frame-id="<%= frame[:id] %>" href="#">
20
- <%= frame[:trace] %>
21
- </a>
22
- <br>
19
+ <div class="trace">
20
+ <% file_url = editor_url(frame[:trace]) %>
21
+ <%= file_url && link_to("✏️", file_url, class: "edit-icon") %>
22
+ <a class="trace-frames trace-frames-<%= error_index %>" data-exception-object-id="<%= frame[:exception_object_id] %>" data-frame-id="<%= frame[:id] %>" href="#">
23
+ <%= frame[:trace] %>
24
+ </a>
25
+ <br>
26
+ </div>
23
27
  <% end %>
24
28
  </code>
25
29
  </div>
@@ -1,4 +1,5 @@
1
1
  <header>
2
+ <%= render "rescues/copy_button" %>
2
3
  <h1>Blocked hosts: <%= @hosts.join(", ") %></h1>
3
4
  </header>
4
5
  <main role="main" id="container">
@@ -1,4 +1,5 @@
1
1
  <header>
2
+ <%= render "rescues/copy_button" %>
2
3
  <h1>
3
4
  <%= @exception_wrapper.exception_class_name %>
4
5
  <% if params_valid? && @request.parameters['controller'] %>
@@ -1,4 +1,5 @@
1
1
  <header role="banner">
2
+ <%= render "rescues/copy_button" %>
2
3
  <h1>
3
4
  <%= @exception.class.to_s %>
4
5
  <% if @request.parameters['controller'] %>
@@ -10,6 +11,9 @@
10
11
  <main role="main" id="container">
11
12
  <h2>
12
13
  <%= h @exception.message %>
14
+ <% if defined?(ActionText) && @exception.message.match?(%r{#{ActionText::RichText.table_name}}) %>
15
+ <br />To resolve this issue run: bin/rails action_text:install
16
+ <% end %>
13
17
  <% if defined?(ActiveStorage) && @exception.message.match?(%r{#{ActiveStorage::Blob.table_name}|#{ActiveStorage::Attachment.table_name}}) %>
14
18
  <br />To resolve this issue run: bin/rails active_storage:install
15
19
  <% end %>
@@ -4,6 +4,9 @@
4
4
  <% end %>
5
5
 
6
6
  <%= @exception.message %>
7
+ <% if defined?(ActionText) && @exception.message.match?(%r{#{ActionText::RichText.table_name}}) %>
8
+ To resolve this issue run: bin/rails action_text:install
9
+ <% end %>
7
10
  <% if defined?(ActiveStorage) && @exception.message.match?(%r{#{ActiveStorage::Blob.table_name}|#{ActiveStorage::Attachment.table_name}}) %>
8
11
  To resolve this issue run: bin/rails active_storage:install
9
12
  <% end %>
@@ -38,6 +38,22 @@
38
38
  padding: 0.5em 1.5em;
39
39
  }
40
40
 
41
+ header button {
42
+ appearance: none;
43
+ background-color: hsl(0 0% 0% / 0.2);
44
+ border: 0;
45
+ border-radius: 14px;
46
+ color: white;
47
+ float: right;
48
+ font-weight: 500;
49
+ height: 28px;
50
+ padding-inline: 14px;
51
+ margin: 0.35em 0;
52
+ }
53
+ header button:active {
54
+ background-color: hsl(0 0% 0% / 0.25);
55
+ }
56
+
41
57
  h1 {
42
58
  overflow-wrap: break-word;
43
59
  margin: 0.2em 0;
@@ -54,6 +70,30 @@
54
70
  font-size: 11px;
55
71
  }
56
72
 
73
+ .trace-container {
74
+ margin-top: 10px;
75
+ }
76
+
77
+ code.traces .trace {
78
+ display: flex;
79
+ align-items: center;
80
+ gap: 2px;
81
+ }
82
+
83
+ .edit-icon {
84
+ width: 16px;
85
+ height: 16px;
86
+ display: flex;
87
+ font-size: 13px;
88
+ align-items: center;
89
+ justify-content: center;
90
+ text-decoration: none;
91
+ }
92
+
93
+ .edit-icon:hover {
94
+ scale: 1.05;
95
+ }
96
+
57
97
  .response-heading, .request-heading {
58
98
  margin-top: 30px;
59
99
  }
@@ -274,11 +314,21 @@
274
314
  var toggleEnvDump = function() {
275
315
  return toggle('env_dump');
276
316
  }
317
+ var copyAsText = function() {
318
+ const text = document.getElementById("exception-message-for-copy").textContent;
319
+
320
+ navigator.clipboard.writeText(text).then(() => {
321
+ const beforeText = this.innerText;
322
+ this.innerText = "Copied!"
323
+ setTimeout(() => this.innerText = beforeText, 1000)
324
+ })
325
+ }
277
326
  </script>
278
327
  </head>
279
328
  <body>
280
329
 
281
330
  <%= yield %>
331
+ <script type="text/plain" id="exception-message-for-copy"><%= @exception_message_for_copy %></script>
282
332
 
283
333
  </body>
284
334
  </html>
@@ -1,4 +1,5 @@
1
1
  <header role="banner">
2
+ <%= render "rescues/copy_button" %>
2
3
  <h1>No view template for interactive request</h1>
3
4
  </header>
4
5
 
@@ -1,4 +1,5 @@
1
1
  <header role="banner">
2
+ <%= render "rescues/copy_button" %>
2
3
  <h1>Template is missing</h1>
3
4
  </header>
4
5
 
@@ -1,4 +1,5 @@
1
1
  <header role="banner">
2
+ <%= render "rescues/copy_button" %>
2
3
  <h1>Routing Error</h1>
3
4
  </header>
4
5
  <main role="main" id="container">
@@ -1,4 +1,5 @@
1
1
  <header role="banner">
2
+ <%= render "rescues/copy_button" %>
2
3
  <h1>
3
4
  <%= @exception_wrapper.exception_name %> in
4
5
  <%= @request.parameters["controller"].camelize if @request.parameters["controller"] %>#<%= @request.parameters["action"] %>
@@ -1,4 +1,5 @@
1
1
  <header role="banner">
2
+ <%= render "rescues/copy_button" %>
2
3
  <h1>Unknown action</h1>
3
4
  </header>
4
5
  <main role="main" id="container">
@@ -4,7 +4,9 @@
4
4
 
5
5
  require "action_dispatch"
6
6
  require "action_dispatch/log_subscriber"
7
+ require "action_dispatch/structured_event_subscriber"
7
8
  require "active_support/messages/rotation_configuration"
9
+ require "rails/railtie"
8
10
 
9
11
  module ActionDispatch
10
12
  class Railtie < Rails::Railtie # :nodoc:
@@ -29,6 +31,11 @@ module ActionDispatch
29
31
  config.action_dispatch.request_id_header = ActionDispatch::Constants::X_REQUEST_ID
30
32
  config.action_dispatch.log_rescued_responses = true
31
33
  config.action_dispatch.debug_exception_log_level = :fatal
34
+ config.action_dispatch.strict_freshness = false
35
+
36
+ config.action_dispatch.ignore_leading_brackets = nil
37
+ config.action_dispatch.strict_query_string_separator = nil
38
+ config.action_dispatch.verbose_redirect_logs = false
32
39
 
33
40
  config.action_dispatch.default_headers = {
34
41
  "X-Frame-Options" => "SAMEORIGIN",
@@ -51,6 +58,19 @@ module ActionDispatch
51
58
  ActionDispatch::Http::URL.secure_protocol = app.config.force_ssl
52
59
  ActionDispatch::Http::URL.tld_length = app.config.action_dispatch.tld_length
53
60
 
61
+ unless app.config.action_dispatch.domain_extractor.nil?
62
+ ActionDispatch::Http::URL.domain_extractor = app.config.action_dispatch.domain_extractor
63
+ end
64
+
65
+ unless app.config.action_dispatch.ignore_leading_brackets.nil?
66
+ ActionDispatch::ParamBuilder.ignore_leading_brackets = app.config.action_dispatch.ignore_leading_brackets
67
+ end
68
+ unless app.config.action_dispatch.strict_query_string_separator.nil?
69
+ ActionDispatch::QueryParser.strict_query_string_separator = app.config.action_dispatch.strict_query_string_separator
70
+ end
71
+
72
+ ActionDispatch.verbose_redirect_logs = app.config.action_dispatch.verbose_redirect_logs
73
+
54
74
  ActiveSupport.on_load(:action_dispatch_request) do
55
75
  self.ignore_accept_header = app.config.action_dispatch.ignore_accept_header
56
76
  ActionDispatch::Request::Utils.perform_deep_munge = app.config.action_dispatch.perform_deep_munge
@@ -69,6 +89,7 @@ module ActionDispatch
69
89
 
70
90
  ActionDispatch::Routing::Mapper.route_source_locations = Rails.env.development?
71
91
 
92
+ ActionDispatch::Http::Cache::Request.strict_freshness = app.config.action_dispatch.strict_freshness
72
93
  ActionDispatch.test_app = app
73
94
  end
74
95
  end
@@ -155,6 +155,7 @@ module ActionDispatch
155
155
  load_for_write!
156
156
  @delegate[key.to_s] = value
157
157
  end
158
+ alias store []=
158
159
 
159
160
  # Clears the session.
160
161
  def clear
@@ -83,8 +83,8 @@ module ActionDispatch
83
83
  end
84
84
 
85
85
  class CustomParamEncoder # :nodoc:
86
- def self.encode(request, params, controller, action)
87
- return params unless controller && controller.valid_encoding? && encoding_template = action_encoding_template(request, controller, action)
86
+ def self.encode_for_template(params, encoding_template)
87
+ return params unless encoding_template
88
88
  params.except(:controller, :action).each do |key, value|
89
89
  ActionDispatch::Request::Utils.each_param_value(value) do |param|
90
90
  # If `param` is frozen, it comes from the router defaults
@@ -98,8 +98,14 @@ module ActionDispatch
98
98
  params
99
99
  end
100
100
 
101
+ def self.encode(request, params, controller, action)
102
+ encoding_template = action_encoding_template(request, controller, action)
103
+ encode_for_template(params, encoding_template)
104
+ end
105
+
101
106
  def self.action_encoding_template(request, controller, action) # :nodoc:
102
- request.controller_class_for(controller).action_encoding_template(action)
107
+ controller && controller.valid_encoding? &&
108
+ request.controller_class_for(controller).action_encoding_template(action)
103
109
  rescue MissingController
104
110
  nil
105
111
  end
@@ -64,6 +64,14 @@ module ActionDispatch
64
64
  def engine?
65
65
  app.engine?
66
66
  end
67
+
68
+ def to_h
69
+ { name: name,
70
+ verb: verb,
71
+ path: path,
72
+ reqs: reqs,
73
+ source_location: source_location }
74
+ end
67
75
  end
68
76
 
69
77
  ##
@@ -72,36 +80,57 @@ module ActionDispatch
72
80
  # not use this class.
73
81
  class RoutesInspector # :nodoc:
74
82
  def initialize(routes)
75
- @engines = {}
76
- @routes = routes
83
+ @routes = wrap_routes(routes)
84
+ @engines = load_engines_routes
77
85
  end
78
86
 
79
87
  def format(formatter, filter = {})
80
- routes_to_display = filter_routes(normalize_filter(filter))
81
- routes = collect_routes(routes_to_display)
82
- if routes.none?
83
- formatter.no_routes(collect_routes(@routes), filter)
84
- return formatter.result
85
- end
88
+ all_routes = { nil => @routes }.merge(@engines)
86
89
 
87
- formatter.header routes
88
- formatter.section routes
89
-
90
- @engines.each do |name, engine_routes|
91
- formatter.section_title "Routes for #{name}"
92
- formatter.section engine_routes
90
+ all_routes.each do |engine_name, routes|
91
+ format_routes(formatter, filter, engine_name, routes)
93
92
  end
94
93
 
95
94
  formatter.result
96
95
  end
97
96
 
98
97
  private
98
+ def format_routes(formatter, filter, engine_name, routes)
99
+ routes = filter_routes(routes, normalize_filter(filter)).map(&:to_h)
100
+
101
+ formatter.section_title "Routes for #{engine_name || "application"}" if @engines.any?
102
+ if routes.any?
103
+ formatter.header routes
104
+ formatter.section routes
105
+ else
106
+ formatter.no_routes engine_name, routes, filter
107
+ end
108
+ formatter.footer routes
109
+ end
110
+
111
+ def wrap_routes(routes)
112
+ routes.routes.map { |route| RouteWrapper.new(route) }.reject(&:internal?)
113
+ end
114
+
115
+ def load_engines_routes
116
+ engine_routes = @routes.select(&:engine?)
117
+
118
+ engines = engine_routes.to_h do |engine_route|
119
+ engine_app_routes = engine_route.rack_app.routes
120
+ engine_app_routes = engine_app_routes.routes if engine_app_routes.is_a?(ActionDispatch::Routing::RouteSet)
121
+
122
+ [engine_route.endpoint, wrap_routes(engine_app_routes)]
123
+ end
124
+
125
+ engines
126
+ end
127
+
99
128
  def normalize_filter(filter)
100
129
  if filter[:controller]
101
130
  { controller: /#{filter[:controller].underscore.sub(/_?controller\z/, "")}/ }
102
131
  elsif filter[:grep]
103
132
  grep_pattern = Regexp.new(filter[:grep])
104
- path = RFC2396_PARSER.escape(filter[:grep])
133
+ path = URI::RFC2396_PARSER.escape(filter[:grep])
105
134
  normalized_path = ("/" + path).squeeze("/")
106
135
 
107
136
  {
@@ -115,39 +144,13 @@ module ActionDispatch
115
144
  end
116
145
  end
117
146
 
118
- def filter_routes(filter)
147
+ def filter_routes(routes, filter)
119
148
  if filter
120
- @routes.select do |route|
121
- route_wrapper = RouteWrapper.new(route)
122
- filter.any? { |filter_type, value| route_wrapper.matches_filter?(filter_type, value) }
149
+ routes.select do |route|
150
+ filter.any? { |filter_type, value| route.matches_filter?(filter_type, value) }
123
151
  end
124
152
  else
125
- @routes
126
- end
127
- end
128
-
129
- def collect_routes(routes)
130
- routes.collect do |route|
131
- RouteWrapper.new(route)
132
- end.reject(&:internal?).collect do |route|
133
- collect_engine_routes(route)
134
-
135
- { name: route.name,
136
- verb: route.verb,
137
- path: route.path,
138
- reqs: route.reqs,
139
- source_location: route.source_location }
140
- end
141
- end
142
-
143
- def collect_engine_routes(route)
144
- name = route.endpoint
145
- return unless route.engine?
146
- return if @engines[name]
147
-
148
- routes = route.rack_app.routes
149
- if routes.is_a?(ActionDispatch::Routing::RouteSet)
150
- @engines[name] = collect_routes(routes.routes)
153
+ routes
151
154
  end
152
155
  end
153
156
  end
@@ -171,27 +174,36 @@ module ActionDispatch
171
174
  def header(routes)
172
175
  end
173
176
 
174
- def no_routes(routes, filter)
175
- @buffer <<
176
- if routes.none?
177
- <<~MESSAGE
178
- You don't have any routes defined!
177
+ def footer(routes)
178
+ end
179
179
 
180
- Please add some routes in config/routes.rb.
181
- MESSAGE
182
- elsif filter.key?(:controller)
180
+ def no_routes(engine, routes, filter)
181
+ @buffer <<
182
+ if filter.key?(:controller)
183
183
  "No routes were found for this controller."
184
184
  elsif filter.key?(:grep)
185
185
  "No routes were found for this grep pattern."
186
+ elsif routes.none?
187
+ if engine
188
+ "No routes defined."
189
+ else
190
+ <<~MESSAGE
191
+ You don't have any routes defined!
192
+
193
+ Please add some routes in config/routes.rb.
194
+ MESSAGE
195
+ end
186
196
  end
187
197
 
188
- @buffer << "For more information about routes, see the Rails guide: https://guides.rubyonrails.org/routing.html."
198
+ unless engine
199
+ @buffer << "For more information about routes, see the Rails guide: https://guides.rubyonrails.org/routing.html."
200
+ end
189
201
  end
190
202
  end
191
203
 
192
204
  class Sheet < Base
193
205
  def section_title(title)
194
- @buffer << "\n#{title}:"
206
+ @buffer << "#{title}:"
195
207
  end
196
208
 
197
209
  def section(routes)
@@ -202,6 +214,10 @@ module ActionDispatch
202
214
  @buffer << draw_header(routes)
203
215
  end
204
216
 
217
+ def footer(routes)
218
+ @buffer << ""
219
+ end
220
+
205
221
  private
206
222
  def draw_section(routes)
207
223
  header_lengths = ["Prefix", "Verb", "URI Pattern"].map(&:length)
@@ -232,13 +248,17 @@ module ActionDispatch
232
248
  end
233
249
 
234
250
  def section_title(title)
235
- @buffer << "\n#{"[ #{title} ]"}"
251
+ @buffer << "#{"[ #{title} ]"}"
236
252
  end
237
253
 
238
254
  def section(routes)
239
255
  @buffer << draw_expanded_section(routes)
240
256
  end
241
257
 
258
+ def footer(routes)
259
+ @buffer << ""
260
+ end
261
+
242
262
  private
243
263
  def draw_expanded_section(routes)
244
264
  routes.map.each_with_index do |r, i|
@@ -269,7 +289,7 @@ module ActionDispatch
269
289
  super
270
290
  end
271
291
 
272
- def no_routes(routes, filter)
292
+ def no_routes(engine, routes, filter)
273
293
  @buffer <<
274
294
  if filter.none?
275
295
  "No unused routes found."
@@ -300,6 +320,9 @@ module ActionDispatch
300
320
  def header(routes)
301
321
  end
302
322
 
323
+ def footer(routes)
324
+ end
325
+
303
326
  def no_routes(*)
304
327
  @buffer << <<~MESSAGE
305
328
  <p>You don't have any routes defined!</p>