actionpack 5.2.7.1 → 6.1.4.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 (155) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +329 -352
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +4 -3
  5. data/lib/abstract_controller/base.rb +38 -4
  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 +14 -2
  9. data/lib/abstract_controller/collector.rb +1 -2
  10. data/lib/abstract_controller/helpers.rb +106 -90
  11. data/lib/abstract_controller/railties/routes_helpers.rb +17 -1
  12. data/lib/abstract_controller/rendering.rb +9 -9
  13. data/lib/abstract_controller/translation.rb +11 -5
  14. data/lib/abstract_controller.rb +1 -0
  15. data/lib/action_controller/api.rb +4 -3
  16. data/lib/action_controller/base.rb +6 -9
  17. data/lib/action_controller/caching.rb +1 -3
  18. data/lib/action_controller/log_subscriber.rb +10 -7
  19. data/lib/action_controller/metal/basic_implicit_render.rb +1 -1
  20. data/lib/action_controller/metal/conditional_get.rb +19 -5
  21. data/lib/action_controller/metal/content_security_policy.rb +1 -2
  22. data/lib/action_controller/metal/cookies.rb +3 -1
  23. data/lib/action_controller/metal/data_streaming.rb +6 -7
  24. data/lib/action_controller/metal/default_headers.rb +17 -0
  25. data/lib/action_controller/metal/etag_with_template_digest.rb +4 -6
  26. data/lib/action_controller/metal/exceptions.rb +56 -2
  27. data/lib/action_controller/metal/flash.rb +5 -5
  28. data/lib/action_controller/metal/head.rb +7 -4
  29. data/lib/action_controller/metal/helpers.rb +14 -5
  30. data/lib/action_controller/metal/http_authentication.rb +24 -23
  31. data/lib/action_controller/metal/implicit_render.rb +5 -15
  32. data/lib/action_controller/metal/instrumentation.rb +13 -14
  33. data/lib/action_controller/metal/live.rb +39 -32
  34. data/lib/action_controller/metal/logging.rb +20 -0
  35. data/lib/action_controller/metal/mime_responds.rb +19 -4
  36. data/lib/action_controller/metal/parameter_encoding.rb +35 -4
  37. data/lib/action_controller/metal/params_wrapper.rb +32 -22
  38. data/lib/action_controller/metal/permissions_policy.rb +46 -0
  39. data/lib/action_controller/metal/redirecting.rb +6 -6
  40. data/lib/action_controller/metal/renderers.rb +4 -4
  41. data/lib/action_controller/metal/rendering.rb +8 -3
  42. data/lib/action_controller/metal/request_forgery_protection.rb +26 -49
  43. data/lib/action_controller/metal/rescue.rb +1 -1
  44. data/lib/action_controller/metal/streaming.rb +0 -1
  45. data/lib/action_controller/metal/strong_parameters.rb +167 -58
  46. data/lib/action_controller/metal/url_for.rb +1 -1
  47. data/lib/action_controller/metal.rb +10 -8
  48. data/lib/action_controller/railties/helpers.rb +1 -1
  49. data/lib/action_controller/renderer.rb +37 -13
  50. data/lib/action_controller/template_assertions.rb +1 -1
  51. data/lib/action_controller/test_case.rb +71 -63
  52. data/lib/action_controller.rb +7 -4
  53. data/lib/action_dispatch/http/cache.rb +31 -27
  54. data/lib/action_dispatch/http/content_disposition.rb +45 -0
  55. data/lib/action_dispatch/http/content_security_policy.rb +39 -17
  56. data/lib/action_dispatch/http/filter_parameters.rb +9 -8
  57. data/lib/action_dispatch/http/filter_redirect.rb +2 -3
  58. data/lib/action_dispatch/http/headers.rb +4 -4
  59. data/lib/action_dispatch/http/mime_negotiation.rb +26 -13
  60. data/lib/action_dispatch/http/mime_type.rb +43 -24
  61. data/lib/action_dispatch/http/parameters.rb +14 -23
  62. data/lib/action_dispatch/http/permissions_policy.rb +173 -0
  63. data/lib/action_dispatch/http/request.rb +45 -22
  64. data/lib/action_dispatch/http/response.rb +45 -25
  65. data/lib/action_dispatch/http/upload.rb +9 -1
  66. data/lib/action_dispatch/http/url.rb +82 -82
  67. data/lib/action_dispatch/journey/formatter.rb +55 -31
  68. data/lib/action_dispatch/journey/gtg/builder.rb +22 -37
  69. data/lib/action_dispatch/journey/gtg/simulator.rb +8 -7
  70. data/lib/action_dispatch/journey/gtg/transition_table.rb +6 -5
  71. data/lib/action_dispatch/journey/nfa/dot.rb +0 -11
  72. data/lib/action_dispatch/journey/nodes/node.rb +13 -11
  73. data/lib/action_dispatch/journey/parser.rb +13 -13
  74. data/lib/action_dispatch/journey/parser.y +1 -1
  75. data/lib/action_dispatch/journey/path/pattern.rb +19 -21
  76. data/lib/action_dispatch/journey/route.rb +10 -20
  77. data/lib/action_dispatch/journey/router/utils.rb +14 -12
  78. data/lib/action_dispatch/journey/router.rb +26 -34
  79. data/lib/action_dispatch/journey/routes.rb +0 -2
  80. data/lib/action_dispatch/journey/scanner.rb +10 -4
  81. data/lib/action_dispatch/journey/visitors.rb +1 -4
  82. data/lib/action_dispatch/journey.rb +0 -2
  83. data/lib/action_dispatch/middleware/actionable_exceptions.rb +46 -0
  84. data/lib/action_dispatch/middleware/callbacks.rb +2 -4
  85. data/lib/action_dispatch/middleware/cookies.rb +128 -109
  86. data/lib/action_dispatch/middleware/debug_exceptions.rb +43 -66
  87. data/lib/action_dispatch/middleware/debug_locks.rb +5 -5
  88. data/lib/action_dispatch/middleware/debug_view.rb +66 -0
  89. data/lib/action_dispatch/middleware/exception_wrapper.rb +75 -30
  90. data/lib/action_dispatch/middleware/flash.rb +1 -1
  91. data/lib/action_dispatch/middleware/host_authorization.rb +141 -0
  92. data/lib/action_dispatch/middleware/public_exceptions.rb +6 -3
  93. data/lib/action_dispatch/middleware/remote_ip.rb +14 -16
  94. data/lib/action_dispatch/middleware/request_id.rb +5 -6
  95. data/lib/action_dispatch/middleware/session/abstract_store.rb +2 -3
  96. data/lib/action_dispatch/middleware/session/cookie_store.rb +3 -9
  97. data/lib/action_dispatch/middleware/show_exceptions.rb +3 -2
  98. data/lib/action_dispatch/middleware/ssl.rb +20 -15
  99. data/lib/action_dispatch/middleware/stack.rb +56 -2
  100. data/lib/action_dispatch/middleware/static.rb +153 -93
  101. data/lib/action_dispatch/middleware/templates/rescues/_actions.html.erb +13 -0
  102. data/lib/action_dispatch/middleware/templates/rescues/_actions.text.erb +0 -0
  103. data/lib/action_dispatch/middleware/templates/rescues/_message_and_suggestions.html.erb +22 -0
  104. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +3 -1
  105. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb +1 -1
  106. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +4 -2
  107. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +45 -35
  108. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +7 -0
  109. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb +5 -0
  110. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +23 -4
  111. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +1 -1
  112. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +6 -3
  113. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +4 -1
  114. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +104 -8
  115. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +19 -0
  116. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb +3 -0
  117. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +2 -2
  118. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +1 -1
  119. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +2 -2
  120. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +1 -1
  121. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +24 -1
  122. data/lib/action_dispatch/railtie.rb +8 -2
  123. data/lib/action_dispatch/request/session.rb +11 -10
  124. data/lib/action_dispatch/request/utils.rb +26 -2
  125. data/lib/action_dispatch/routing/inspector.rb +100 -52
  126. data/lib/action_dispatch/routing/mapper.rb +155 -103
  127. data/lib/action_dispatch/routing/polymorphic_routes.rb +13 -15
  128. data/lib/action_dispatch/routing/redirection.rb +4 -4
  129. data/lib/action_dispatch/routing/route_set.rb +71 -69
  130. data/lib/action_dispatch/routing/url_for.rb +2 -2
  131. data/lib/action_dispatch/routing.rb +21 -20
  132. data/lib/action_dispatch/system_test_case.rb +54 -11
  133. data/lib/action_dispatch/system_testing/browser.rb +53 -16
  134. data/lib/action_dispatch/system_testing/driver.rb +11 -3
  135. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +49 -7
  136. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +8 -10
  137. data/lib/action_dispatch/testing/assertion_response.rb +0 -1
  138. data/lib/action_dispatch/testing/assertions/response.rb +4 -7
  139. data/lib/action_dispatch/testing/assertions/routing.rb +20 -8
  140. data/lib/action_dispatch/testing/assertions.rb +1 -1
  141. data/lib/action_dispatch/testing/integration.rb +60 -28
  142. data/lib/action_dispatch/testing/request_encoder.rb +2 -2
  143. data/lib/action_dispatch/testing/test_process.rb +29 -4
  144. data/lib/action_dispatch/testing/test_request.rb +3 -3
  145. data/lib/action_dispatch/testing/test_response.rb +4 -32
  146. data/lib/action_dispatch.rb +9 -3
  147. data/lib/action_pack/gem_version.rb +4 -4
  148. data/lib/action_pack.rb +1 -1
  149. metadata +35 -23
  150. data/lib/action_controller/metal/force_ssl.rb +0 -99
  151. data/lib/action_dispatch/http/parameter_filter.rb +0 -86
  152. data/lib/action_dispatch/journey/nfa/builder.rb +0 -78
  153. data/lib/action_dispatch/journey/nfa/simulator.rb +0 -49
  154. data/lib/action_dispatch/journey/nfa/transition_table.rb +0 -120
  155. data/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb +0 -26
