actionpack 6.0.5.1 → 6.1.7.1

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 (116) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +393 -253
  3. data/MIT-LICENSE +1 -2
  4. data/lib/abstract_controller/base.rb +35 -2
  5. data/lib/abstract_controller/callbacks.rb +2 -2
  6. data/lib/abstract_controller/collector.rb +4 -2
  7. data/lib/abstract_controller/helpers.rb +105 -90
  8. data/lib/abstract_controller/railties/routes_helpers.rb +17 -1
  9. data/lib/abstract_controller/rendering.rb +9 -9
  10. data/lib/abstract_controller/translation.rb +8 -2
  11. data/lib/abstract_controller.rb +1 -0
  12. data/lib/action_controller/api.rb +2 -2
  13. data/lib/action_controller/base.rb +4 -2
  14. data/lib/action_controller/caching.rb +0 -1
  15. data/lib/action_controller/log_subscriber.rb +3 -3
  16. data/lib/action_controller/metal/conditional_get.rb +11 -3
  17. data/lib/action_controller/metal/content_security_policy.rb +1 -1
  18. data/lib/action_controller/metal/cookies.rb +3 -1
  19. data/lib/action_controller/metal/data_streaming.rb +1 -1
  20. data/lib/action_controller/metal/etag_with_template_digest.rb +3 -5
  21. data/lib/action_controller/metal/exceptions.rb +33 -0
  22. data/lib/action_controller/metal/head.rb +7 -4
  23. data/lib/action_controller/metal/helpers.rb +11 -1
  24. data/lib/action_controller/metal/http_authentication.rb +5 -2
  25. data/lib/action_controller/metal/implicit_render.rb +1 -1
  26. data/lib/action_controller/metal/instrumentation.rb +11 -9
  27. data/lib/action_controller/metal/live.rb +10 -1
  28. data/lib/action_controller/metal/logging.rb +20 -0
  29. data/lib/action_controller/metal/mime_responds.rb +6 -2
  30. data/lib/action_controller/metal/parameter_encoding.rb +35 -4
  31. data/lib/action_controller/metal/params_wrapper.rb +14 -8
  32. data/lib/action_controller/metal/permissions_policy.rb +46 -0
  33. data/lib/action_controller/metal/redirecting.rb +1 -1
  34. data/lib/action_controller/metal/rendering.rb +6 -0
  35. data/lib/action_controller/metal/request_forgery_protection.rb +1 -1
  36. data/lib/action_controller/metal/rescue.rb +1 -1
  37. data/lib/action_controller/metal/strong_parameters.rb +104 -16
  38. data/lib/action_controller/metal.rb +2 -2
  39. data/lib/action_controller/renderer.rb +23 -13
  40. data/lib/action_controller/test_case.rb +65 -56
  41. data/lib/action_controller.rb +2 -3
  42. data/lib/action_dispatch/http/cache.rb +18 -17
  43. data/lib/action_dispatch/http/content_security_policy.rb +6 -1
  44. data/lib/action_dispatch/http/filter_parameters.rb +1 -1
  45. data/lib/action_dispatch/http/filter_redirect.rb +1 -1
  46. data/lib/action_dispatch/http/headers.rb +3 -2
  47. data/lib/action_dispatch/http/mime_negotiation.rb +14 -8
  48. data/lib/action_dispatch/http/mime_type.rb +29 -16
  49. data/lib/action_dispatch/http/parameters.rb +1 -19
  50. data/lib/action_dispatch/http/permissions_policy.rb +173 -0
  51. data/lib/action_dispatch/http/request.rb +24 -8
  52. data/lib/action_dispatch/http/response.rb +17 -16
  53. data/lib/action_dispatch/http/url.rb +3 -2
  54. data/lib/action_dispatch/journey/formatter.rb +55 -30
  55. data/lib/action_dispatch/journey/gtg/builder.rb +22 -36
  56. data/lib/action_dispatch/journey/gtg/simulator.rb +8 -7
  57. data/lib/action_dispatch/journey/gtg/transition_table.rb +6 -4
  58. data/lib/action_dispatch/journey/nfa/dot.rb +0 -11
  59. data/lib/action_dispatch/journey/nodes/node.rb +4 -3
  60. data/lib/action_dispatch/journey/parser.rb +13 -13
  61. data/lib/action_dispatch/journey/parser.y +1 -1
  62. data/lib/action_dispatch/journey/path/pattern.rb +13 -18
  63. data/lib/action_dispatch/journey/route.rb +7 -18
  64. data/lib/action_dispatch/journey/router/utils.rb +6 -4
  65. data/lib/action_dispatch/journey/router.rb +26 -30
  66. data/lib/action_dispatch/journey/visitors.rb +1 -1
  67. data/lib/action_dispatch/journey.rb +0 -2
  68. data/lib/action_dispatch/middleware/actionable_exceptions.rb +1 -1
  69. data/lib/action_dispatch/middleware/cookies.rb +89 -46
  70. data/lib/action_dispatch/middleware/debug_exceptions.rb +8 -15
  71. data/lib/action_dispatch/middleware/debug_view.rb +1 -1
  72. data/lib/action_dispatch/middleware/exception_wrapper.rb +28 -16
  73. data/lib/action_dispatch/middleware/host_authorization.rb +63 -14
  74. data/lib/action_dispatch/middleware/remote_ip.rb +5 -4
  75. data/lib/action_dispatch/middleware/request_id.rb +4 -5
  76. data/lib/action_dispatch/middleware/session/abstract_store.rb +2 -2
  77. data/lib/action_dispatch/middleware/session/cookie_store.rb +2 -2
  78. data/lib/action_dispatch/middleware/show_exceptions.rb +12 -0
  79. data/lib/action_dispatch/middleware/ssl.rb +12 -7
  80. data/lib/action_dispatch/middleware/stack.rb +19 -1
  81. data/lib/action_dispatch/middleware/static.rb +154 -93
  82. data/lib/action_dispatch/middleware/templates/rescues/_message_and_suggestions.html.erb +22 -0
  83. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +2 -5
  84. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +2 -2
  85. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +2 -2
  86. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +100 -8
  87. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +1 -1
  88. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +21 -1
  89. data/lib/action_dispatch/railtie.rb +3 -2
  90. data/lib/action_dispatch/request/session.rb +2 -8
  91. data/lib/action_dispatch/request/utils.rb +26 -2
  92. data/lib/action_dispatch/routing/inspector.rb +8 -7
  93. data/lib/action_dispatch/routing/mapper.rb +102 -71
  94. data/lib/action_dispatch/routing/polymorphic_routes.rb +12 -11
  95. data/lib/action_dispatch/routing/redirection.rb +4 -4
  96. data/lib/action_dispatch/routing/route_set.rb +49 -41
  97. data/lib/action_dispatch/system_test_case.rb +35 -24
  98. data/lib/action_dispatch/system_testing/browser.rb +33 -27
  99. data/lib/action_dispatch/system_testing/driver.rb +6 -7
  100. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +47 -6
  101. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +4 -7
  102. data/lib/action_dispatch/testing/assertions/response.rb +2 -4
  103. data/lib/action_dispatch/testing/assertions/routing.rb +5 -5
  104. data/lib/action_dispatch/testing/assertions.rb +1 -1
  105. data/lib/action_dispatch/testing/integration.rb +40 -29
  106. data/lib/action_dispatch/testing/test_process.rb +32 -4
  107. data/lib/action_dispatch/testing/test_request.rb +3 -3
  108. data/lib/action_dispatch.rb +3 -2
  109. data/lib/action_pack/gem_version.rb +2 -2
  110. data/lib/action_pack.rb +1 -1
  111. metadata +18 -19
  112. data/lib/action_controller/metal/force_ssl.rb +0 -58
  113. data/lib/action_dispatch/http/parameter_filter.rb +0 -12
  114. data/lib/action_dispatch/journey/nfa/builder.rb +0 -78
  115. data/lib/action_dispatch/journey/nfa/simulator.rb +0 -47
  116. data/lib/action_dispatch/journey/nfa/transition_table.rb +0 -119
