actionpack 7.0.4 → 7.1.3.4

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 (140) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +397 -269
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +4 -4
  5. data/lib/abstract_controller/base.rb +20 -11
  6. data/lib/abstract_controller/caching/fragments.rb +2 -0
  7. data/lib/abstract_controller/callbacks.rb +31 -6
  8. data/lib/abstract_controller/deprecator.rb +7 -0
  9. data/lib/abstract_controller/helpers.rb +75 -28
  10. data/lib/abstract_controller/railties/routes_helpers.rb +1 -16
  11. data/lib/abstract_controller/rendering.rb +12 -14
  12. data/lib/abstract_controller/translation.rb +9 -6
  13. data/lib/abstract_controller/url_for.rb +2 -0
  14. data/lib/abstract_controller.rb +6 -0
  15. data/lib/action_controller/api.rb +6 -4
  16. data/lib/action_controller/base.rb +3 -17
  17. data/lib/action_controller/caching.rb +2 -0
  18. data/lib/action_controller/deprecator.rb +7 -0
  19. data/lib/action_controller/form_builder.rb +2 -0
  20. data/lib/action_controller/log_subscriber.rb +16 -4
  21. data/lib/action_controller/metal/basic_implicit_render.rb +3 -1
  22. data/lib/action_controller/metal/conditional_get.rb +121 -123
  23. data/lib/action_controller/metal/content_security_policy.rb +5 -5
  24. data/lib/action_controller/metal/data_streaming.rb +20 -18
  25. data/lib/action_controller/metal/default_headers.rb +2 -0
  26. data/lib/action_controller/metal/etag_with_flash.rb +3 -1
  27. data/lib/action_controller/metal/etag_with_template_digest.rb +2 -0
  28. data/lib/action_controller/metal/exceptions.rb +8 -0
  29. data/lib/action_controller/metal/head.rb +9 -7
  30. data/lib/action_controller/metal/helpers.rb +3 -14
  31. data/lib/action_controller/metal/http_authentication.rb +17 -8
  32. data/lib/action_controller/metal/implicit_render.rb +5 -3
  33. data/lib/action_controller/metal/instrumentation.rb +8 -1
  34. data/lib/action_controller/metal/live.rb +25 -1
  35. data/lib/action_controller/metal/mime_responds.rb +2 -2
  36. data/lib/action_controller/metal/params_wrapper.rb +4 -2
  37. data/lib/action_controller/metal/permissions_policy.rb +2 -2
  38. data/lib/action_controller/metal/redirecting.rb +29 -8
  39. data/lib/action_controller/metal/renderers.rb +4 -4
  40. data/lib/action_controller/metal/rendering.rb +114 -9
  41. data/lib/action_controller/metal/request_forgery_protection.rb +144 -53
  42. data/lib/action_controller/metal/rescue.rb +6 -3
  43. data/lib/action_controller/metal/streaming.rb +71 -31
  44. data/lib/action_controller/metal/strong_parameters.rb +158 -101
  45. data/lib/action_controller/metal/url_for.rb +9 -4
  46. data/lib/action_controller/metal.rb +79 -21
  47. data/lib/action_controller/railtie.rb +24 -10
  48. data/lib/action_controller/renderer.rb +99 -85
  49. data/lib/action_controller/test_case.rb +15 -5
  50. data/lib/action_controller.rb +8 -1
  51. data/lib/action_dispatch/constants.rb +32 -0
  52. data/lib/action_dispatch/deprecator.rb +7 -0
  53. data/lib/action_dispatch/http/cache.rb +9 -11
  54. data/lib/action_dispatch/http/content_security_policy.rb +14 -9
  55. data/lib/action_dispatch/http/filter_parameters.rb +14 -28
  56. data/lib/action_dispatch/http/headers.rb +3 -1
  57. data/lib/action_dispatch/http/mime_negotiation.rb +22 -22
  58. data/lib/action_dispatch/http/mime_type.rb +35 -12
  59. data/lib/action_dispatch/http/mime_types.rb +3 -1
  60. data/lib/action_dispatch/http/parameters.rb +1 -1
  61. data/lib/action_dispatch/http/permissions_policy.rb +38 -23
  62. data/lib/action_dispatch/http/rack_cache.rb +2 -0
  63. data/lib/action_dispatch/http/request.rb +63 -30
  64. data/lib/action_dispatch/http/response.rb +80 -63
  65. data/lib/action_dispatch/http/upload.rb +15 -2
  66. data/lib/action_dispatch/journey/formatter.rb +8 -2
  67. data/lib/action_dispatch/journey/path/pattern.rb +14 -14
  68. data/lib/action_dispatch/journey/route.rb +3 -2
  69. data/lib/action_dispatch/journey/router.rb +9 -8
  70. data/lib/action_dispatch/journey/routes.rb +2 -2
  71. data/lib/action_dispatch/log_subscriber.rb +23 -0
  72. data/lib/action_dispatch/middleware/actionable_exceptions.rb +5 -6
  73. data/lib/action_dispatch/middleware/assume_ssl.rb +24 -0
  74. data/lib/action_dispatch/middleware/callbacks.rb +2 -0
  75. data/lib/action_dispatch/middleware/cookies.rb +108 -117
  76. data/lib/action_dispatch/middleware/debug_exceptions.rb +26 -25
  77. data/lib/action_dispatch/middleware/debug_locks.rb +4 -1
  78. data/lib/action_dispatch/middleware/debug_view.rb +7 -2
  79. data/lib/action_dispatch/middleware/exception_wrapper.rb +186 -27
  80. data/lib/action_dispatch/middleware/executor.rb +1 -1
  81. data/lib/action_dispatch/middleware/flash.rb +7 -0
  82. data/lib/action_dispatch/middleware/host_authorization.rb +18 -8
  83. data/lib/action_dispatch/middleware/public_exceptions.rb +5 -3
  84. data/lib/action_dispatch/middleware/reloader.rb +7 -5
  85. data/lib/action_dispatch/middleware/remote_ip.rb +21 -20
  86. data/lib/action_dispatch/middleware/request_id.rb +4 -2
  87. data/lib/action_dispatch/middleware/server_timing.rb +4 -4
  88. data/lib/action_dispatch/middleware/session/abstract_store.rb +5 -0
  89. data/lib/action_dispatch/middleware/session/cache_store.rb +2 -0
  90. data/lib/action_dispatch/middleware/session/cookie_store.rb +11 -5
  91. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +3 -1
  92. data/lib/action_dispatch/middleware/show_exceptions.rb +25 -18
  93. data/lib/action_dispatch/middleware/ssl.rb +18 -6
  94. data/lib/action_dispatch/middleware/stack.rb +7 -2
  95. data/lib/action_dispatch/middleware/static.rb +14 -10
  96. data/lib/action_dispatch/middleware/templates/rescues/_actions.html.erb +2 -2
  97. data/lib/action_dispatch/middleware/templates/rescues/_message_and_suggestions.html.erb +4 -4
  98. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +8 -1
  99. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +7 -3
  100. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb +5 -3
  101. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +7 -7
  102. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +2 -2
  103. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +17 -0
  104. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +16 -12
  105. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +1 -1
  106. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +3 -3
  107. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +4 -4
  108. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +1 -1
  109. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb +1 -1
  110. data/lib/action_dispatch/middleware/templates/routes/_route.html.erb +3 -0
  111. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +59 -41
  112. data/lib/action_dispatch/railtie.rb +14 -4
  113. data/lib/action_dispatch/request/session.rb +16 -6
  114. data/lib/action_dispatch/request/utils.rb +8 -3
  115. data/lib/action_dispatch/routing/inspector.rb +54 -6
  116. data/lib/action_dispatch/routing/mapper.rb +58 -24
  117. data/lib/action_dispatch/routing/polymorphic_routes.rb +2 -0
  118. data/lib/action_dispatch/routing/redirection.rb +15 -6
  119. data/lib/action_dispatch/routing/route_set.rb +52 -22
  120. data/lib/action_dispatch/routing/routes_proxy.rb +10 -15
  121. data/lib/action_dispatch/routing/url_for.rb +26 -22
  122. data/lib/action_dispatch/routing.rb +7 -7
  123. data/lib/action_dispatch/system_test_case.rb +3 -3
  124. data/lib/action_dispatch/system_testing/browser.rb +20 -19
  125. data/lib/action_dispatch/system_testing/driver.rb +14 -22
  126. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +27 -16
  127. data/lib/action_dispatch/testing/assertion_response.rb +1 -1
  128. data/lib/action_dispatch/testing/assertions/response.rb +14 -7
  129. data/lib/action_dispatch/testing/assertions/routing.rb +67 -28
  130. data/lib/action_dispatch/testing/assertions.rb +3 -1
  131. data/lib/action_dispatch/testing/integration.rb +27 -17
  132. data/lib/action_dispatch/testing/request_encoder.rb +4 -1
  133. data/lib/action_dispatch/testing/test_process.rb +4 -3
  134. data/lib/action_dispatch/testing/test_request.rb +1 -1
  135. data/lib/action_dispatch/testing/test_response.rb +23 -9
  136. data/lib/action_dispatch.rb +37 -4
  137. data/lib/action_pack/gem_version.rb +4 -4
  138. data/lib/action_pack/version.rb +1 -1
  139. data/lib/action_pack.rb +1 -1
  140. metadata +65 -29
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionDispatch
4
+ class LogSubscriber < ActiveSupport::LogSubscriber
5
+ def redirect(event)
6
+ payload = event.payload
7
+
8
+ info { "Redirected to #{payload[:location]}" }
9
+
10
+ info do
11
+ status = payload[:status]
12
+
13
+ message = +"Completed #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]} in #{event.duration.round}ms"
14
+ message << "\n\n" if defined?(Rails.env) && Rails.env.development?
15
+
16
+ message
17
+ end
18
+ end
19
+ subscribe_log_level :redirect, :info
20
+ end
21
+ end
22
+
23
+ ActionDispatch::LogSubscriber.attach_to :action_dispatch
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "erb"
4
3
  require "uri"