@@ -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)
@@ -83,7 +86,7 @@ module ActionDispatch
83
86
 
84
87
  private
85
88
  def set_hsts_header!(headers)
86
- headers["Strict-Transport-Security".freeze] ||= @hsts_header
89
+ headers["Strict-Transport-Security"] ||= @hsts_header
87
90
  end
88
91
 
89
92
  def normalize_hsts_options(options)
@@ -102,23 +105,23 @@ module ActionDispatch
102
105
 
103
106
  # https://tools.ietf.org/html/rfc6797#section-6.1
104
107
  def build_hsts_header(hsts)
105
- value = "max-age=#{hsts[:expires].to_i}".dup
108
+ value = +"max-age=#{hsts[:expires].to_i}"
106
109
  value << "; includeSubDomains" if hsts[:subdomains]
107
110
  value << "; preload" if hsts[:preload]
108
111
  value
109
112
  end
110
113
 
111
114
  def flag_cookies_as_secure!(headers)
112
- if cookies = headers["Set-Cookie".freeze]
113
- cookies = cookies.split("\n".freeze)
115
+ if cookies = headers["Set-Cookie"]
116
+ cookies = cookies.split("\n")
114
117
 
115
- headers["Set-Cookie".freeze] = cookies.map { |cookie|
116
- if cookie !~ /;\s*secure\s*(;|$)/i
118
+ headers["Set-Cookie"] = cookies.map { |cookie|
119
+ if !/;\s*secure\s*(;|$)/i.match?(cookie)
117
120
  "#{cookie}; secure"
118
121
  else
119
122
  cookie
120
123
  end
121
- }.join("\n".freeze)
124
+ }.join("\n")
122
125
  end
