actionpack 5.2.8.1 → 6.0.6

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 (136) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +270 -347
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +4 -3
  5. data/lib/abstract_controller/base.rb +4 -3
  6. data/lib/abstract_controller/caching/fragments.rb +6 -22
  7. data/lib/abstract_controller/caching.rb +1 -1
  8. data/lib/abstract_controller/callbacks.rb +12 -0
  9. data/lib/abstract_controller/collector.rb +1 -2
  10. data/lib/abstract_controller/helpers.rb +7 -6
  11. data/lib/abstract_controller/railties/routes_helpers.rb +1 -1
  12. data/lib/abstract_controller/translation.rb +4 -4
  13. data/lib/action_controller/api.rb +2 -1
  14. data/lib/action_controller/base.rb +2 -7
  15. data/lib/action_controller/caching.rb +1 -2
  16. data/lib/action_controller/log_subscriber.rb +8 -5
  17. data/lib/action_controller/metal/basic_implicit_render.rb +1 -1
  18. data/lib/action_controller/metal/conditional_get.rb +9 -3
  19. data/lib/action_controller/metal/content_security_policy.rb +0 -1
  20. data/lib/action_controller/metal/data_streaming.rb +5 -6
  21. data/lib/action_controller/metal/default_headers.rb +17 -0
  22. data/lib/action_controller/metal/etag_with_template_digest.rb +1 -1
  23. data/lib/action_controller/metal/exceptions.rb +23 -2
  24. data/lib/action_controller/metal/flash.rb +5 -5
  25. data/lib/action_controller/metal/force_ssl.rb +15 -56
  26. data/lib/action_controller/metal/head.rb +1 -1
  27. data/lib/action_controller/metal/helpers.rb +3 -4
  28. data/lib/action_controller/metal/http_authentication.rb +20 -21
  29. data/lib/action_controller/metal/implicit_render.rb +4 -14
  30. data/lib/action_controller/metal/instrumentation.rb +3 -6
  31. data/lib/action_controller/metal/live.rb +29 -31
  32. data/lib/action_controller/metal/mime_responds.rb +13 -2
  33. data/lib/action_controller/metal/params_wrapper.rb +18 -14
  34. data/lib/action_controller/metal/redirecting.rb +5 -5
  35. data/lib/action_controller/metal/renderers.rb +4 -4
  36. data/lib/action_controller/metal/rendering.rb +2 -3
  37. data/lib/action_controller/metal/request_forgery_protection.rb +25 -48
  38. data/lib/action_controller/metal/streaming.rb +0 -1
  39. data/lib/action_controller/metal/strong_parameters.rb +65 -44
  40. data/lib/action_controller/metal/url_for.rb +1 -1
  41. data/lib/action_controller/metal.rb +8 -6
  42. data/lib/action_controller/railties/helpers.rb +1 -1
  43. data/lib/action_controller/renderer.rb +17 -3
  44. data/lib/action_controller/template_assertions.rb +1 -1
  45. data/lib/action_controller/test_case.rb +7 -8
  46. data/lib/action_controller.rb +5 -1
  47. data/lib/action_dispatch/http/cache.rb +14 -11
  48. data/lib/action_dispatch/http/content_disposition.rb +45 -0
  49. data/lib/action_dispatch/http/content_security_policy.rb +28 -17
  50. data/lib/action_dispatch/http/filter_parameters.rb +8 -7
  51. data/lib/action_dispatch/http/filter_redirect.rb +1 -2
  52. data/lib/action_dispatch/http/headers.rb +1 -2
  53. data/lib/action_dispatch/http/mime_negotiation.rb +13 -6
  54. data/lib/action_dispatch/http/mime_type.rb +14 -8
  55. data/lib/action_dispatch/http/parameter_filter.rb +5 -79
  56. data/lib/action_dispatch/http/parameters.rb +15 -6
  57. data/lib/action_dispatch/http/request.rb +21 -14
  58. data/lib/action_dispatch/http/response.rb +40 -21
  59. data/lib/action_dispatch/http/upload.rb +9 -1
  60. data/lib/action_dispatch/http/url.rb +81 -82
  61. data/lib/action_dispatch/journey/formatter.rb +2 -3
  62. data/lib/action_dispatch/journey/gtg/builder.rb +0 -1
  63. data/lib/action_dispatch/journey/gtg/transition_table.rb +0 -1
  64. data/lib/action_dispatch/journey/nfa/simulator.rb +0 -2
  65. data/lib/action_dispatch/journey/nfa/transition_table.rb +0 -1
  66. data/lib/action_dispatch/journey/nodes/node.rb +9 -8
  67. data/lib/action_dispatch/journey/path/pattern.rb +6 -3
  68. data/lib/action_dispatch/journey/route.rb +5 -4
  69. data/lib/action_dispatch/journey/router/utils.rb +10 -10
  70. data/lib/action_dispatch/journey/router.rb +0 -4
  71. data/lib/action_dispatch/journey/routes.rb +0 -2
  72. data/lib/action_dispatch/journey/scanner.rb +10 -4
  73. data/lib/action_dispatch/journey/visitors.rb +1 -4
  74. data/lib/action_dispatch/middleware/actionable_exceptions.rb +46 -0
  75. data/lib/action_dispatch/middleware/callbacks.rb +2 -4
  76. data/lib/action_dispatch/middleware/cookies.rb +62 -78
  77. data/lib/action_dispatch/middleware/debug_exceptions.rb +45 -61
  78. data/lib/action_dispatch/middleware/debug_locks.rb +5 -5
  79. data/lib/action_dispatch/middleware/debug_view.rb +66 -0
  80. data/lib/action_dispatch/middleware/exception_wrapper.rb +49 -16
  81. data/lib/action_dispatch/middleware/flash.rb +1 -1
  82. data/lib/action_dispatch/middleware/host_authorization.rb +121 -0
  83. data/lib/action_dispatch/middleware/public_exceptions.rb +6 -3
  84. data/lib/action_dispatch/middleware/remote_ip.rb +9 -12
  85. data/lib/action_dispatch/middleware/request_id.rb +2 -2
  86. data/lib/action_dispatch/middleware/session/abstract_store.rb +0 -1
  87. data/lib/action_dispatch/middleware/session/cookie_store.rb +1 -7
  88. data/lib/action_dispatch/middleware/show_exceptions.rb +1 -2
  89. data/lib/action_dispatch/middleware/ssl.rb +8 -8
  90. data/lib/action_dispatch/middleware/stack.rb +38 -2
  91. data/lib/action_dispatch/middleware/static.rb +6 -7
  92. data/lib/action_dispatch/middleware/templates/rescues/_actions.html.erb +13 -0
  93. data/lib/action_dispatch/middleware/templates/rescues/_actions.text.erb +0 -0
  94. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +3 -1
  95. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb +1 -1
  96. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +4 -2
  97. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +45 -35
  98. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +7 -0
  99. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb +5 -0
  100. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +26 -4
  101. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +1 -1
  102. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +7 -4
  103. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +5 -2
  104. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +4 -0
  105. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +19 -0
  106. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb +3 -0
  107. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +2 -2
  108. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +1 -1
  109. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +2 -2
  110. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +3 -0
  111. data/lib/action_dispatch/railtie.rb +7 -2
  112. data/lib/action_dispatch/request/session.rb +9 -2
  113. data/lib/action_dispatch/routing/inspector.rb +97 -50
  114. data/lib/action_dispatch/routing/mapper.rb +63 -42
  115. data/lib/action_dispatch/routing/polymorphic_routes.rb +3 -6
  116. data/lib/action_dispatch/routing/route_set.rb +25 -31
  117. data/lib/action_dispatch/routing/url_for.rb +2 -2
  118. data/lib/action_dispatch/routing.rb +21 -20
  119. data/lib/action_dispatch/system_test_case.rb +44 -6
  120. data/lib/action_dispatch/system_testing/browser.rb +38 -7
  121. data/lib/action_dispatch/system_testing/driver.rb +11 -2
  122. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +6 -5
  123. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +7 -6
  124. data/lib/action_dispatch/testing/assertion_response.rb +0 -1
  125. data/lib/action_dispatch/testing/assertions/response.rb +2 -3
  126. data/lib/action_dispatch/testing/assertions/routing.rb +15 -3
  127. data/lib/action_dispatch/testing/assertions.rb +1 -1
  128. data/lib/action_dispatch/testing/integration.rb +33 -12
  129. data/lib/action_dispatch/testing/request_encoder.rb +2 -2
  130. data/lib/action_dispatch/testing/test_process.rb +2 -2
  131. data/lib/action_dispatch/testing/test_response.rb +4 -32
  132. data/lib/action_dispatch.rb +7 -2
  133. data/lib/action_pack/gem_version.rb +4 -4
  134. data/lib/action_pack.rb +1 -1
  135. metadata +29 -15
  136. data/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb +0 -26