5
4
  require "active_support/actionable_error"
6
5
 
@@ -30,15 +29,15 @@ module ActionDispatch
30
29
  uri = URI.parse location
31
30
 
32
31
  if uri.relative? || uri.scheme == "http" || uri.scheme == "https"
33
- body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(location)}\">redirected</a>.</body></html>"
32
+ body = ""
34
33
  else
35
- return [400, { "Content-Type" => "text/plain" }, ["Invalid redirection URI"]]
34
+ return [400, { Rack::CONTENT_TYPE => "text/plain; charset=utf-8" }, ["Invalid redirection URI"]]
36
35
  end
37
36
 
38
37
  [302, {
39
- "Content-Type" => "text/html; charset=#{Response.default_charset}",
40
- "Content-Length" => body.bytesize.to_s,
41
- "Location" => location,
38
+ Rack::CONTENT_TYPE => "text/html; charset=#{Response.default_charset}",
39
+ Rack::CONTENT_LENGTH => body.bytesize.to_s,
40
+ ActionDispatch::Constants::LOCATION => location,
42
41
  }, [body]]
43
42
  end
44
43
  end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionDispatch
4
+ # = Action Dispatch \AssumeSSL
5
+ #
6
+ # When proxying through a load balancer that terminates SSL, the forwarded request will appear
7
+ # as though it's HTTP instead of HTTPS to the application. This makes redirects and cookie
8
+ # security target HTTP instead of HTTPS. This middleware makes the server assume that the
9
+ # proxy already terminated SSL, and that the request really is HTTPS.
10
+ class AssumeSSL
11
+ def initialize(app)
12
+ @app = app
13
+ end
14
+
15
+ def call(env)
16
+ env["HTTPS"] = "on"
17
+ env["HTTP_X_FORWARDED_PORT"] = "443"
18
+ env["HTTP_X_FORWARDED_PROTO"] = "https"
19
+ env["rack.url_scheme"] = "https"
20
+
21
+ @app.call(env)
22
+ end
23
+ end
24
+ end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActionDispatch
4
+ # = Action Dispatch \Callbacks
5
+ #
4
6
  # Provides callbacks to be executed before and after dispatching the request.