@@ -46,7 +46,10 @@ module ActionDispatch
46
46
  status = wrapper.status_code
47
47
  request.set_header "action_dispatch.exception", wrapper.unwrapped_exception
48
48
  request.set_header "action_dispatch.original_path", request.path_info
49
+ request.set_header "action_dispatch.original_request_method", request.raw_request_method
50
+ fallback_to_html_format_if_invalid_mime_type(request)
49
51
  request.path_info = "/#{status}"
52
+ request.request_method = "GET"
50
53
  response = @exceptions_app.call(request.env)
51
54
  response[1]["X-Cascade"] == "pass" ? pass_response(status) : response
52
55
  rescue Exception => failsafe_error
@@ -54,6 +57,15 @@ module ActionDispatch
54
57
  FAILSAFE_RESPONSE
55
58
  end
56
59
 
60
+ def fallback_to_html_format_if_invalid_mime_type(request)
61
+ # If the MIME type for the request is invalid then the
62
+ # @exceptions_app may not be able to handle it. To make it
63
+ # easier to handle, we switch to HTML.
64
+ request.formats
65
+ rescue ActionDispatch::Http::MimeNegotiation::InvalidType
66
+ request.set_header "HTTP_ACCEPT", "text/html"
67
+ end
68
+
57
69
  def pass_response(status)