@@ -9,7 +9,7 @@ require "rack/utils"
9
9
  module ActionDispatch
10
10
  class Request
11
11
  def cookie_jar
12
- fetch_header("action_dispatch.cookies".freeze) do
12
+ fetch_header("action_dispatch.cookies") do
13
13
  self.cookie_jar = Cookies::CookieJar.build(self, cookies)
14
14
  end
15
15
  end
@@ -22,11 +22,11 @@ module ActionDispatch
22
22
  }
23
23
 
24
24
  def have_cookie_jar?
25
- has_header? "action_dispatch.cookies".freeze
25
+ has_header? "action_dispatch.cookies"
26
26
  end
27
27
 
28
28
  def cookie_jar=(jar)
29
- set_header "action_dispatch.cookies".freeze, jar
29
+ set_header "action_dispatch.cookies", jar
30
30
  end
31
31
 
32
32
  def key_generator
@@ -61,10 +61,6 @@ module ActionDispatch
61
61
  get_header Cookies::SIGNED_COOKIE_DIGEST
62
62
  end
63
63
 
64
- def secret_token
65
- get_header Cookies::SECRET_TOKEN
66
- end
67
-
68
64
  def secret_key_base
69
65
  get_header Cookies::SECRET_KEY_BASE
70
66
  end