5
7
  class Callbacks
6
8
  include ActiveSupport::Callbacks
@@ -70,7 +70,7 @@ module ActionDispatch
70
70
  end
71
71
 
72
72
  def cookies_same_site_protection
73
- get_header(Cookies::COOKIES_SAME_SITE_PROTECTION) || Proc.new { }
73
+ get_header(Cookies::COOKIES_SAME_SITE_PROTECTION)&.call(self)
74
74
  end
75
75
 
76
76
  def cookies_digest
@@ -92,10 +92,10 @@ module ActionDispatch
92
92
  include RequestCookieMethods
93
93
  end
94
94
 
95
- # Read and write data to cookies through ActionController::Base#cookies.
95
+ # Read and write data to cookies through ActionController::Cookies#cookies.
96
96
  #
97
97
  # When reading cookie data, the data is read from the HTTP request header, Cookie.
98
- # When writing cookie data, the data is sent out in the HTTP response header, Set-Cookie.
98
+ # When writing cookie data, the data is sent out in the HTTP response header, +Set-Cookie+.
99
99
  #
100
100
  # Examples of writing:
101
101
  #
@@ -160,13 +160,18 @@ module ActionDispatch
160
160
  # to <tt>:all</tt>. To support multiple domains, provide an array, and
161
161
  # the first domain matching <tt>request.host</tt> will be used. Make