123
126
  end
124
127
 
@@ -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
@@ -141,7 +146,7 @@ module ActionDispatch
141
146
  host = @redirect[:host] || request.host
142
147
  port = @redirect[:port] || request.port
143
148
 
144
- location = "https://#{host}".dup
149
+ location = +"https://#{host}"
145
150
  location << ":#{port}" if port != 80 && port != 443
146
151
  location << request.fullpath
147
152
  location
@@ -36,6 +36,31 @@ module ActionDispatch
36
36
  def build(app)
37
37
  klass.new(app, *args, &block)
38
38
  end
39
+
40
+ def build_instrumented(app)
41
+ InstrumentationProxy.new(build(app), inspect)
42
+ end
43
+ end
44
+
45
+ # This class is used to instrument the execution of a single middleware.
46
+ # It proxies the +call+ method transparently and instruments the method
47
+ # call.
48
+ class InstrumentationProxy
49
+ EVENT_NAME = "process_middleware.action_dispatch"
50
+
51
+ def initialize(middleware, class_name)
52
+ @middleware = middleware
53
+
54
+ @payload = {
55
+ middleware: class_name,
56
+ }
57
+ end
58
+
59
+ def call(env)
60
+ ActiveSupport::Notifications.instrument(EVENT_NAME, @payload) do
61
+ @middleware.call(env)
62
+ end
63
+ end
39
64
  end