@@ -81,6 +77,10 @@ module ActionDispatch
81
77
  get_header Cookies::COOKIES_ROTATIONS
82
78
  end
83
79
 
80
+ def use_cookies_with_metadata
81
+ get_header Cookies::USE_COOKIES_WITH_METADATA
82
+ end
83
+
84
84
  # :startdoc:
85
85
  end
86
86
 
@@ -168,20 +168,20 @@ module ActionDispatch
168
168
  # * <tt>:httponly</tt> - Whether this cookie is accessible via scripting or
169
169
  # only HTTP. Defaults to +false+.
170
170
  class Cookies
171
- HTTP_HEADER = "Set-Cookie".freeze
172
- GENERATOR_KEY = "action_dispatch.key_generator".freeze
173
- SIGNED_COOKIE_SALT = "action_dispatch.signed_cookie_salt".freeze
174
- ENCRYPTED_COOKIE_SALT = "action_dispatch.encrypted_cookie_salt".freeze
175
- ENCRYPTED_SIGNED_COOKIE_SALT = "action_dispatch.encrypted_signed_cookie_salt".freeze
176
- AUTHENTICATED_ENCRYPTED_COOKIE_SALT = "action_dispatch.authenticated_encrypted_cookie_salt".freeze
177
- USE_AUTHENTICATED_COOKIE_ENCRYPTION = "action_dispatch.use_authenticated_cookie_encryption".freeze
178
- ENCRYPTED_COOKIE_CIPHER = "action_dispatch.encrypted_cookie_cipher".freeze
179
- SIGNED_COOKIE_DIGEST = "action_dispatch.signed_cookie_digest".freeze
180
- SECRET_TOKEN = "action_dispatch.secret_token".freeze
181
- SECRET_KEY_BASE = "action_dispatch.secret_key_base".freeze
182
- COOKIES_SERIALIZER = "action_dispatch.cookies_serializer".freeze
183
- COOKIES_DIGEST = "action_dispatch.cookies_digest".freeze
184
- COOKIES_ROTATIONS = "action_dispatch.cookies_rotations".freeze
171
+ HTTP_HEADER = "Set-Cookie"
172
+ GENERATOR_KEY = "action_dispatch.key_generator"
173
+ SIGNED_COOKIE_SALT = "action_dispatch.signed_cookie_salt"
174
+ ENCRYPTED_COOKIE_SALT = "action_dispatch.encrypted_cookie_salt"
175
+ ENCRYPTED_SIGNED_COOKIE_SALT = "action_dispatch.encrypted_signed_cookie_salt"
176
+ AUTHENTICATED_ENCRYPTED_COOKIE_SALT = "action_dispatch.authenticated_encrypted_cookie_salt"
177
+ USE_AUTHENTICATED_COOKIE_ENCRYPTION = "action_dispatch.use_authenticated_cookie_encryption"
178
+ ENCRYPTED_COOKIE_CIPHER = "action_dispatch.encrypted_cookie_cipher"
179
+ SIGNED_COOKIE_DIGEST = "action_dispatch.signed_cookie_digest"
180
+ SECRET_KEY_BASE = "action_dispatch.secret_key_base"
181
+ COOKIES_SERIALIZER = "action_dispatch.cookies_serializer"
182
+ COOKIES_DIGEST = "action_dispatch.cookies_digest"
183
+ COOKIES_ROTATIONS = "action_dispatch.cookies_rotations"
184
+ USE_COOKIES_WITH_METADATA = "action_dispatch.use_cookies_with_metadata"
185
185
 
186
186
  # Cookies can typically store 4096 bytes.
187
187
  MAX_COOKIE_SIZE = 4096
@@ -210,9 +210,6 @@ module ActionDispatch
210
210
  # the cookie again. This is useful for creating cookies with values that the user is not supposed to change. If a signed
211
211
  # cookie was tampered with by the user (or a 3rd party), +nil+ will be returned.
212
212
  #