162
162
  # sure to specify the <tt>:domain</tt> option with <tt>:all</tt> or
163
- # <tt>Array</tt> again when deleting cookies.
163
+ # <tt>Array</tt> again when deleting cookies. For more flexibility you
164
+ # can set the domain on a per-request basis by specifying <tt>:domain</tt>
165
+ # with a proc.
164
166
  #
165
167
  # domain: nil # Does not set cookie domain. (default)
166
168
  # domain: :all # Allow the cookie for the top most level
167
169
  # # domain and subdomains.
168
170
  # domain: %w(.example.com .example.org) # Allow the cookie
169
171
  # # for concrete domain names.
172
+ # domain: proc { Tenant.current.cookie_domain } # Set cookie domain dynamically
173
+ # domain: proc { |req| ".sub.#{req.host}" } # Set cookie domain dynamically based on request
174
+ #
170
175
  #
171
176
  # * <tt>:tld_length</tt> - When using <tt>:domain => :all</tt>, this option can be used to explicitly
172
177
  # set the TLD length when using a short (<= 3 character) domain that is being interpreted as part of a TLD.
@@ -178,7 +183,8 @@ module ActionDispatch
178
183
  # only HTTP. Defaults to +false+.
179
184
  # * <tt>:same_site</tt> - The value of the +SameSite+ cookie attribute, which
180
185
  # determines how this cookie should be restricted in cross-site contexts.
181
- # Possible values are +:none+, +:lax+, and +:strict+. Defaults to +:lax+.
186
+ # Possible values are +nil+, +:none+, +:lax+, and +:strict+. Defaults to
187
+ # +:lax+.
182
188
  class Cookies
183
189
  HTTP_HEADER = "Set-Cookie"
184
190
  GENERATOR_KEY = "action_dispatch.key_generator"
@@ -290,20 +296,6 @@ module ActionDispatch
290
296
  class CookieJar # :nodoc:
291
297
  include Enumerable, ChainedCookieJars
292
298
 
293
- # This regular expression is used to split the levels of a domain.
294
- # The top level domain can be any string without a period or
295
- # **.**, ***.** style TLDs like co.uk or com.au
296
- #
297
- # www.example.co.uk gives:
298
- # $& => example.co.uk
299
- #
300
- # example.com gives:
301
- # $& => example.com
302
- #
303
- # lots.of.subdomains.example.local gives:
304
- # $& => example.local
305
- DOMAIN_REGEXP = /[^.]*\.([^.]*|..\...|...\...)$/
306
-
307
299
  def self.build(req, cookies)
308
300
  jar = new(req)
309
301
  jar.update(cookies)
@@ -390,6 +382,8 @@ module ActionDispatch
390
382
  # Removes the cookie on the client machine by setting the value to an empty string
391
383
  # and the expiration date in the past. Like <tt>[]=</tt>, you can pass in
392
384
  # an options hash to delete cookies with extra data such as a <tt>:path</tt>.
385
+ #
386
+ # Returns the value of the cookie, or +nil+ if the cookie does not exist.
393
387
  def delete(name, options = {})
394
388
  return unless @cookies.has_key? name.to_s
395
389
 
@@ -415,9 +409,15 @@ module ActionDispatch
415
409
  @cookies.each_key { |k| delete(k, options) }
416
410
  end
417
411
 
418
- def write(headers)
419
- if header = make_set_cookie_header(headers[HTTP_HEADER])
420
- headers[HTTP_HEADER] = header
412
+ def write(response)
413
+ @set_cookies.each do |name, value|
414
+ if write_cookie?(value)
415
+ response.set_cookie(name, value)
416
+ end
417
+ end
418
+
419
+ @delete_cookies.each do |name, value|
420
+ response.delete_cookie(name, value)
421
421
  end
422
422
  end
423
423
 
@@ -428,19 +428,6 @@ module ActionDispatch
428
428
  ::Rack::Utils.escape(string)
429
429
  end
430
430
 
431
- def make_set_cookie_header(header)
432
- header = @set_cookies.inject(header) { |m, (k, v)|
433
- if write_cookie?(v)
434
- ::Rack::Utils.add_cookie_to_header(m, k, v)
435
- else
436
- m
437
- end
438
- }
439
- @delete_cookies.inject(header) { |m, (k, v)|
440
- ::Rack::Utils.add_remove_cookie_to_header(m, k, v)
441
- }
442
- end
443
-
444
431
  def write_cookie?(cookie)