40
65
 
41
66
  include Enumerable
@@ -66,6 +91,7 @@ module ActionDispatch
66
91
  def unshift(klass, *args, &block)
67
92
  middlewares.unshift(build_middleware(klass, args, block))
68
93
  end
94
+ ruby2_keywords(:unshift) if respond_to?(:ruby2_keywords, true)
69
95
 
70
96
  def initialize_copy(other)
71
97
  self.middlewares = other.middlewares.dup
@@ -75,6 +101,7 @@ module ActionDispatch
75
101
  index = assert_index(index, :before)
76
102
  middlewares.insert(index, build_middleware(klass, args, block))
77
103
  end
104
+ ruby2_keywords(:insert) if respond_to?(:ruby2_keywords, true)
78
105
 
79
106
  alias_method :insert_before, :insert
80
107
 
@@ -82,27 +109,54 @@ module ActionDispatch
82
109
  index = assert_index(index, :after)
83
110
  insert(index + 1, *args, &block)
84
111
  end
112
+ ruby2_keywords(:insert_after) if respond_to?(:ruby2_keywords, true)
85
113
 
86
114
  def swap(target, *args, &block)
87
115
  index = assert_index(target, :before)
88
116
  insert(index, *args, &block)
89
117
  middlewares.delete_at(index + 1)
90
118
  end
119
+ ruby2_keywords(:swap) if respond_to?(:ruby2_keywords, true)
91
120
 
92
121
  def delete(target)
93
122
  middlewares.delete_if { |m| m.klass == target }
94
123
  end
95
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
+
96
143
  def use(klass, *args, &block)
97
144
  middlewares.push(build_middleware(klass, args, block))
98
145
  end
146
+ ruby2_keywords(:use) if respond_to?(:ruby2_keywords, true)
99
147
 
100
148
  def build(app = nil, &block)
101
- middlewares.freeze.reverse.inject(app || block) { |a, e| e.build(a) }
149
+ instrumenting = ActiveSupport::Notifications.notifier.listening?(InstrumentationProxy::EVENT_NAME)
150
+ middlewares.freeze.reverse.inject(app || block) do |a, e|
151
+ if instrumenting
152
+ e.build_instrumented(a)
153
+ else
154
+ e.build(a)
155
+ end
156
+ end
102
157
  end
103
158
 
104
159
  private
105
-
106
160
  def assert_index(index, where)
107
161
  i = index.is_a?(Integer) ? index : middlewares.index { |m| m.klass == index }
108
162
  raise "No such middleware to insert #{where}: #{index.inspect}" unless i
@@ -4,127 +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
23
+ def call(env)
24
+ @file_handler.attempt(env) || @app.call(env)
25
+ end
26
+ end
44
27
 
45
- }
46
- return ::Rack::Utils.escape_path(match).b
47
- end
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)
48
58
  end
49
59
 
50
60
  def call(env)
51
- serve(Rack::Request.new(env))
61
+ attempt(env) || @file_server.call(env)
52
62
  end
53
63
 
54
- def serve(request)
55
- path = request.path_info
56
- gzip_path = gzip_file_path(path)
64
+ def attempt(env)
65
+ request = Rack::Request.new env
57
66
 
58
- if gzip_path && gzip_encoding_accepted?(request)
59
- request.path_info = gzip_path
60
- status, headers, body = @file_server.call(request.env)
61
- if status == 304
62
- 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
63
70
  end
64
- headers["Content-Encoding"] = "gzip"
65
- headers["Content-Type"] = content_type(path)
66
- else
67
- status, headers, body = @file_server.call(request.env)
68
71
  end
69
-
70
- headers["Vary"] = "Accept-Encoding" if gzip_path
71
-
72
- return [status, headers, body]
73
- ensure
74
- request.path_info = path
75
72
  end
76
73
 
77
74
  private
78
- def ext
79
- ::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
80
87
  end
81
88
 
82
- def content_type(path)
83
- ::Rack::Mime.mime_type(::File.extname(path), "text/plain".freeze)
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
84
105
  end