213
- # If +secret_key_base+ and +secrets.secret_token+ (deprecated) are both set,
214
- # legacy cookies signed with the old key generator will be transparently upgraded.
215
- #
216
213
  # This jar requires that you set a suitable secret for the verification on your app's +secret_key_base+.
217
214
  #
218
215
  # Example:
@@ -228,9 +225,6 @@ module ActionDispatch
228
225
  # Returns a jar that'll automatically encrypt cookie values before sending them to the client and will decrypt them for read.
229
226
  # If the cookie was tampered with by the user (or a 3rd party), +nil+ will be returned.
230
227
  #
231
- # If +secret_key_base+ and +secrets.secret_token+ (deprecated) are both set,
232
- # legacy cookies signed with the old key generator will be transparently upgraded.
233
- #
234
228
  # If +config.action_dispatch.encrypted_cookie_salt+ and +config.action_dispatch.encrypted_signed_cookie_salt+
235
229
  # are both set, legacy cookies encrypted with HMAC AES-256-CBC will be transparently upgraded.
236
230
  #
@@ -258,11 +252,6 @@ module ActionDispatch
258
252
  end
259
253
 
260
254
  private
261
-
262
- def upgrade_legacy_signed_cookies?
263
- request.secret_token.present? && request.secret_key_base.present?
264
- end
265
-
266
255
  def upgrade_legacy_hmac_aes_cbc_cookies?
267
256
  request.secret_key_base.present? &&
268
257
  request.encrypted_signed_cookie_salt.present? &&
@@ -348,7 +337,7 @@ module ActionDispatch
348
337
 
349
338
  def update_cookies_from_jar
350
339
  request_jar = @request.cookie_jar.instance_variable_get(:@cookies)
351
- set_cookies = request_jar.reject { |k, _| @delete_cookies.key?(k) }
340
+ set_cookies = request_jar.reject { |k, _| @delete_cookies.key?(k) || @set_cookies.key?(k) }
352
341
 
353
342
  @cookies.update set_cookies if set_cookies
354
343
  end
@@ -438,7 +427,6 @@ module ActionDispatch
438
427
  mattr_accessor :always_write_cookie, default: false
439
428
 
440
429
  private
441
-
442
430
  def escape(string)
443
431
  ::Rack::Utils.escape(string)
444
432
  end
@@ -470,7 +458,13 @@ module ActionDispatch
470
458
 
471
459
  def [](name)
472
460
  if data = @parent_jar[name.to_s]
473
- parse name, data
461
+ result = parse(name, data, purpose: "cookie.#{name}")
462
+
463
+ if result.nil?
464
+ parse(name, data)
465
+ else
466
+ result
467
+ end
474
468
  end
475
469
  end
476
470
 
@@ -481,7 +475,7 @@ module ActionDispatch
481
475
  options = { value: options }
482
476
  end
483
477
 
484
- commit(options)
478
+ commit(name, options)
485
479
  @parent_jar[name] = options
486
480
  end
487
481
 
@@ -490,24 +484,26 @@ module ActionDispatch
490
484
 
491
485
  private
492
486
  def expiry_options(options)
493
- if request.use_authenticated_cookie_encryption
494
- if options[:expires].respond_to?(:from_now)
495
- { expires_in: options[:expires] }
496
- else
497
- { expires_at: options[:expires] }
498
- end
487
+ if options[:expires].respond_to?(:from_now)
488
+ { expires_in: options[:expires] }
499
489
  else
500
- {}
490
+ { expires_at: options[:expires] }
491
+ end
492
+ end
493
+
494
+ def cookie_metadata(name, options)
495
+ expiry_options(options).tap do |metadata|
496
+ metadata[:purpose] = "cookie.#{name}" if request.use_cookies_with_metadata
501
497
  end
502
498
  end
503
499
 
504
- def parse(name, data); data; end
505
- def commit(options); end
500
+ def parse(name, data, purpose: nil); data; end
501
+ def commit(name, options); end
506
502
  end
507
503
 
508
504
  class PermanentCookieJar < AbstractCookieJar # :nodoc:
509
505
  private
510
- def commit(options)
506
+ def commit(name, options)
511
507
  options[:expires] = 20.years.from_now
512
508
  end
513
509
  end
@@ -523,7 +519,7 @@ module ActionDispatch
523
519
  end
524
520
 
525
521
  module SerializedCookieJars # :nodoc:
526
- MARSHAL_SIGNATURE = "\x04\x08".freeze
522
+ MARSHAL_SIGNATURE = "\x04\x08"
527
523
  SERIALIZER = ActiveSupport::MessageEncryptor::NullSerializer
528
524
 
529
525
  protected
@@ -542,9 +538,13 @@ module ActionDispatch
542
538
  if value
543
539
  case
544
540
  when needs_migration?(value)