445
432
  request.ssl? || !cookie[:secure] || always_write_cookie || request.host.end_with?(".onion")
446
433
  end
@@ -452,17 +439,40 @@ module ActionDispatch
452
439
 
453
440
  options[:path] ||= "/"
454
441
 
455
- cookies_same_site_protection = request.cookies_same_site_protection
456
- options[:same_site] ||= cookies_same_site_protection.call(request)
442
+ unless options.key?(:same_site)
443
+ options[:same_site] = request.cookies_same_site_protection
444
+ end
457
445
 
458
446
  if options[:domain] == :all || options[:domain] == "all"
459
- # If there is a provided tld length then we use it otherwise default domain regexp.
460
- domain_regexp = options[:tld_length] ? /([^.]+\.?){#{options[:tld_length]}}$/ : DOMAIN_REGEXP
447
+ cookie_domain = ""
448
+ dot_splitted_host = request.host.split(".", -1)
449
+
450
+ # Case where request.host is not an IP address or it's an invalid domain
451
+ # (ip confirms to the domain structure we expect so we explicitly check for ip)
452
+ if request.host.match?(/^[\d.]+$/) || dot_splitted_host.include?("") || dot_splitted_host.length == 1
453
+ options[:domain] = nil
454
+ return
455
+ end
461
456
 
462
- # If host is not ip and matches domain regexp.
463
- # (ip confirms to domain regexp so we explicitly check for ip)
464
- options[:domain] = if !request.host.match?(/^[\d.]+$/) && (request.host =~ domain_regexp)
465
- ".#{$&}"
457
+ # If there is a provided tld length then we use it otherwise default domain.
458
+ if options[:tld_length].present?
459
+ # Case where the tld_length provided is valid
460
+ if dot_splitted_host.length >= options[:tld_length]
461
+ cookie_domain = dot_splitted_host.last(options[:tld_length]).join(".")
462
+ end
463
+ # Case where tld_length is not provided
464
+ else
465
+ # Regular TLDs
466
+ if !(/\.[^.]{2,3}\.[^.]{2}\z/.match?(request.host))
467
+ cookie_domain = dot_splitted_host.last(2).join(".")
468
+ # **.**, ***.** style TLDs like co.uk and com.au
469
+ else
470
+ cookie_domain = dot_splitted_host.last(3).join(".")
471
+ end
472
+ end
473
+
474
+ options[:domain] = if cookie_domain.present?
475
+ cookie_domain
466
476
  end
467
477
  elsif options[:domain].is_a? Array
468
478
  # If host matches one of the supplied domains.
@@ -470,6 +480,8 @@ module ActionDispatch
470
480
  domain = domain.delete_prefix(".")
471
481
  request.host == domain || request.host.end_with?(".#{domain}")
472
482
  end
483
+ elsif options[:domain].respond_to?(:call)
484
+ options[:domain] = options[:domain].call(request)
473
485
  end
474
486
  end
475
487
  end
@@ -533,75 +545,57 @@ module ActionDispatch
533
545
  end
534
546
  end
535
547
 
536
- class MarshalWithJsonFallback # :nodoc:
537
- def self.load(value)
538
- Marshal.load(value)
539
- rescue TypeError => e
540
- ActiveSupport::JSON.decode(value) rescue raise e
541
- end
542
-
543
- def self.dump(value)
544
- Marshal.dump(value)
545
- end
546
- end
547
-
548
- class JsonSerializer # :nodoc:
549
- def self.load(value)
550
- ActiveSupport::JSON.decode(value)
551
- end
552
-
553
- def self.dump(value)
554
- ActiveSupport::JSON.encode(value)
555
- end
556
- end
557
-
558
548
  module SerializedCookieJars # :nodoc:
559
- MARSHAL_SIGNATURE = "\x04\x08"
560
549
  SERIALIZER = ActiveSupport::MessageEncryptor::NullSerializer
561
550
 
562
551
  protected
563
- def needs_migration?(value)
564
- request.cookies_serializer == :hybrid && value.start_with?(MARSHAL_SIGNATURE)
552
+ def digest
553
+ request.cookies_digest || "SHA1"
565
554
  end
566
555
 
567
- def serialize(value)
568
- serializer.dump(value)
556
+ private
557
+ def serializer
558
+ @serializer ||=
559
+ case request.cookies_serializer
560
+ when nil
561
+ ActiveSupport::Messages::SerializerWithFallback[:marshal]
562
+ when :hybrid
563
+ ActiveSupport::Messages::SerializerWithFallback[:json_allow_marshal]
564
+ when Symbol
565
+ ActiveSupport::Messages::SerializerWithFallback[request.cookies_serializer]
566
+ else
567
+ request.cookies_serializer
568
+ end
569
569
  end
570
570
 
571
- def deserialize(name)
572
- rotate = false
573
- value = yield -> { rotate = true }
571
+ def reserialize?(dumped)
572
+ serializer.is_a?(ActiveSupport::Messages::SerializerWithFallback) &&
573
+ serializer != ActiveSupport::Messages::SerializerWithFallback[:marshal] &&
574
+ !serializer.dumped?(dumped)
575
+ end
574
576
 
575
- if value
576
- case
577
- when needs_migration?(value)
578
- Marshal.load(value).tap do |v|
579
- self[name] = { value: v }
580
- end
581
- when rotate
582
- serializer.load(value).tap do |v|
583
- self[name] = { value: v }
584
- end
585
- else
586
- serializer.load(value)
577
+ def parse(name, dumped, force_reserialize: false, **)
578
+ if dumped
579
+ begin
580
+ value = serializer.load(dumped)
581
+ rescue StandardError
582
+ return
587
583
  end
584
+
585
+ self[name] = { value: value } if force_reserialize || reserialize?(dumped)
586
+
587
+ value
588
588
  end
589
589
  end
590
590
 
591
- def serializer
592
- serializer = request.cookies_serializer || :marshal
593
- case serializer
594
- when :marshal
595
- MarshalWithJsonFallback
596
- when :json, :hybrid
597
- JsonSerializer
598
- else
599
- serializer
600
- end
591
+ def commit(name, options)
592
+ options[:value] = serializer.dump(options[:value])
601
593
  end
602
594
 
603
- def digest
604
- request.cookies_digest || "SHA1"
595
+ def check_for_overflow!(name, options)
596
+ if options[:value].bytesize > MAX_COOKIE_SIZE
597
+ raise CookieOverflow, "#{name} cookie overflowed with size #{options[:value].bytesize} bytes"
598
+ end
605
599
  end
606
600
  end
607
601
 
@@ -622,15 +616,15 @@ module ActionDispatch
622
616
 
623
617
  private
624
618
  def parse(name, signed_message, purpose: nil)
625
- deserialize(name) do |rotate|
626
- @verifier.verified(signed_message, on_rotation: rotate, purpose: purpose)
627
- end
619
+ rotated = false
620
+ data = @verifier.verified(signed_message, purpose: purpose, on_rotation: -> { rotated = true })
621
+ super(name, data, force_reserialize: rotated)
628
622
  end
629
623
 
630
624
  def commit(name, options)
631
- options[:value] = @verifier.generate(serialize(options[:value]), **cookie_metadata(name, options))
632
-
633
- raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE
625
+ super
626
+ options[:value] = @verifier.generate(options[:value], **cookie_metadata(name, options))
627
+ check_for_overflow!(name, options)
634
628
  end
635
629
  end
636
630
 
@@ -672,17 +666,17 @@ module ActionDispatch
672
666
 
673
667
  private
674
668
  def parse(name, encrypted_message, purpose: nil)
675
- deserialize(name) do |rotate|
676
- @encryptor.decrypt_and_verify(encrypted_message, on_rotation: rotate, purpose: purpose)
677
- end
669
+ rotated = false
670
+ data = @encryptor.decrypt_and_verify(encrypted_message, purpose: purpose, on_rotation: -> { rotated = true })
671
+ super(name, data, force_reserialize: rotated)
678
672
  rescue ActiveSupport::MessageEncryptor::InvalidMessage, ActiveSupport::MessageVerifier::InvalidSignature
679
673
  nil
680
674
  end
681
675
 
682
676
  def commit(name, options)
683
- options[:value] = @encryptor.encrypt_and_sign(serialize(options[:value]), **cookie_metadata(name, options))
684
-
685
- raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE
677
+ super
678
+ options[:value] = @encryptor.encrypt_and_sign(options[:value], **cookie_metadata(name, options))
679
+ check_for_overflow!(name, options)
686
680
  end
687
681
  end
688
682
 
@@ -691,21 +685,18 @@ module ActionDispatch
691
685
  end
692
686
 
693
687
  def call(env)
694
- request = ActionDispatch::Request.new env
695
-
696
- status, headers, body = @app.call(env)
688
+ request = ActionDispatch::Request.new(env)
689
+ response = @app.call(env)
697
690
 
698
691
  if request.have_cookie_jar?
699
692
  cookie_jar = request.cookie_jar
700
693
  unless cookie_jar.committed?
701
- cookie_jar.write(headers)
702
- if headers[HTTP_HEADER].respond_to?(:join)
703
- headers[HTTP_HEADER] = headers[HTTP_HEADER].join("\n")
704
- end
694
+ response = Rack::Response[*response]
695
+ cookie_jar.write(response)
705
696
  end
706
697
  end
707
698
 
708
- [status, headers, body]
699
+ response.to_a
709
700
  end
710
701
  end
711
702
  end
@@ -6,6 +6,8 @@ require "action_dispatch/routing/inspector"
6
6
  require "action_view"
7
7
 
8
8
  module ActionDispatch
9
+ # = Action Dispatch \DebugExceptions
10
+ #
9
11
  # This middleware is responsible for logging exceptions and
10
12
  # showing a debugging page in case the request is local.
11
13
  class DebugExceptions
@@ -24,26 +26,26 @@ module ActionDispatch
24
26
  end
25
27
 
26
28
  def call(env)
27
- request = ActionDispatch::Request.new env
28
29
  _, headers, body = response = @app.call(env)
29
30
 
30
- if headers["X-Cascade"] == "pass"
31
+ if headers[Constants::X_CASCADE] == "pass"
31
32
  body.close if body.respond_to?(:close)
32
33
  raise ActionController::RoutingError, "No route matches [#{env['REQUEST_METHOD']}] #{env['PATH_INFO'].inspect}"
33
34
  end
34
35
 
35
36
  response
36
37
  rescue Exception => exception
37
- invoke_interceptors(request, exception)
38
- raise exception unless request.show_exceptions?
39
- render_exception(request, exception)
38
+ request = ActionDispatch::Request.new env
39
+ backtrace_cleaner = request.get_header("action_dispatch.backtrace_cleaner")
40
+ wrapper = ExceptionWrapper.new(backtrace_cleaner, exception)
41
+
42
+ invoke_interceptors(request, exception, wrapper)
43
+ raise exception unless wrapper.show?(request)
44
+ render_exception(request, exception, wrapper)
40
45
  end
41
46
 
42
47
  private
43
- def invoke_interceptors(request, exception)
44
- backtrace_cleaner = request.get_header("action_dispatch.backtrace_cleaner")
45
- wrapper = ExceptionWrapper.new(backtrace_cleaner, exception)
46
-
48
+ def invoke_interceptors(request, exception, wrapper)
47
49
  @interceptors.each do |interceptor|
48
50
  interceptor.call(request, exception)
49
51
  rescue Exception
@@ -51,9 +53,7 @@ module ActionDispatch
51
53
  end
52
54
  end
53
55
 
54
- def render_exception(request, exception)
55
- backtrace_cleaner = request.get_header("action_dispatch.backtrace_cleaner")
56
- wrapper = ExceptionWrapper.new(backtrace_cleaner, exception)
56
+ def render_exception(request, exception, wrapper)
57
57
  log_error(request, wrapper)
58
58
 
59
59
  if request.get_header("action_dispatch.show_detailed_exceptions")
@@ -94,7 +94,7 @@ module ActionDispatch
94
94
  wrapper.status_code,
95
95
  Rack::Utils::HTTP_STATUS_CODES[500]
96
96
  ),