58
70
  [status, { "Content-Type" => "text/html; charset=#{Response.default_charset}", "Content-Length" => "0" }, []]
59
71
  end
@@ -13,7 +13,7 @@ module ActionDispatch
13
13
  #
14
14
  # Requests can opt-out of redirection with +exclude+:
15
15
  #
16
- # config.ssl_options = { redirect: { exclude: -> request { request.path =~ /healthcheck/ } } }
16
+ # config.ssl_options = { redirect: { exclude: -> request { /healthcheck/.match?(request.path) } } }
17
17
  #
18
18
  # Cookies will not be flagged as secure for excluded requests.
19
19
  #
@@ -29,7 +29,7 @@ module ActionDispatch
29
29
  #
30
30
  # * +expires+: How long, in seconds, these settings will stick. The minimum
31
31
  # required to qualify for browser preload lists is 1 year. Defaults to
32
- # 1 year (recommended).
32
+ # 2 years (recommended).
33
33
  #
34
34
  # * +subdomains+: Set to +true+ to tell the browser to apply these settings
35
35
  # to all subdomains. This protects your cookies from interception by a
@@ -49,14 +49,16 @@ module ActionDispatch
49
49
  class SSL
50
50
  # :stopdoc:
51
51
 
52
- # Default to 1 year, the minimum for browser preload lists.
53
- HSTS_EXPIRES_IN = 31536000
52
+ # Default to 2 years as recommended on hstspreload.org.
53
+ HSTS_EXPIRES_IN = 63072000
54
+
55
+ PERMANENT_REDIRECT_REQUEST_METHODS = %w[GET HEAD] # :nodoc:
54
56
 
55
57
  def self.default_hsts_options
56
58
  { expires: HSTS_EXPIRES_IN, subdomains: true, preload: false }
57
59
  end
58
60
 
59
- def initialize(app, redirect: {}, hsts: {}, secure_cookies: true)
61
+ def initialize(app, redirect: {}, hsts: {}, secure_cookies: true, ssl_default_redirect_status: nil)
60
62
  @app = app
61
63
 
62
64
  @redirect = redirect
@@ -65,6 +67,7 @@ module ActionDispatch
65
67
  @secure_cookies = secure_cookies
66
68
 
67
69
  @hsts_header = build_hsts_header(normalize_hsts_options(hsts))
70
+ @ssl_default_redirect_status = ssl_default_redirect_status
68
71
  end
69
72
 
70
73
  def call(env)
@@ -126,12 +129,14 @@ module ActionDispatch
126
129
  [ @redirect.fetch(:status, redirection_status(request)),
127
130
  { "Content-Type" => "text/html",
128
131
  "Location" => https_location_for(request) },
129
- @redirect.fetch(:body, []) ]
132
+ (@redirect[:body] || []) ]
130
133
  end
131
134
 
132
135
  def redirection_status(request)
133
- if request.get? || request.head?
136
+ if PERMANENT_REDIRECT_REQUEST_METHODS.include?(request.raw_request_method)
134
137
  301 # Issue a permanent redirect via a GET request.
138
+ elsif @ssl_default_redirect_status
139
+ @ssl_default_redirect_status
135
140
  else
136
141
  307 # Issue a fresh request redirect to preserve the HTTP method.