545
- self[name] = Marshal.load(value)
541
+ Marshal.load(value).tap do |v|
542
+ self[name] = { value: v }
543
+ end
546
544
  when rotate
547
- self[name] = serializer.load(value)
545
+ serializer.load(value).tap do |v|
546
+ self[name] = { value: v }
547
+ end
548
548
  else
549
549
  serializer.load(value)
550
550
  end
@@ -577,24 +577,21 @@ module ActionDispatch
577
577
  secret = request.key_generator.generate_key(request.signed_cookie_salt)
578
578
  @verifier = ActiveSupport::MessageVerifier.new(secret, digest: signed_cookie_digest, serializer: SERIALIZER)
579
579
 
580
- request.cookies_rotations.signed.each do |*secrets, **options|
580
+ request.cookies_rotations.signed.each do |(*secrets)|
581
+ options = secrets.extract_options!
581
582
  @verifier.rotate(*secrets, serializer: SERIALIZER, **options)
582
583
  end
583
-
584
- if upgrade_legacy_signed_cookies?
585
- @verifier.rotate request.secret_token, serializer: SERIALIZER
586
- end
587
584
  end
588
585
 
589
586
  private
590
- def parse(name, signed_message)
587
+ def parse(name, signed_message, purpose: nil)
591
588
  deserialize(name) do |rotate|
592
- @verifier.verified(signed_message, on_rotation: rotate)
589
+ @verifier.verified(signed_message, on_rotation: rotate, purpose: purpose)
593
590
  end
594
591
  end
595
592
 
596
- def commit(options)
597
- options[:value] = @verifier.generate(serialize(options[:value]), expiry_options(options))
593
+ def commit(name, options)
594
+ options[:value] = @verifier.generate(serialize(options[:value]), **cookie_metadata(name, options))
598
595
 
599
596
  raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE
600
597
  end
@@ -617,7 +614,8 @@ module ActionDispatch
617
614
  @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, cipher: "aes-256-cbc", serializer: SERIALIZER)
618
615
  end
619
616
 
620
- request.cookies_rotations.encrypted.each do |*secrets, **options|
617
+ request.cookies_rotations.encrypted.each do |(*secrets)|
618
+ options = secrets.extract_options!
621
619
  @encryptor.rotate(*secrets, serializer: SERIALIZER, **options)
622
620
  end
623
621
 
@@ -628,36 +626,22 @@ module ActionDispatch
628
626
 
629
627
  @encryptor.rotate(secret, sign_secret, cipher: legacy_cipher, digest: digest, serializer: SERIALIZER)
630
628
  end
631
-
632
- if upgrade_legacy_signed_cookies?
633
- @legacy_verifier = ActiveSupport::MessageVerifier.new(request.secret_token, digest: digest, serializer: SERIALIZER)
634
- end
635
629
  end
636
630
 
637
631
  private
638
- def parse(name, encrypted_message)
632
+ def parse(name, encrypted_message, purpose: nil)
639
633
  deserialize(name) do |rotate|
640
- @encryptor.decrypt_and_verify(encrypted_message, on_rotation: rotate)
634
+ @encryptor.decrypt_and_verify(encrypted_message, on_rotation: rotate, purpose: purpose)
641
635
  end
642
636
  rescue ActiveSupport::MessageEncryptor::InvalidMessage, ActiveSupport::MessageVerifier::InvalidSignature
643
- parse_legacy_signed_message(name, encrypted_message)
637
+ nil
644
638
  end
645
639
 
646
- def commit(options)
647
- options[:value] = @encryptor.encrypt_and_sign(serialize(options[:value]), expiry_options(options))
640
+ def commit(name, options)
641
+ options[:value] = @encryptor.encrypt_and_sign(serialize(options[:value]), **cookie_metadata(name, options))
648
642
 
649
643
  raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE
650
644
  end
651
-
652
- def parse_legacy_signed_message(name, legacy_signed_message)
653
- if defined?(@legacy_verifier)
654
- deserialize(name) do |rotate|
655
- rotate.call
656
-
657
- @legacy_verifier.verified(legacy_signed_message)
658
- end
659
- end
660
- end
661
645
  end
662
646
 
663
647
  def initialize(app)
@@ -3,57 +3,28 @@
3
3
  require "action_dispatch/http/request"
4
4
  require "action_dispatch/middleware/exception_wrapper"
5
5
  require "action_dispatch/routing/inspector"
6
+
7
+ require "active_support/actionable_error"
8
+
6
9
  require "action_view"
7
10
  require "action_view/base"
8
11
 
9
- require "pp"
10
-
11
12
  module ActionDispatch
12
13
  # This middleware is responsible for logging exceptions and
13
14
  # showing a debugging page in case the request is local.
14
15
  class DebugExceptions