97
- exception: wrapper.exception.inspect,
97
+ exception: wrapper.exception_inspect,
98
98
  traces: wrapper.traces
99
99
  }
100
100
 
@@ -115,19 +115,19 @@ module ActionDispatch
115
115
  DebugView.new(
116
116
  request: request,
117
117
  exception_wrapper: wrapper,
118
+ # Everything should use the wrapper, but we need to pass
119
+ # `exception` for legacy code.
118
120
  exception: wrapper.exception,
119
121
  traces: wrapper.traces,
120
122
  show_source_idx: wrapper.source_to_show_id,
121
123
  trace_to_show: wrapper.trace_to_show,
122
- routes_inspector: routes_inspector(wrapper.exception),
124
+ routes_inspector: routes_inspector(wrapper),
123
125
  source_extracts: wrapper.source_extracts,
124
- line_number: wrapper.line_number,
125
- file: wrapper.file
126
126
  )
127
127
  end
128
128
 
129
129
  def render(status, body, format)
130
- [status, { "Content-Type" => "#{format}; charset=#{Response.default_charset}", "Content-Length" => body.bytesize.to_s }, [body]]
130
+ [status, { Rack::CONTENT_TYPE => "#{format}; charset=#{Response.default_charset}", Rack::CONTENT_LENGTH => body.bytesize.to_s }, [body]]
131
131
  end