85
106
 
86
- def gzip_encoding_accepted?(request)
87
- 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
88
115
  end
89
116
 
90
- def gzip_file_path(path)
91
- can_gzip_mime = content_type(path) =~ /\A(?:text\/|application\/javascript)/
92
- gzip_path = "#{path}.gz"
93
- if can_gzip_mime && File.exist?(File.join(@root, ::Rack::Utils.unescape_path(gzip_path).b))
94
- gzip_path.b
95
- else
96
- 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
97
136
  end
98
137
  end
99
- end
100
138
 
101
- # This middleware will attempt to return the contents of a file's body from
102
- # disk in the response. If a file is not found on disk, the request will be
103
- # delegated to the application stack. This middleware is commonly initialized
104
- # to serve assets from a server's +public/+ directory.
105
- #
106
- # This middleware verifies the path to ensure that only files
107
- # living in the root directory can be rendered. A request cannot
108
- # produce a directory traversal using this middleware. Only 'GET' and 'HEAD'
109
- # requests will result in a file being returned.
110
- class Static
111
- def initialize(app, path, index: "index", headers: {})
112
- @app = app
113
- @file_handler = FileHandler.new(path, index: index, headers: headers)
114
- 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
115
146
 
116
- def call(env)
117
- req = Rack::Request.new env
147
+ def compressible?(content_type)
148
+ @compressible_content_types.match?(content_type)
149
+ end
118
150
 
119
- if req.get? || req.head?
120
- path = req.path_info.chomp("/".freeze)
121
- if match = @file_handler.match?(path)
122
- req.path_info = match
123
- 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}"
124
155
  end
156
+
157
+ nil
125
158
  end
126
159
 
127
- @app.call(req.env)
128
- 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
129
189
  end
130
190
  end
@@ -0,0 +1,13 @@
1
+ <% actions = ActiveSupport::ActionableError.actions(exception) %>
2
+
3
+ <% if actions.any? %>
4
+ <div class="actions">
5
+ <% actions.each do |action, _| %>
6
+ <%= button_to action, ActionDispatch::ActionableExceptions.endpoint, params: {
7
+ error: exception.class.name,
8
+ action: action,
9
+ location: request.path
10
+ } %>
11
+ <% end %>
12
+ </div>
13
+ <% 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 %>
@@ -6,7 +6,9 @@
6
6
  <% end %>
7
7
 
8
8
  <h2 style="margin-top: 30px">Request</h2>
9
- <p><b>Parameters</b>:</p> <pre><%= debug_params(@request.filtered_parameters) %></pre>
9
+ <% if params_valid? %>
10
+ <p><b>Parameters</b>:</p> <pre><%= debug_params(@request.filtered_parameters) %></pre>
11
+ <% end %>
10
12
 
11
13
  <div class="details">
12
14
  <div class="summary"><a href="#" onclick="return toggleSessionDump()">Toggle session dump</a></div>
@@ -1,5 +1,5 @@
1
1
  <%
2
- clean_params = @request.filtered_parameters.clone
2
+ clean_params = params_valid? ? @request.filtered_parameters.clone : {}
3
3
  clean_params.delete("action")
4
4
  clean_params.delete("controller")
5
5
 
@@ -1,6 +1,8 @@
1
- <% @source_extracts.each_with_index do |source_extract, index| %>
1
+ <% error_index = local_assigns[:error_index] || 0 %>
2
+
3
+ <% source_extracts.each_with_index do |source_extract, index| %>
2
4
  <% if source_extract[:code] %>
3
- <div class="source <%="hidden" if @show_source_idx != index%>" id="frame-source-<%=index%>">
5
+ <div class="source <%= "hidden" if show_source_idx != index %>" id="frame-source-<%= error_index %>-<%= index %>">
4
6
  <div class="info">