15
- RESCUES_TEMPLATE_PATH = File.expand_path("templates", __dir__)
16
-
17
- class DebugView < ActionView::Base
18
- def debug_params(params)
19
- clean_params = params.clone
20
- clean_params.delete("action")
21
- clean_params.delete("controller")
22
-
23
- if clean_params.empty?
24
- "None"
25
- else
26
- PP.pp(clean_params, "".dup, 200)
27
- end
28
- end
16
+ cattr_reader :interceptors, instance_accessor: false, default: []
29
17
 
30
- def debug_headers(headers)
31
- if headers.present?
32
- headers.inspect.gsub(",", ",\n")
33
- else
34
- "None"
35
- end
36
- end
37
-
38
- def debug_hash(object)
39
- object.to_hash.sort_by { |k, _| k.to_s }.map { |k, v| "#{k}: #{v.inspect rescue $!.message}" }.join("\n")
40
- end
41
-
42
- def render(*)
43
- logger = ActionView::Base.logger
44
-
45
- if logger && logger.respond_to?(:silence)
46
- logger.silence { super }
47
- else
48
- super
49
- end
50
- end
18
+ def self.register_interceptor(object = nil, &block)
19
+ interceptor = object || block
20
+ interceptors << interceptor
51
21
  end
52
22
 
53
- def initialize(app, routes_app = nil, response_format = :default)
23
+ def initialize(app, routes_app = nil, response_format = :default, interceptors = self.class.interceptors)
54
24
  @app = app
55
25
  @routes_app = routes_app
56
26
  @response_format = response_format
27
+ @interceptors = interceptors
57
28
  end
58
29
 
59
30
  def call(env)
@@ -67,11 +38,22 @@ module ActionDispatch
67
38
 
68
39
  response
69
40
  rescue Exception => exception
41
+ invoke_interceptors(request, exception)
70
42
  raise exception unless request.show_exceptions?
71
43
  render_exception(request, exception)
72
44
  end
73
45
 
74
46
  private
47
+ def invoke_interceptors(request, exception)
48
+ backtrace_cleaner = request.get_header("action_dispatch.backtrace_cleaner")
49
+ wrapper = ExceptionWrapper.new(backtrace_cleaner, exception)
50
+
51
+ @interceptors.each do |interceptor|
52
+ interceptor.call(request, exception)
53
+ rescue Exception
54
+ log_error(request, wrapper)
55
+ end
56
+ end
75
57
 
76
58
  def render_exception(request, exception)
77
59
  backtrace_cleaner = request.get_header("action_dispatch.backtrace_cleaner")
@@ -79,7 +61,11 @@ module ActionDispatch
79
61
  log_error(request, wrapper)
80
62
 
81
63
  if request.get_header("action_dispatch.show_detailed_exceptions")
82
- content_type = request.formats.first
64
+ begin
65
+ content_type = request.formats.first
66
+ rescue ActionDispatch::Http::MimeNegotiation::InvalidType
67
+ content_type = Mime[:text]
68
+ end
83
69
 
84
70
  if api_request?(content_type)
85
71
  render_for_api_request(content_type, wrapper)
@@ -130,23 +116,13 @@ module ActionDispatch
130
116
  end
131
117
 
132
118
  def create_template(request, wrapper)