132
132
 
133
133
  def log_error(request, wrapper)
@@ -136,26 +136,27 @@ module ActionDispatch
136
136
  return unless logger
137
137
  return if !log_rescued_responses?(request) && wrapper.rescue_response?
138
138
 
139
- exception = wrapper.exception
140
139
  trace = wrapper.exception_trace
141
140
 
142
141
  message = []
143
142
  message << " "
144
- message << "#{exception.class} (#{exception.message}):"
145
- message.concat(exception.annotated_source_code) if exception.respond_to?(:annotated_source_code)
143
+ message << "#{wrapper.exception_class_name} (#{wrapper.message}):"
144
+ message.concat(wrapper.annotated_source_code)
146
145
  message << " "
147
146
  message.concat(trace)
148
147
 
149
- log_array(logger, message)
148
+ log_array(logger, message, request)
150
149
  end
151
150
 
152
- def log_array(logger, lines)
151
+ def log_array(logger, lines, request)
153
152
  return if lines.empty?
154
153
 
154
+ level = request.get_header("action_dispatch.debug_exception_log_level")
155
+
155
156
  if logger.formatter && logger.formatter.respond_to?(:tags_text)
156
- logger.fatal lines.join("\n#{logger.formatter.tags_text}")
157
+ logger.add(level, lines.join("\n#{logger.formatter.tags_text}"))
157
158
  else