137
142
  end
@@ -43,7 +43,7 @@ module ActionDispatch
43
43
  end
44
44
 
45
45
  # This class is used to instrument the execution of a single middleware.
46
- # It proxies the `call` method transparently and instruments the method
46
+ # It proxies the +call+ method transparently and instruments the method
47
47
  # call.
48
48
  class InstrumentationProxy
49
49
  EVENT_NAME = "process_middleware.action_dispatch"
@@ -122,6 +122,24 @@ module ActionDispatch
122
122
  middlewares.delete_if { |m| m.klass == target }
123
123
  end
124
124
 
125
+ def move(target, source)
126
+ source_index = assert_index(source, :before)
127
+ source_middleware = middlewares.delete_at(source_index)
128
+
129
+ target_index = assert_index(target, :before)
130
+ middlewares.insert(target_index, source_middleware)
131
+ end
132
+
133
+ alias_method :move_before, :move
134
+
135
+ def move_after(target, source)
136
+ source_index = assert_index(source, :after)
137
+ source_middleware = middlewares.delete_at(source_index)
138
+
139
+ target_index = assert_index(target, :after)
140
+ middlewares.insert(target_index + 1, source_middleware)
141
+ end
142
+
125
143
  def use(klass, *args, &block)
126
144
  middlewares.push(build_middleware(klass, args, block))
127
145
  end
@@ -4,126 +4,187 @@ require "rack/utils"
4
4
  require "active_support/core_ext/uri"
5
5
 
6
6
  module ActionDispatch
7
- # This middleware returns a file's contents from disk in the body response.
8
- # When initialized, it can accept optional HTTP headers, which will be set
9
- # when a response containing a file's contents is delivered.
7
+ # This middleware serves static files from disk, if available.
8
+ # If no file is found, it hands off to the main app.
10
9
  #
11
- # This middleware will render the file specified in <tt>env["PATH_INFO"]</tt>
12
- # where the base path is in the +root+ directory. For example, if the +root+
13
- # is set to +public/+, then a request with <tt>env["PATH_INFO"]</tt> of
14
- # +assets/application.js+ will return a response with the contents of a file
15
- # located at +public/assets/application.js+ if the file exists. If the file
16
- # does not exist, a 404 "File not Found" response will be returned.
17
- class FileHandler
18
- def initialize(root, index: "index", headers: {})
19
- @root = root.chomp("/").b
20
- @file_server = ::Rack::File.new(@root, headers)
21
- @index = index
10
+ # In Rails apps, this middleware is configured to serve assets from
11
+ # the +public/+ directory.
12
+ #
13
+ # Only GET and HEAD requests are served. POST and other HTTP methods
14
+ # are handed off to the main app.
15
+ #
16
+ # Only files in the root directory are served; path traversal is denied.
17
+ class Static
18
+ def initialize(app, path, index: "index", headers: {})
19
+ @app = app
20
+ @file_handler = FileHandler.new(path, index: index, headers: headers)
22
21
  end
23
22
 
24
- # Takes a path to a file. If the file is found, has valid encoding, and has
25
- # correct read permissions, the return value is a URI-escaped string
26
- # representing the filename. Otherwise, false is returned.
27
- #
28
- # Used by the +Static+ class to check the existence of a valid file
29
- # in the server's +public/+ directory (see Static#call).
30
- def match?(path)
31
- path = ::Rack::Utils.unescape_path path
32
- return false unless ::Rack::Utils.valid_path? path
33
- path = ::Rack::Utils.clean_path_info path
34
-
35
- paths = [path, "#{path}#{ext}", "#{path}/#{@index}#{ext}"]
36
-
37
- if match = paths.detect { |p|
38
- path = File.join(@root, p.b)
39
- begin
40
- File.file?(path) && File.readable?(path)
41
- rescue SystemCallError
42
- false
43
- end
44
- }
45
- ::Rack::Utils.escape_path(match).b
46
- end
23
+ def call(env)
24
+ @file_handler.attempt(env) || @app.call(env)
25
+ end
26
+ end
27
+
28
+ # This endpoint serves static files from disk using Rack::File.
29
+ #
30
+ # URL paths are matched with static files according to expected
31
+ # conventions: +path+, +path+.html, +path+/index.html.
32
+ #
33
+ # Precompressed versions of these files are checked first. Brotli (.br)
34
+ # and gzip (.gz) files are supported. If +path+.br exists, this
35
+ # endpoint returns that file with a <tt>Content-Encoding: br</tt> header.
36
+ #
37
+ # If no matching file is found, this endpoint responds 404 Not Found.
38
+ #
39
+ # Pass the +root+ directory to search for matching files, an optional
40
+ # <tt>index: "index"</tt> to change the default +path+/index.html, and optional
41
+ # additional response headers.
42
+ class FileHandler
43
+ # Accept-Encoding value -> file extension
44
+ PRECOMPRESSED = {
45
+ "br" => ".br",
46
+ "gzip" => ".gz",
47
+ "identity" => nil
48
+ }
49
+
50
+ def initialize(root, index: "index", headers: {}, precompressed: %i[ br gzip ], compressible_content_types: /\A(?:text\/|application\/javascript)/)
51
+ @root = root.chomp("/").b
52
+ @index = index
53
+
54
+ @precompressed = Array(precompressed).map(&:to_s) | %w[ identity ]
55
+ @compressible_content_types = compressible_content_types
56
+
57
+ @file_server = ::Rack::File.new(@root, headers)
47
58
  end