5
7
  Extracted source (around line <strong>#<%= source_extract[:line_number] %></strong>):
6
8
  </div>
@@ -1,52 +1,62 @@
1
- <% names = @traces.keys %>
1
+ <% names = traces.keys %>
2
+ <% error_index = local_assigns[:error_index] || 0 %>
2
3
 
3
4
  <p><code>Rails.root: <%= defined?(Rails) && Rails.respond_to?(:root) ? Rails.root : "unset" %></code></p>
4
5
 
5
- <div id="traces">
6
+ <div id="traces-<%= error_index %>">
6
7
  <% names.each do |name| %>
7
8
  <%
8
- show = "show('#{name.gsub(/\s/, '-')}');"
9
- hide = (names - [name]).collect {|hide_name| "hide('#{hide_name.gsub(/\s/, '-')}');"}
9
+ show = "show('#{name.gsub(/\s/, '-')}-#{error_index}');"
10
+ hide = (names - [name]).collect {|hide_name| "hide('#{hide_name.gsub(/\s/, '-')}-#{error_index}');"}
10
11
  %>
11
12
  <a href="#" onclick="<%= hide.join %><%= show %>; return false;"><%= name %></a> <%= '|' unless names.last == name %>
12
13
  <% end %>
13
14
 
14
- <% @traces.each do |name, trace| %>
15
- <div id="<%= name.gsub(/\s/, '-') %>" style="display: <%= (name == @trace_to_show) ? 'block' : 'none' %>;">
16
- <pre><code><% trace.each do |frame| %><a class="trace-frames" data-frame-id="<%= frame[:id] %>" href="#"><%= frame[:trace] %></a><br><% end %></code></pre>
15
+ <% traces.each do |name, trace| %>
16
+ <div id="<%= "#{name.gsub(/\s/, '-')}-#{error_index}" %>" style="display: <%= (name == trace_to_show) ? 'block' : 'none' %>;">
17
+ <code style="font-size: 11px;">
18
+ <% trace.each do |frame| %>
19
+ <a class="trace-frames trace-frames-<%= error_index %>" data-exception-object-id="<%= frame[:exception_object_id] %>" data-frame-id="<%= frame[:id] %>" href="#">
20
+ <%= frame[:trace] %>
21
+ </a>
22
+ <br>
23
+ <% end %>
24
+ </code>
17
25
  </div>
18
26
  <% end %>
19
27
 
20
28
  <script type="text/javascript">
21
- var traceFrames = document.getElementsByClassName('trace-frames');
22
- var selectedFrame, currentSource = document.getElementById('frame-source-0');
23
-
24
- // Add click listeners for all stack frames
25
- for (var i = 0; i < traceFrames.length; i++) {
26
- traceFrames[i].addEventListener('click', function(e) {
27
- e.preventDefault();
28
- var target = e.target;
29
- var frame_id = target.dataset.frameId;
30
-
31
- if (selectedFrame) {
32
- selectedFrame.className = selectedFrame.className.replace("selected", "");
33
- }
34
-
35
- target.className += " selected";
36
- selectedFrame = target;
37
-
38
- // Change the extracted source code
39
- changeSourceExtract(frame_id);
40
- });
41
-
42
- function changeSourceExtract(frame_id) {
43
- var el = document.getElementById('frame-source-' + frame_id);
44
- if (currentSource && el) {
45
- currentSource.className += " hidden";
46
- el.className = el.className.replace(" hidden", "");
47
- currentSource = el;
29
+ (function() {
30
+ var traceFrames = document.getElementsByClassName('trace-frames-<%= error_index %>');
31
+ var selectedFrame, currentSource = document.getElementById('frame-source-<%= error_index %>-0');
32
+
33
+ // Add click listeners for all stack frames
34
+ for (var i = 0; i < traceFrames.length; i++) {
35
+ traceFrames[i].addEventListener('click', function(e) {
36
+ e.preventDefault();
37
+ var target = e.target;
38
+ var frame_id = target.dataset.frameId;
39
+
40
+ if (selectedFrame) {
41
+ selectedFrame.className = selectedFrame.className.replace("selected", "");
42
+ }
43
+
44
+ target.className += " selected";
45
+ selectedFrame = target;
46
+
47
+ // Change the extracted source code
48
+ changeSourceExtract(frame_id);
49
+ });
50
+
51
+ function changeSourceExtract(frame_id) {
52
+ var el = document.getElementById('frame-source-<%= error_index %>-' + frame_id);
53
+ if (currentSource && el) {
54
+ currentSource.className += " hidden";
55
+ el.className = el.className.replace(" hidden", "");
56
+ currentSource = el;
57
+ }
48
58
  }
49
59
  }
50
- }
60
+ })();
51
61
  </script>
52
62
  </div>