158
- logger.fatal lines.join("\n")
159
+ logger.add(level, lines.join("\n"))
159
160
  end
160
161
  end
161
162
 
@@ -168,7 +169,7 @@ module ActionDispatch
168
169
  end
169
170
 
170
171
  def routes_inspector(exception)
171
- if @routes_app.respond_to?(:routes) && (exception.is_a?(ActionController::RoutingError) || exception.is_a?(ActionView::Template::Error))
172
+ if @routes_app.respond_to?(:routes) && (exception.routing_error? || exception.template_error?)
172
173
  ActionDispatch::Routing::RoutesInspector.new(@routes_app.routes.routes)
173
174
  end
174
175
  end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActionDispatch
4
+ # = Action Dispatch \DebugLocks
5
+ #
4
6
  # This middleware can be used to diagnose deadlocks in the autoload interlock.
5
7
  #
6
8
  # To use it, insert it near the top of the middleware stack, using
@@ -97,7 +99,8 @@ module ActionDispatch
97
99
  msg << "\n#{info[:backtrace].join("\n")}\n" if info[:backtrace]
98
100
  end.join("\n\n---\n\n\n")
99
101
 
100
- [200, { "Content-Type" => "text/plain", "Content-Length" => str.size }, [str]]
102
+ [200, { Rack::CONTENT_TYPE => "text/plain; charset=#{ActionDispatch::Response.default_charset}",
103
+ Rack::CONTENT_LENGTH => str.size.to_s }, [str]]
101
104
  end
102
105
 
103
106
  def blocked_by?(victim, blocker, all_threads)
@@ -7,18 +7,23 @@ require "action_view/base"
7
7
 
8
8
  module ActionDispatch
9
9
  class DebugView < ActionView::Base # :nodoc:
10
- RESCUES_TEMPLATE_PATH = File.expand_path("templates", __dir__)
10
+ RESCUES_TEMPLATE_PATHS = [File.expand_path("templates", __dir__)]
11
11
 
12
12
  def initialize(assigns)
13
- paths = [RESCUES_TEMPLATE_PATH]
13
+ paths = RESCUES_TEMPLATE_PATHS.dup
14
14
  lookup_context = ActionView::LookupContext.new(paths)
15
15
  super(lookup_context, assigns, nil)
16
+ @exception_wrapper = assigns[:exception_wrapper]
16
17
  end
17
18
 
18
19
  def compiled_method_container
19
20
  self.class
20
21
  end
21
22
 
23
+ def error_highlight_available?
24
+ @exception_wrapper.error_highlight_available?
25
+ end
26
+
22
27
  def debug_params(params)
23
28
  clean_params = params.clone
24
29
  clean_params.delete("action")