48
59
 
49
60
  def call(env)
50
- serve(Rack::Request.new(env))
61
+ attempt(env) || @file_server.call(env)
51
62
  end
52
63
 
53
- def serve(request)
54
- path = request.path_info
55
- gzip_path = gzip_file_path(path)
64
+ def attempt(env)
65
+ request = Rack::Request.new env
56
66
 
57
- if gzip_path && gzip_encoding_accepted?(request)
58
- request.path_info = gzip_path
59
- status, headers, body = @file_server.call(request.env)
60
- if status == 304
61
- return [status, headers, body]
67
+ if request.get? || request.head?
68
+ if found = find_file(request.path_info, accept_encoding: request.accept_encoding)
69
+ serve request, *found
62
70
  end
63
- headers["Content-Encoding"] = "gzip"
64
- headers["Content-Type"] = content_type(path)
65
- else
66
- status, headers, body = @file_server.call(request.env)
67
71
  end
68
-
69
- headers["Vary"] = "Accept-Encoding" if gzip_path
70
-
71
- [status, headers, body]
72
- ensure
73
- request.path_info = path
74
72
  end
75
73
 
76
74
  private
77
- def ext
78
- ::ActionController::Base.default_static_extension
75
+ def serve(request, filepath, content_headers)
76
+ original, request.path_info =
77
+ request.path_info, ::Rack::Utils.escape_path(filepath).b
78
+
79
+ @file_server.call(request.env).tap do |status, headers, body|
80
+ # Omit Content-Encoding/Type/etc headers for 304 Not Modified
81
+ if status != 304
82
+ headers.update(content_headers)
83
+ end
84
+ end
85
+ ensure
86
+ request.path_info = original
79
87
  end
80
88
 
81
- def content_type(path)
82
- ::Rack::Mime.mime_type(::File.extname(path), "text/plain")
89
+ # Match a URI path to a static file to be served.
90
+ #
91
+ # Used by the +Static+ class to negotiate a servable file in the
92
+ # +public/+ directory (see Static#call).
93
+ #
94
+ # Checks for +path+, +path+.html, and +path+/index.html files,
95
+ # in that order, including .br and .gzip compressed extensions.
96
+ #
97
+ # If a matching file is found, the path and necessary response headers
98
+ # (Content-Type, Content-Encoding) are returned.
99
+ def find_file(path_info, accept_encoding:)
100
+ each_candidate_filepath(path_info) do |filepath, content_type|
101
+ if response = try_files(filepath, content_type, accept_encoding: accept_encoding)
102
+ return response
103
+ end
104
+ end
83
105
  end
84
106
 
85
- def gzip_encoding_accepted?(request)
86
- request.accept_encoding.any? { |enc, quality| enc =~ /\bgzip\b/i }
107
+ def try_files(filepath, content_type, accept_encoding:)
108
+ headers = { "Content-Type" => content_type }
109
+
110
+ if compressible? content_type
111
+ try_precompressed_files filepath, headers, accept_encoding: accept_encoding
112
+ elsif file_readable? filepath
113
+ [ filepath, headers ]
114
+ end
87
115
  end