133
- traces = wrapper.traces
134
-
135
- trace_to_show = "Application Trace"
136
- if traces[trace_to_show].empty? && wrapper.rescue_template != "routing_error"
137
- trace_to_show = "Full Trace"
138
- end
139
-
140
- if source_to_show = traces[trace_to_show].first
141
- source_to_show_id = source_to_show[:id]
142
- end
143
-
144
- DebugView.new([RESCUES_TEMPLATE_PATH],
119
+ DebugView.new(
145
120
  request: request,
121
+ exception_wrapper: wrapper,
146
122
  exception: wrapper.exception,
147
- traces: traces,
148
- show_source_idx: source_to_show_id,
149
- trace_to_show: trace_to_show,
123
+ traces: wrapper.traces,
124
+ show_source_idx: wrapper.source_to_show_id,
125
+ trace_to_show: wrapper.trace_to_show,
150
126
  routes_inspector: routes_inspector(wrapper.exception),
151
127
  source_extracts: wrapper.source_extracts,
152
128
  line_number: wrapper.line_number,
@@ -160,6 +136,7 @@ module ActionDispatch
160
136
 
161
137
  def log_error(request, wrapper)
162
138
  logger = logger(request)
139
+
163
140
  return unless logger
164
141
 
165
142
  exception = wrapper.exception
@@ -168,19 +145,26 @@ module ActionDispatch
168
145
  trace = wrapper.framework_trace if trace.empty?
169
146
 
170
147
  ActiveSupport::Deprecation.silence do
171
- logger.fatal " "
172
- logger.fatal "#{exception.class} (#{exception.message}):"
173
- log_array logger, exception.annoted_source_code if exception.respond_to?(:annoted_source_code)
174
- logger.fatal " "
175
- log_array logger, trace
148
+ message = []
149
+ message << " "
150
+ message << "#{exception.class} (#{exception.message}):"
151
+ message.concat(exception.annotated_source_code) if exception.respond_to?(:annotated_source_code)
152
+ message << " "
153
+ message.concat(trace)
154
+
155
+ log_array(logger, message)
176
156
  end
177
157
  end
178
158
 
179
159
  def log_array(logger, array)
160
+ lines = Array(array)
161
+
162
+ return if lines.empty?
163
+
180
164
  if logger.formatter && logger.formatter.respond_to?(:tags_text)
181
- logger.fatal array.join("\n#{logger.formatter.tags_text}")
165
+ logger.fatal lines.join("\n#{logger.formatter.tags_text}")
182
166
  else
183
- logger.fatal array.join("\n")
167
+ logger.fatal lines.join("\n")
184
168
  end
185
169
  end
186
170
 
@@ -32,7 +32,7 @@ module ActionDispatch
32
32
  req = ActionDispatch::Request.new env
33
33
 
34
34
  if req.get?
35
- path = req.path_info.chomp("/".freeze)
35
+ path = req.path_info.chomp("/")
36
36
  if path == @path
37
37
  return render_details(req)
38
38
  end
@@ -63,19 +63,19 @@ module ActionDispatch
63
63
 
64
64
  str = threads.map do |thread, info|
65
65
  if info[:exclusive]
66
- lock_state = "Exclusive".dup
66
+ lock_state = +"Exclusive"
67
67
  elsif info[:sharing] > 0
68
- lock_state = "Sharing".dup
68
+ lock_state = +"Sharing"
69
69
  lock_state << " x#{info[:sharing]}" if info[:sharing] > 1
70
70
  else
71
- lock_state = "No lock".dup
71
+ lock_state = +"No lock"
72
72
  end
73
73
 
74
74
  if info[:waiting]
75
75
  lock_state << " (yielded share)"
76
76
  end
77
77
 
78
- msg = "Thread #{info[:index]} [0x#{thread.__id__.to_s(16)} #{thread.status || 'dead'}] #{lock_state}\n".dup
78
+ msg = +"Thread #{info[:index]} [0x#{thread.__id__.to_s(16)} #{thread.status || 'dead'}] #{lock_state}\n"
79
79
 
80
80
  if info[:sleeper]
81
81
  msg << " Waiting in #{info[:sleeper]}"
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pp"
4
+
5
+ require "action_view"
6
+ require "action_view/base"
7
+
8
+ module ActionDispatch
9
+ class DebugView < ActionView::Base # :nodoc:
10
+ RESCUES_TEMPLATE_PATH = File.expand_path("templates", __dir__)
11
+
12
+ def initialize(assigns)
13
+ paths = [RESCUES_TEMPLATE_PATH]
14
+ lookup_context = ActionView::LookupContext.new(paths)
15
+ super(lookup_context, assigns)
16
+ end
17
+
18
+ def compiled_method_container
19
+ self.class
20
+ end
21
+
22
+ def debug_params(params)
23
+ clean_params = params.clone
24
+ clean_params.delete("action")
25
+ clean_params.delete("controller")
26
+
27
+ if clean_params.empty?
28
+ "None"
29
+ else
30
+ PP.pp(clean_params, +"", 200)
31
+ end
32
+ end
33
+
34
+ def debug_headers(headers)
35
+ if headers.present?
36
+ headers.inspect.gsub(",", ",\n")
37
+ else
38
+ "None"
39
+ end
40
+ end
41
+
42
+ def debug_hash(object)
43
+ object.to_hash.sort_by { |k, _| k.to_s }.map { |k, v| "#{k}: #{v.inspect rescue $!.message}" }.join("\n")
44
+ end
45
+
46
+ def render(*)
47
+ logger = ActionView::Base.logger
48
+
49
+ if logger && logger.respond_to?(:silence)
50
+ logger.silence { super }
51
+ else
52
+ super
53
+ end
54
+ end
55
+
56
+ def protect_against_forgery?
57
+ false
58
+ end
59
+
60
+ def params_valid?
61
+ @request.parameters
62
+ rescue ActionController::BadRequest
63
+ false
64
+ end
65
+ end
66
+ end
@@ -12,6 +12,8 @@ module ActionDispatch
12
12
  "ActionController::UnknownHttpMethod" => :method_not_allowed,
13
13
  "ActionController::NotImplemented" => :not_implemented,
14
14
  "ActionController::UnknownFormat" => :not_acceptable,
15
+ "ActionDispatch::Http::MimeNegotiation::InvalidType" => :not_acceptable,
16
+ "ActionController::MissingExactTemplate" => :not_acceptable,
15
17
  "ActionController::InvalidAuthenticityToken" => :unprocessable_entity,
16
18
  "ActionController::InvalidCrossOriginRequest" => :unprocessable_entity,
17
19
  "ActionDispatch::Http::Parameters::ParseError" => :bad_request,
@@ -22,28 +24,42 @@ module ActionDispatch
22
24
  )
23
25
 
24
26
  cattr_accessor :rescue_templates, default: Hash.new("diagnostics").merge!(
25
- "ActionView::MissingTemplate" => "missing_template",
26
- "ActionController::RoutingError" => "routing_error",
27
- "AbstractController::ActionNotFound" => "unknown_action",
28
- "ActiveRecord::StatementInvalid" => "invalid_statement",
29
- "ActionView::Template::Error" => "template_error"
27
+ "ActionView::MissingTemplate" => "missing_template",
28
+ "ActionController::RoutingError" => "routing_error",
29
+ "AbstractController::ActionNotFound" => "unknown_action",
30
+ "ActiveRecord::StatementInvalid" => "invalid_statement",
31
+ "ActionView::Template::Error" => "template_error",
32
+ "ActionController::MissingExactTemplate" => "missing_exact_template",
30
33
  )
31
34
 
32
- attr_reader :backtrace_cleaner, :exception, :line_number, :file
35
+ cattr_accessor :wrapper_exceptions, default: [
36
+ "ActionView::Template::Error"
37
+ ]
38
+
39
+ attr_reader :backtrace_cleaner, :exception, :wrapped_causes, :line_number, :file
33
40
 
34
41
  def initialize(backtrace_cleaner, exception)
35
42
  @backtrace_cleaner = backtrace_cleaner
36
- @exception = original_exception(exception)
43
+ @exception = exception
44
+ @wrapped_causes = wrapped_causes_for(exception, backtrace_cleaner)
37
45
 
38
46
  expand_backtrace if exception.is_a?(SyntaxError) || exception.cause.is_a?(SyntaxError)
39
47
  end
40
48
 
49
+ def unwrapped_exception
50
+ if wrapper_exceptions.include?(exception.class.to_s)
51
+ exception.cause
52
+ else
53
+ exception
54
+ end
55
+ end
56
+
41
57
  def rescue_template
42
58
  @@rescue_templates[@exception.class.name]
43
59
  end
44
60
 
45
61
  def status_code
46
- self.class.status_code_for_exception(@exception.class.name)
62
+ self.class.status_code_for_exception(unwrapped_exception.class.name)
47
63
  end
48
64
 
49
65
  def application_trace
@@ -64,7 +80,11 @@ module ActionDispatch
64
80
  full_trace_with_ids = []
65
81
 
66
82
  full_trace.each_with_index do |trace, idx|
67
- trace_with_id = { id: idx, trace: trace }
83
+ trace_with_id = {
84
+ exception_object_id: @exception.object_id,
85
+ id: idx,
86
+ trace: trace
87
+ }
68
88
 
69
89
  if application_trace.include?(trace)
70
90
  application_trace_with_ids << trace_with_id
@@ -97,18 +117,31 @@ module ActionDispatch
97
117
  end
98
118
  end
99
119
 
100
- private
120
+ def trace_to_show
121
+ if traces["Application Trace"].empty? && rescue_template != "routing_error"
122
+ "Full Trace"
123
+ else
124
+ "Application Trace"
125
+ end
126
+ end
101
127
 
128
+ def source_to_show_id
129
+ (traces[trace_to_show].first || {})[:id]
130
+ end
131
+
132
+ private
102
133
  def backtrace
103
134
  Array(@exception.backtrace)
104
135
  end
105
136
 
106
- def original_exception(exception)
107
- if @@rescue_responses.has_key?(exception.cause.class.name)
108
- exception.cause
109
- else
110
- exception
111
- end
137
+ def causes_for(exception)
138
+ return enum_for(__method__, exception) unless block_given?
139
+
140
+ yield exception while exception = exception.cause
141
+ end
142
+
143
+ def wrapped_causes_for(exception, backtrace_cleaner)
144
+ causes_for(exception).map { |cause| self.class.new(backtrace_cleaner, cause) }
112
145
  end
113
146
 
114
147
  def clean_backtrace(*args)
@@ -38,7 +38,7 @@ module ActionDispatch
38
38
  #
39
39
  # See docs on the FlashHash class for more details about the flash.
40
40
  class Flash
41
- KEY = "action_dispatch.request.flash_hash".freeze
41
+ KEY = "action_dispatch.request.flash_hash"
42
42
 
43
43
  module RequestMethods
44
44
  # Access the contents of the flash. Use <tt>flash["notice"]</tt> to