88
116
 
89
- def gzip_file_path(path)
90
- can_gzip_mime = content_type(path) =~ /\A(?:text\/|application\/javascript)/
91
- gzip_path = "#{path}.gz"
92
- if can_gzip_mime && File.exist?(File.join(@root, ::Rack::Utils.unescape_path(gzip_path)))
93
- gzip_path
94
- else
95
- false
117
+ def try_precompressed_files(filepath, headers, accept_encoding:)
118
+ each_precompressed_filepath(filepath) do |content_encoding, precompressed_filepath|
119
+ if file_readable? precompressed_filepath
120
+ # Identity encoding is default, so we skip Accept-Encoding
121
+ # negotiation and needn't set Content-Encoding.
122
+ #
123
+ # Vary header is expected when we've found other available
124
+ # encodings that Accept-Encoding ruled out.
125
+ if content_encoding == "identity"
126
+ return precompressed_filepath, headers
127
+ else
128
+ headers["Vary"] = "Accept-Encoding"
129
+
130
+ if accept_encoding.any? { |enc, _| /\b#{content_encoding}\b/i.match?(enc) }
131
+ headers["Content-Encoding"] = content_encoding
132
+ return precompressed_filepath, headers
133
+ end
134
+ end
135
+ end
96
136
  end
97
137
  end
98
- end
99
138
 
100
- # This middleware will attempt to return the contents of a file's body from
101
- # disk in the response. If a file is not found on disk, the request will be
102
- # delegated to the application stack. This middleware is commonly initialized
103
- # to serve assets from a server's +public/+ directory.
104
- #
105
- # This middleware verifies the path to ensure that only files
106
- # living in the root directory can be rendered. A request cannot
107
- # produce a directory traversal using this middleware. Only 'GET' and 'HEAD'
108
- # requests will result in a file being returned.
109
- class Static
110
- def initialize(app, path, index: "index", headers: {})
111
- @app = app
112
- @file_handler = FileHandler.new(path, index: index, headers: headers)
113
- end
139
+ def file_readable?(path)
140
+ file_stat = File.stat(File.join(@root, path.b))
141
+ rescue SystemCallError
142
+ false
143
+ else
144
+ file_stat.file? && file_stat.readable?
145
+ end
114
146
 
115
- def call(env)
116
- req = Rack::Request.new env
147
+ def compressible?(content_type)
148
+ @compressible_content_types.match?(content_type)
149
+ end
117
150
 
118
- if req.get? || req.head?
119
- path = req.path_info.chomp("/")
120
- if match = @file_handler.match?(path)
121
- req.path_info = match
122
- return @file_handler.serve(req)
151
+ def each_precompressed_filepath(filepath)
152
+ @precompressed.each do |content_encoding|
153
+ precompressed_ext = PRECOMPRESSED.fetch(content_encoding)
154
+ yield content_encoding, "#{filepath}#{precompressed_ext}"
123
155
  end
156
+
157
+ nil
124
158
  end
125
159
 
126
- @app.call(req.env)
127
- end
160
+ def each_candidate_filepath(path_info)
161
+ return unless path = clean_path(path_info)
162
+
163
+ ext = ::File.extname(path)
164
+ content_type = ::Rack::Mime.mime_type(ext, nil)
165
+ yield path, content_type || "text/plain"
166
+
167
+ # Tack on .html and /index.html only for paths that don't have
168
+ # an explicit, resolvable file extension. No need to check
169
+ # for foo.js.html and foo.js/index.html.
170
+ unless content_type
171
+ default_ext = ::ActionController::Base.default_static_extension
172
+ if ext != default_ext
173
+ default_content_type = ::Rack::Mime.mime_type(default_ext, "text/plain")
174
+
175
+ yield "#{path}#{default_ext}", default_content_type
176
+ yield "#{path}/#{@index}#{default_ext}", default_content_type
177
+ end
178
+ end
179
+
180
+ nil
181
+ end
182
+
183
+ def clean_path(path_info)
184
+ path = ::Rack::Utils.unescape_path path_info.chomp("/")
185
+ if ::Rack::Utils.valid_path? path
186
+ ::Rack::Utils.clean_path_info path
187
+ end
188
+ end
128
189
  end
129
190
  end
@@ -0,0 +1,22 @@
1
+ <% if exception.respond_to?(:original_message) && exception.respond_to?(:corrections) %>
2
+ <div class="exception-message">
3
+ <%= simple_format h(exception.original_message), { class: "message" }, wrapper_tag: "div" %>
4
+ </div>
5
+ <%
6
+ # The 'did_you_mean' gem can raise exceptions when calling #corrections on
7
+ # the exception. If it does there are no corrections to show.
8
+ corrections = exception.corrections rescue []
9
+ %>
10
+ <% if corrections.any? %>
11
+ <b>Did you mean?</b>
12
+ <ul>
13
+ <% corrections.each do |correction| %>
14
+ <li style="list-style-type: none"><%= h correction %></li>
15
+ <% end %>
16
+ </ul>
17
+ <% end %>
18
+ <% else %>
19
+ <div class="exception-message">
20
+ <%= simple_format h(exception.message), { class: "message" }, wrapper_tag: "div" %>
21
+ </div>
22
+ <% end %>
@@ -8,11 +8,8 @@
8
8
  </header>
9
9
 
10
10
  <div id="container">
11
- <h2>
12
- <%= h @exception.message %>
13
-
14
- <%= render "rescues/actions", exception: @exception, request: @request %>
15
- </h2>
11
+ <%= render "rescues/message_and_suggestions", exception: @exception %>
12
+ <%= render "rescues/actions", exception: @exception, request: @request %>
16
13
 
17
14
  <%= render "rescues/source", source_extracts: @source_extracts, show_source_idx: @show_source_idx, error_index: 0 %>
18
15
  <%= render "rescues/trace", traces: @traces, trace_to_show: @trace_to_show, error_index: 0 %>
@@ -11,10 +11,10 @@
11
11
  <h2>
12
12
  <%= h @exception.message %>
13
13
  <% if defined?(ActiveStorage) && @exception.message.match?(%r{#{ActiveStorage::Blob.table_name}|#{ActiveStorage::Attachment.table_name}}) %>
14
- <br />To resolve this issue run: rails active_storage:install
14
+ <br />To resolve this issue run: bin/rails active_storage:install
15
15
  <% end %>
16
16
  <% if defined?(ActionMailbox) && @exception.message.match?(%r{#{ActionMailbox::InboundEmail.table_name}}) %>
17
- <br />To resolve this issue run: rails action_mailbox:install
17
+ <br />To resolve this issue run: bin/rails action_mailbox:install
18
18
  <% end %>
19
19
  </h2>
20
20
 
@@ -5,10 +5,10 @@
5
5
 
6
6
  <%= @exception.message %>
7
7
  <% if defined?(ActiveStorage) && @exception.message.match?(%r{#{ActiveStorage::Blob.table_name}|#{ActiveStorage::Attachment.table_name}}) %>
8
- To resolve this issue run: rails active_storage:install
8
+ To resolve this issue run: bin/rails active_storage:install
9
9
  <% end %>
10
10
  <% if defined?(ActionMailbox) && @exception.message.match?(%r{#{ActionMailbox::InboundEmail.table_name}}) %>
11
- To resolve this issue run: rails action_mailbox:install
11
+ To resolve this issue run: bin/rails action_mailbox:install
12
12
  <% end %>
13
13
 
14
14
  <%= render template: "rescues/_source" %>
@@ -2,11 +2,14 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
5
6
  <title>Action Controller: Exception caught</title>
6
7
  <style>
7
8
  body {
8
9
  background-color: #FAFAFA;
9
10
  color: #333;
11
+ color-scheme: light dark;
12
+ supported-color-schemes: light dark;
10
13
  margin: 0px;
11
14
  }
12
15
 
@@ -30,27 +33,40 @@
30
33
 
31
34
  header {
32
35
  color: #F0F0F0;
33
- background: #C52F24;
36
+ background: #C00;
34
37
  padding: 0.5em 1.5em;
35
38
  }
36
39
 
37
40
  h1 {
41
+ overflow-wrap: break-word;
38
42
  margin: 0.2em 0;
39
43
  line-height: 1.1em;
40
44
  font-size: 2em;
41
45
  }
42
46
 
43
47
  h2 {
44
- color: #C52F24;
48
+ color: #C00;
45
49
  line-height: 25px;
46
50
  }
47
51
 
52
+ .exception-message {
53
+ padding: 8px 0;
54
+ }
55
+
56
+ .exception-message .message{
57
+ margin-bottom: 8px;
58
+ line-height: 25px;
59
+ font-size: 1.5em;
60
+ font-weight: bold;
61
+ color: #C00;
62
+ }
63
+
48
64
  .details {
49
65
  border: 1px solid #D0D0D0;
50
66
  border-radius: 4px;
51
67
  margin: 1em 0px;
52
68
  display: block;
53
- width: 978px;
69
+ max-width: 978px;
54
70
  }
55
71
 
56
72
  .summary {
@@ -78,7 +94,7 @@
78
94
  .source {
79
95
  border: 1px solid #D9D9D9;
80
96
  background: #ECECEC;
81
- width: 978px;
97
+ max-width: 978px;
82
98
  }
83
99
 
84
100
  .source pre {
@@ -114,22 +130,98 @@
114
130
  }
115
131
 
116
132
  .line.active {
117
- background-color: #FFCCCC;
133
+ background-color: #FCC;
118
134
  }
119
135
 
120
136
  .button_to {
121
137
  display: inline-block;
138
+ margin-top: 0.75em;
139
+ margin-bottom: 0.75em;
122
140
  }
123
141
 
124
142
  .hidden {
125
143
  display: none;
126
144
  }
127
145
 
146
+ input[type="submit"] {
147
+ color: white;
148
+ background-color: #C00;
149
+ border: none;
150
+ border-radius: 12px;
151
+ box-shadow: 0 3px #F99;
152
+ font-size: 13px;
153
+ font-weight: bold;
154
+ margin: 0;
155
+ padding: 10px 18px;
156
+ -webkit-appearance: none;
157
+ }
158
+ input[type="submit"]:focus,
159
+ input[type="submit"]:hover {
160
+ opacity: 0.8;
161
+ }
162
+ input[type="submit"]:active {
163
+ box-shadow: 0 2px #F99;
164
+ transform: translateY(1px)
165
+ }
166
+
167
+
128
168
  a { color: #980905; }
129
169
  a:visited { color: #666; }
130
- a.trace-frames { color: #666; }
131
- a:hover { color: #C52F24; }
132
- a.trace-frames.selected { color: #C52F24 }
170
+ a.trace-frames {
171
+ color: #666;
172
+ overflow-wrap: break-word;
173
+ }
174
+ a:hover { color: #C00; }
175
+ a.trace-frames.selected { color: #C00 }
176
+
177
+ @media (prefers-color-scheme: dark) {
178
+ body {
179
+ background-color: #222;
180
+ color: #ECECEC;
181
+ }
182
+
183
+ .details {
184
+ border-color: #666;
185
+ }
186
+
187
+ .summary {
188
+ border-color: #666;
189
+ }
190
+
191
+ .source {
192
+ border-color: #555;
193
+ background-color: #333;
194
+ }
195
+
196
+ .source .data {
197
+ background: #444;
198
+ }
199
+
200
+ .source .data .line_numbers {
201
+ background: #333;
202
+ border-color: #222;
203
+ }
204
+
205
+ .line:hover {
206
+ background: #666;
207
+ }
208
+
209
+ .line.active {
210
+ background-color: #900;
211
+ }
212
+
213
+ input[type="submit"] {
214
+ box-shadow: 0 3px #800;
215
+ }
216
+ input[type="submit"]:active {
217
+ box-shadow: 0 2px #800;
218
+ }
219
+
220
+ a { color: #C00; }
221
+ a.trace-frames { color: #999; }
222
+ a:hover { color: #E9382B; }
223
+ a.trace-frames.selected { color: #E9382B; }
224
+ }
133
225
 
134
226
  <%= yield :style %>
135
227
  </style>
@@ -2,5 +2,5 @@
2
2
  <h1>Unknown action</h1>
3
3
  </header>
4
4
  <div id="container">
5
- <h2><%= h @exception.message %></h2>
5
+ <%= render "rescues/message_and_suggestions", exception: @exception %>
6
6
  </div>