actionpack 4.2.11.1 → 6.1.3.2

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 (187) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +291 -489
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +9 -9
  5. data/lib/abstract_controller/asset_paths.rb +2 -0
  6. data/lib/abstract_controller/base.rb +81 -51
  7. data/lib/{action_controller → abstract_controller}/caching/fragments.rb +64 -17
  8. data/lib/abstract_controller/caching.rb +66 -0
  9. data/lib/abstract_controller/callbacks.rb +61 -33
  10. data/lib/abstract_controller/collector.rb +9 -13
  11. data/lib/abstract_controller/error.rb +6 -0
  12. data/lib/abstract_controller/helpers.rb +115 -99
  13. data/lib/abstract_controller/logger.rb +2 -0
  14. data/lib/abstract_controller/railties/routes_helpers.rb +21 -3
  15. data/lib/abstract_controller/rendering.rb +48 -47
  16. data/lib/abstract_controller/translation.rb +17 -8
  17. data/lib/abstract_controller/url_for.rb +2 -0
  18. data/lib/abstract_controller.rb +13 -5
  19. data/lib/action_controller/api/api_rendering.rb +16 -0
  20. data/lib/action_controller/api.rb +150 -0
  21. data/lib/action_controller/base.rb +29 -24
  22. data/lib/action_controller/caching.rb +12 -57
  23. data/lib/action_controller/form_builder.rb +50 -0
  24. data/lib/action_controller/log_subscriber.rb +17 -19
  25. data/lib/action_controller/metal/basic_implicit_render.rb +13 -0
  26. data/lib/action_controller/metal/conditional_get.rb +134 -46
  27. data/lib/action_controller/metal/content_security_policy.rb +51 -0
  28. data/lib/action_controller/metal/cookies.rb +6 -4
  29. data/lib/action_controller/metal/data_streaming.rb +30 -50
  30. data/lib/action_controller/metal/default_headers.rb +17 -0
  31. data/lib/action_controller/metal/etag_with_flash.rb +18 -0
  32. data/lib/action_controller/metal/etag_with_template_digest.rb +21 -16
  33. data/lib/action_controller/metal/exceptions.rb +63 -15
  34. data/lib/action_controller/metal/flash.rb +9 -8
  35. data/lib/action_controller/metal/head.rb +26 -21
  36. data/lib/action_controller/metal/helpers.rb +37 -18
  37. data/lib/action_controller/metal/http_authentication.rb +81 -73
  38. data/lib/action_controller/metal/implicit_render.rb +53 -9
  39. data/lib/action_controller/metal/instrumentation.rb +32 -35
  40. data/lib/action_controller/metal/live.rb +102 -120
  41. data/lib/action_controller/metal/logging.rb +20 -0
  42. data/lib/action_controller/metal/mime_responds.rb +49 -47
  43. data/lib/action_controller/metal/parameter_encoding.rb +82 -0
  44. data/lib/action_controller/metal/params_wrapper.rb +83 -66
  45. data/lib/action_controller/metal/permissions_policy.rb +46 -0
  46. data/lib/action_controller/metal/redirecting.rb +53 -32
  47. data/lib/action_controller/metal/renderers.rb +87 -44
  48. data/lib/action_controller/metal/rendering.rb +77 -50
  49. data/lib/action_controller/metal/request_forgery_protection.rb +267 -103
  50. data/lib/action_controller/metal/rescue.rb +10 -17
  51. data/lib/action_controller/metal/streaming.rb +12 -11
  52. data/lib/action_controller/metal/strong_parameters.rb +714 -186
  53. data/lib/action_controller/metal/testing.rb +2 -17
  54. data/lib/action_controller/metal/url_for.rb +19 -10
  55. data/lib/action_controller/metal.rb +104 -87
  56. data/lib/action_controller/railtie.rb +28 -10
  57. data/lib/action_controller/railties/helpers.rb +3 -1
  58. data/lib/action_controller/renderer.rb +141 -0
  59. data/lib/action_controller/template_assertions.rb +11 -0
  60. data/lib/action_controller/test_case.rb +296 -422
  61. data/lib/action_controller.rb +34 -23
  62. data/lib/action_dispatch/http/cache.rb +107 -56
  63. data/lib/action_dispatch/http/content_disposition.rb +45 -0
  64. data/lib/action_dispatch/http/content_security_policy.rb +286 -0
  65. data/lib/action_dispatch/http/filter_parameters.rb +32 -25
  66. data/lib/action_dispatch/http/filter_redirect.rb +10 -12
  67. data/lib/action_dispatch/http/headers.rb +55 -22
  68. data/lib/action_dispatch/http/mime_negotiation.rb +79 -51
  69. data/lib/action_dispatch/http/mime_type.rb +153 -121
  70. data/lib/action_dispatch/http/mime_types.rb +20 -6
  71. data/lib/action_dispatch/http/parameters.rb +90 -40
  72. data/lib/action_dispatch/http/permissions_policy.rb +173 -0
  73. data/lib/action_dispatch/http/rack_cache.rb +2 -0
  74. data/lib/action_dispatch/http/request.rb +226 -121
  75. data/lib/action_dispatch/http/response.rb +248 -113
  76. data/lib/action_dispatch/http/upload.rb +21 -7
  77. data/lib/action_dispatch/http/url.rb +182 -100
  78. data/lib/action_dispatch/journey/formatter.rb +90 -43
  79. data/lib/action_dispatch/journey/gtg/builder.rb +28 -41
  80. data/lib/action_dispatch/journey/gtg/simulator.rb +11 -16
  81. data/lib/action_dispatch/journey/gtg/transition_table.rb +23 -21
  82. data/lib/action_dispatch/journey/nfa/dot.rb +3 -14
  83. data/lib/action_dispatch/journey/nodes/node.rb +29 -15
  84. data/lib/action_dispatch/journey/parser.rb +17 -16
  85. data/lib/action_dispatch/journey/parser.y +4 -3
  86. data/lib/action_dispatch/journey/parser_extras.rb +12 -4
  87. data/lib/action_dispatch/journey/path/pattern.rb +58 -54
  88. data/lib/action_dispatch/journey/route.rb +100 -32
  89. data/lib/action_dispatch/journey/router/utils.rb +29 -18
  90. data/lib/action_dispatch/journey/router.rb +55 -51
  91. data/lib/action_dispatch/journey/routes.rb +17 -17
  92. data/lib/action_dispatch/journey/scanner.rb +26 -17
  93. data/lib/action_dispatch/journey/visitors.rb +98 -54
  94. data/lib/action_dispatch/journey.rb +5 -5
  95. data/lib/action_dispatch/middleware/actionable_exceptions.rb +46 -0
  96. data/lib/action_dispatch/middleware/callbacks.rb +3 -6
  97. data/lib/action_dispatch/middleware/cookies.rb +347 -217
  98. data/lib/action_dispatch/middleware/debug_exceptions.rb +135 -63
  99. data/lib/action_dispatch/middleware/debug_locks.rb +124 -0
  100. data/lib/action_dispatch/middleware/debug_view.rb +66 -0
  101. data/lib/action_dispatch/middleware/exception_wrapper.rb +115 -71
  102. data/lib/action_dispatch/middleware/executor.rb +21 -0
  103. data/lib/action_dispatch/middleware/flash.rb +78 -54
  104. data/lib/action_dispatch/middleware/host_authorization.rb +130 -0
  105. data/lib/action_dispatch/middleware/public_exceptions.rb +32 -27
  106. data/lib/action_dispatch/middleware/reloader.rb +5 -91
  107. data/lib/action_dispatch/middleware/remote_ip.rb +53 -45
  108. data/lib/action_dispatch/middleware/request_id.rb +17 -10
  109. data/lib/action_dispatch/middleware/session/abstract_store.rb +41 -26
  110. data/lib/action_dispatch/middleware/session/cache_store.rb +24 -14
  111. data/lib/action_dispatch/middleware/session/cookie_store.rb +74 -75
  112. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +8 -2
  113. data/lib/action_dispatch/middleware/show_exceptions.rb +28 -23
  114. data/lib/action_dispatch/middleware/ssl.rb +118 -35
  115. data/lib/action_dispatch/middleware/stack.rb +82 -41
  116. data/lib/action_dispatch/middleware/static.rb +156 -89
  117. data/lib/action_dispatch/middleware/templates/rescues/_actions.html.erb +13 -0
  118. data/lib/action_dispatch/middleware/templates/rescues/_actions.text.erb +0 -0
  119. data/lib/action_dispatch/middleware/templates/rescues/_message_and_suggestions.html.erb +22 -0
  120. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +4 -14
  121. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb +1 -1
  122. data/lib/action_dispatch/middleware/templates/rescues/{_source.erb → _source.html.erb} +4 -2
  123. data/lib/action_dispatch/middleware/templates/rescues/_source.text.erb +8 -0
  124. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +45 -35
  125. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +7 -0
  126. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb +5 -0
  127. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +23 -4
  128. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +1 -1
  129. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +24 -0
  130. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +15 -0
  131. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +105 -8
  132. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +19 -0
  133. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb +3 -0
  134. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +2 -2
  135. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +1 -1
  136. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +3 -3
  137. data/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb +1 -1
  138. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +1 -1
  139. data/lib/action_dispatch/middleware/templates/routes/_route.html.erb +4 -4
  140. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +87 -64
  141. data/lib/action_dispatch/railtie.rb +27 -13
  142. data/lib/action_dispatch/request/session.rb +109 -61
  143. data/lib/action_dispatch/request/utils.rb +90 -23
  144. data/lib/action_dispatch/routing/endpoint.rb +9 -2
  145. data/lib/action_dispatch/routing/inspector.rb +141 -102
  146. data/lib/action_dispatch/routing/mapper.rb +811 -473
  147. data/lib/action_dispatch/routing/polymorphic_routes.rb +167 -143
  148. data/lib/action_dispatch/routing/redirection.rb +37 -27
  149. data/lib/action_dispatch/routing/route_set.rb +363 -331
  150. data/lib/action_dispatch/routing/routes_proxy.rb +32 -5
  151. data/lib/action_dispatch/routing/url_for.rb +66 -26
  152. data/lib/action_dispatch/routing.rb +36 -36
  153. data/lib/action_dispatch/system_test_case.rb +190 -0
  154. data/lib/action_dispatch/system_testing/browser.rb +86 -0
  155. data/lib/action_dispatch/system_testing/driver.rb +67 -0
  156. data/lib/action_dispatch/system_testing/server.rb +31 -0
  157. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +138 -0
  158. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +29 -0
  159. data/lib/action_dispatch/testing/assertion_response.rb +46 -0
  160. data/lib/action_dispatch/testing/assertions/response.rb +44 -22
  161. data/lib/action_dispatch/testing/assertions/routing.rb +47 -31
  162. data/lib/action_dispatch/testing/assertions.rb +6 -4
  163. data/lib/action_dispatch/testing/integration.rb +391 -220
  164. data/lib/action_dispatch/testing/request_encoder.rb +55 -0
  165. data/lib/action_dispatch/testing/test_process.rb +53 -22
  166. data/lib/action_dispatch/testing/test_request.rb +27 -34
  167. data/lib/action_dispatch/testing/test_response.rb +11 -11
  168. data/lib/action_dispatch.rb +35 -21
  169. data/lib/action_pack/gem_version.rb +6 -4
  170. data/lib/action_pack/version.rb +3 -1
  171. data/lib/action_pack.rb +4 -2
  172. metadata +78 -48
  173. data/lib/action_controller/metal/force_ssl.rb +0 -97
  174. data/lib/action_controller/metal/hide_actions.rb +0 -40
  175. data/lib/action_controller/metal/rack_delegation.rb +0 -32
  176. data/lib/action_controller/middleware.rb +0 -39
  177. data/lib/action_controller/model_naming.rb +0 -12
  178. data/lib/action_dispatch/http/parameter_filter.rb +0 -72
  179. data/lib/action_dispatch/journey/backwards.rb +0 -5
  180. data/lib/action_dispatch/journey/nfa/builder.rb +0 -76
  181. data/lib/action_dispatch/journey/nfa/simulator.rb +0 -47
  182. data/lib/action_dispatch/journey/nfa/transition_table.rb +0 -163
  183. data/lib/action_dispatch/journey/router/strexp.rb +0 -27
  184. data/lib/action_dispatch/middleware/params_parser.rb +0 -60
  185. data/lib/action_dispatch/testing/assertions/dom.rb +0 -3
  186. data/lib/action_dispatch/testing/assertions/selector.rb +0 -3
  187. data/lib/action_dispatch/testing/assertions/tag.rb +0 -3
@@ -1,34 +1,45 @@
1
- require 'active_support/rails'
2
- require 'abstract_controller'
3
- require 'action_dispatch'
4
- require 'action_controller/metal/live'
5
- require 'action_controller/metal/strong_parameters'
1
+ # frozen_string_literal: true
2
+
3
+ require "abstract_controller"
4
+ require "action_dispatch"
5
+ require "action_controller/metal/strong_parameters"
6
6
 
7
7
  module ActionController
8
8
  extend ActiveSupport::Autoload
9
9
 
10
+ autoload :API
10
11
  autoload :Base
11
- autoload :Caching
12
12
  autoload :Metal
13
- autoload :Middleware
13
+ autoload :Renderer
14
+ autoload :FormBuilder
15
+
16
+ eager_autoload do
17
+ autoload :Caching
18
+ end
14
19
 
15
20
  autoload_under "metal" do
16
- autoload :Compatibility
21
+ eager_autoload do
22
+ autoload :Live
23
+ end
24
+
17
25
  autoload :ConditionalGet
26
+ autoload :ContentSecurityPolicy
18
27
  autoload :Cookies
19
28
  autoload :DataStreaming
29
+ autoload :DefaultHeaders
20
30
  autoload :EtagWithTemplateDigest
31
+ autoload :EtagWithFlash
32
+ autoload :PermissionsPolicy
21
33
  autoload :Flash
22
- autoload :ForceSSL
23
34
  autoload :Head
24
35
  autoload :Helpers
25
- autoload :HideActions
26
36
  autoload :HttpAuthentication
37
+ autoload :BasicImplicitRender
27
38
  autoload :ImplicitRender
28
39
  autoload :Instrumentation
40
+ autoload :Logging
29
41
  autoload :MimeResponds
30
42
  autoload :ParamsWrapper
31
- autoload :RackDelegation
32
43
  autoload :Redirecting
33
44
  autoload :Renderers
34
45
  autoload :Rendering
@@ -36,23 +47,23 @@ module ActionController
36
47
  autoload :Rescue
37
48
  autoload :Streaming
38
49
  autoload :StrongParameters
50
+ autoload :ParameterEncoding
39
51
  autoload :Testing
40
52
  autoload :UrlFor
41
53
  end
42
54
 
43
- autoload :TestCase, 'action_controller/test_case'
44
- autoload :TemplateAssertions, 'action_controller/test_case'
45
-
46
- def self.eager_load!
47
- super
48
- ActionController::Caching.eager_load!
55
+ autoload_under "api" do
56
+ autoload :ApiRendering
49
57
  end
58
+
59
+ autoload :TestCase, "action_controller/test_case"
60
+ autoload :TemplateAssertions, "action_controller/test_case"
50
61
  end
51
62
 
52
63
  # Common Active Support usage in Action Controller
53
- require 'active_support/core_ext/module/attribute_accessors'
54
- require 'active_support/core_ext/load_error'
55
- require 'active_support/core_ext/module/attr_internal'
56
- require 'active_support/core_ext/name_error'
57
- require 'active_support/core_ext/uri'
58
- require 'active_support/inflector'
64
+ require "active_support/core_ext/module/attribute_accessors"
65
+ require "active_support/core_ext/load_error"
66
+ require "active_support/core_ext/module/attr_internal"
67
+ require "active_support/core_ext/name_error"
68
+ require "active_support/core_ext/uri"
69
+ require "active_support/inflector"
@@ -1,26 +1,24 @@
1
+ # frozen_string_literal: true
1
2
 
2
3
  module ActionDispatch
3
4
  module Http
4
5
  module Cache
5
6
  module Request
6
-
7
- HTTP_IF_MODIFIED_SINCE = 'HTTP_IF_MODIFIED_SINCE'.freeze
8
- HTTP_IF_NONE_MATCH = 'HTTP_IF_NONE_MATCH'.freeze
7
+ HTTP_IF_MODIFIED_SINCE = "HTTP_IF_MODIFIED_SINCE"
8
+ HTTP_IF_NONE_MATCH = "HTTP_IF_NONE_MATCH"
9
9
 
10
10
  def if_modified_since
11
- if since = env[HTTP_IF_MODIFIED_SINCE]
11
+ if since = get_header(HTTP_IF_MODIFIED_SINCE)
12
12
  Time.rfc2822(since) rescue nil
13
13
  end
14
14
  end
15
15
 
16
16
  def if_none_match
17
- env[HTTP_IF_NONE_MATCH]
17
+ get_header HTTP_IF_NONE_MATCH
18
18
  end
19
19
 
20
20
  def if_none_match_etags
21
- (if_none_match ? if_none_match.split(/\s*,\s*/) : []).collect do |etag|
22
- etag.gsub(/^\"|\"$/, "")
23
- end
21
+ if_none_match ? if_none_match.split(/\s*,\s*/) : []
24
22
  end
25
23
 
26
24
  def not_modified?(modified_at)
@@ -29,8 +27,8 @@ module ActionDispatch
29
27
 
30
28
  def etag_matches?(etag)
31
29
  if etag
32
- etag = etag.gsub(/^\"|\"$/, "")
33
- if_none_match_etags.include?(etag)
30
+ validators = if_none_match_etags
31
+ validators.include?(etag) || validators.include?("*")
34
32
  end
35
33
  end
36
34
 
@@ -51,53 +49,95 @@ module ActionDispatch
51
49
  end
52
50
 
53
51
  module Response
54
- attr_reader :cache_control, :etag
55
- alias :etag? :etag
52
+ attr_reader :cache_control
56
53
 
57
54
  def last_modified
58
- if last = headers[LAST_MODIFIED]
55
+ if last = get_header(LAST_MODIFIED)
59
56
  Time.httpdate(last)
60
57
  end
61
58
  end
62
59
 
63
60
  def last_modified?
64
- headers.include?(LAST_MODIFIED)
61
+ has_header? LAST_MODIFIED
65
62
  end
66
63
 
67
64
  def last_modified=(utc_time)
68
- headers[LAST_MODIFIED] = utc_time.httpdate
65
+ set_header LAST_MODIFIED, utc_time.httpdate
69
66
  end
70
67
 
71
68
  def date
72
- if date_header = headers[DATE]
69
+ if date_header = get_header(DATE)
73
70
  Time.httpdate(date_header)
74
71
  end
75
72
  end
76
73
 
77
74
  def date?
78
- headers.include?(DATE)
75
+ has_header? DATE
79
76
  end
80
77
 
81
78
  def date=(utc_time)
82
- headers[DATE] = utc_time.httpdate
79
+ set_header DATE, utc_time.httpdate
80
+ end
81
+
82
+ # This method sets a weak ETag validator on the response so browsers
83
+ # and proxies may cache the response, keyed on the ETag. On subsequent
84
+ # requests, the If-None-Match header is set to the cached ETag. If it
85
+ # matches the current ETag, we can return a 304 Not Modified response
86
+ # with no body, letting the browser or proxy know that their cache is
87
+ # current. Big savings in request time and network bandwidth.
88
+ #
89
+ # Weak ETags are considered to be semantically equivalent but not
90
+ # byte-for-byte identical. This is perfect for browser caching of HTML
91
+ # pages where we don't care about exact equality, just what the user
92
+ # is viewing.
93
+ #
94
+ # Strong ETags are considered byte-for-byte identical. They allow a
95
+ # browser or proxy cache to support Range requests, useful for paging
96
+ # through a PDF file or scrubbing through a video. Some CDNs only
97
+ # support strong ETags and will ignore weak ETags entirely.
98
+ #
99
+ # Weak ETags are what we almost always need, so they're the default.
100
+ # Check out #strong_etag= to provide a strong ETag validator.
101
+ def etag=(weak_validators)
102
+ self.weak_etag = weak_validators
103
+ end
104
+
105
+ def weak_etag=(weak_validators)
106
+ set_header "ETag", generate_weak_etag(weak_validators)
83
107
  end
84
108
 
85
- def etag=(etag)
86
- key = ActiveSupport::Cache.expand_cache_key(etag)
87
- @etag = self[ETAG] = %("#{Digest::MD5.hexdigest(key)}")
109
+ def strong_etag=(strong_validators)
110
+ set_header "ETag", generate_strong_etag(strong_validators)
111
+ end
112
+
113
+ def etag?; etag; end
114
+
115
+ # True if an ETag is set and it's a weak validator (preceded with W/)
116
+ def weak_etag?
117
+ etag? && etag.start_with?('W/"')
118
+ end
119
+
120
+ # True if an ETag is set and it isn't a weak validator (not preceded with W/)
121
+ def strong_etag?
122
+ etag? && !weak_etag?
88
123
  end
89
124
 
90
125
  private
126
+ DATE = "Date"
127
+ LAST_MODIFIED = "Last-Modified"
128
+ SPECIAL_KEYS = Set.new(%w[extras no-store no-cache max-age public private must-revalidate])
91
129
 
92
- DATE = 'Date'.freeze
93
- LAST_MODIFIED = "Last-Modified".freeze
94
- ETAG = "ETag".freeze
95
- CACHE_CONTROL = "Cache-Control".freeze
96
- SPECIAL_KEYS = Set.new(%w[extras no-cache max-age public must-revalidate])
130
+ def generate_weak_etag(validators)
131
+ "W/#{generate_strong_etag(validators)}"
132
+ end
133
+
134
+ def generate_strong_etag(validators)
135
+ %("#{ActiveSupport::Digest.hexdigest(ActiveSupport::Cache.expand_cache_key(validators))}")
136
+ end
97
137
 
98
138
  def cache_control_segments
99
- if cache_control = self[CACHE_CONTROL]
100
- cache_control.delete(' ').split(',')
139
+ if cache_control = _cache_control
140
+ cache_control.delete(" ").split(",")
101
141
  else
102
142
  []
103
143
  end
@@ -107,11 +147,11 @@ module ActionDispatch
107
147
  cache_control = {}
108
148
 
109
149
  cache_control_segments.each do |segment|
110
- directive, argument = segment.split('=', 2)
150
+ directive, argument = segment.split("=", 2)
111
151
 
112
152
  if SPECIAL_KEYS.include? directive
113
- key = directive.tr('-', '_')
114
- cache_control[key.to_sym] = argument || true
153
+ directive.tr!("-", "_")
154
+ cache_control[directive.to_sym] = argument || true
115
155
  else
116
156
  cache_control[:extras] ||= []
117
157
  cache_control[:extras] << segment
@@ -123,51 +163,62 @@ module ActionDispatch
123
163
 
124
164
  def prepare_cache_control!
125
165
  @cache_control = cache_control_headers
126
- @etag = self[ETAG]
127
166
  end
128
167
 
168
+ DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate"
169
+ NO_STORE = "no-store"
170
+ NO_CACHE = "no-cache"
171
+ PUBLIC = "public"
172
+ PRIVATE = "private"
173
+ MUST_REVALIDATE = "must-revalidate"
174
+
129
175
  def handle_conditional_get!
130
- if etag? || last_modified? || !@cache_control.empty?
131
- set_conditional_cache_control!
176
+ # Normally default cache control setting is handled by ETag
177
+ # middleware. But, if an etag is already set, the middleware
178
+ # defaults to `no-cache` unless a default `Cache-Control` value is
179
+ # previously set. So, set a default one here.
180
+ if (etag? || last_modified?) && !self._cache_control
181
+ self._cache_control = DEFAULT_CACHE_CONTROL
132
182
  end
133
183
  end
134
184
 
135
- DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate".freeze
136
- NO_CACHE = "no-cache".freeze
137
- PUBLIC = "public".freeze
138
- PRIVATE = "private".freeze
139
- MUST_REVALIDATE = "must-revalidate".freeze
185
+ def merge_and_normalize_cache_control!(cache_control)
186
+ control = cache_control_headers
140
187
 
141
- def set_conditional_cache_control!
142
- control = {}
143
- cc_headers = cache_control_headers
144
- if extras = cc_headers.delete(:extras)
145
- @cache_control[:extras] ||= []
146
- @cache_control[:extras] += extras
147
- @cache_control[:extras].uniq!
188
+ return if control.empty? && cache_control.empty? # Let middleware handle default behavior
189
+
190
+ if extras = control.delete(:extras)
191
+ cache_control[:extras] ||= []
192
+ cache_control[:extras] += extras
193
+ cache_control[:extras].uniq!
148
194
  end
149
195
 
150
- control.merge! cc_headers
151
- control.merge! @cache_control
196
+ control.merge! cache_control
152
197
 
153
- if control.empty?
154
- headers[CACHE_CONTROL] = DEFAULT_CACHE_CONTROL
198
+ if control[:no_store]
199
+ self._cache_control = NO_STORE
155
200
  elsif control[:no_cache]
156
- headers[CACHE_CONTROL] = NO_CACHE
157
- if control[:extras]
158
- headers[CACHE_CONTROL] += ", #{control[:extras].join(', ')}"
159
- end
201
+ options = []
202
+ options << PUBLIC if control[:public]
203
+ options << NO_CACHE
204
+ options.concat(control[:extras]) if control[:extras]
205
+
206
+ self._cache_control = options.join(", ")
160
207
  else
161
- extras = control[:extras]
208
+ extras = control[:extras]
162
209
  max_age = control[:max_age]
210
+ stale_while_revalidate = control[:stale_while_revalidate]
211
+ stale_if_error = control[:stale_if_error]
163
212
 
164
213
  options = []
165
214
  options << "max-age=#{max_age.to_i}" if max_age
166
215
  options << (control[:public] ? PUBLIC : PRIVATE)
167
216
  options << MUST_REVALIDATE if control[:must_revalidate]
217
+ options << "stale-while-revalidate=#{stale_while_revalidate.to_i}" if stale_while_revalidate
218
+ options << "stale-if-error=#{stale_if_error.to_i}" if stale_if_error
168
219
  options.concat(extras) if extras
169
220
 
170
- headers[CACHE_CONTROL] = options.join(", ")
221
+ self._cache_control = options.join(", ")
171
222
  end
172
223
  end
173
224
  end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionDispatch
4
+ module Http
5
+ class ContentDisposition # :nodoc:
6
+ def self.format(disposition:, filename:)
7
+ new(disposition: disposition, filename: filename).to_s
8
+ end
9
+
10
+ attr_reader :disposition, :filename
11
+
12
+ def initialize(disposition:, filename:)
13
+ @disposition = disposition
14
+ @filename = filename
15
+ end
16
+
17
+ TRADITIONAL_ESCAPED_CHAR = /[^ A-Za-z0-9!\#$+.^_`|~-]/
18
+
19
+ def ascii_filename
20
+ 'filename="' + percent_escape(I18n.transliterate(filename), TRADITIONAL_ESCAPED_CHAR) + '"'
21
+ end
22
+
23
+ RFC_5987_ESCAPED_CHAR = /[^A-Za-z0-9!\#$&+.^_`|~-]/
24
+
25
+ def utf8_filename
26
+ "filename*=UTF-8''" + percent_escape(filename, RFC_5987_ESCAPED_CHAR)
27
+ end
28
+
29
+ def to_s
30
+ if filename
31
+ "#{disposition}; #{ascii_filename}; #{utf8_filename}"
32
+ else
33
+ "#{disposition}"
34
+ end
35
+ end
36
+
37
+ private
38
+ def percent_escape(string, pattern)
39
+ string.gsub(pattern) do |char|
40
+ char.bytes.map { |byte| "%%%02X" % byte }.join
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,286 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/object/deep_dup"
4
+
5
+ module ActionDispatch #:nodoc:
6
+ class ContentSecurityPolicy
7
+ class Middleware
8
+ CONTENT_TYPE = "Content-Type"
9
+ POLICY = "Content-Security-Policy"
10
+ POLICY_REPORT_ONLY = "Content-Security-Policy-Report-Only"
11
+
12
+ def initialize(app)
13
+ @app = app
14
+ end
15
+
16
+ def call(env)
17
+ request = ActionDispatch::Request.new env
18
+ _, headers, _ = response = @app.call(env)
19
+
20
+ return response unless html_response?(headers)
21
+ return response if policy_present?(headers)
22
+
23
+ if policy = request.content_security_policy
24
+ nonce = request.content_security_policy_nonce
25
+ nonce_directives = request.content_security_policy_nonce_directives
26
+ context = request.controller_instance || request
27
+ headers[header_name(request)] = policy.build(context, nonce, nonce_directives)
28
+ end
29
+
30
+ response
31
+ end
32
+
33
+ private
34
+ def html_response?(headers)
35
+ if content_type = headers[CONTENT_TYPE]
36
+ /html/.match?(content_type)
37
+ end
38
+ end
39
+
40
+ def header_name(request)
41
+ if request.content_security_policy_report_only
42
+ POLICY_REPORT_ONLY
43
+ else
44
+ POLICY
45
+ end
46
+ end
47
+
48
+ def policy_present?(headers)
49
+ headers[POLICY] || headers[POLICY_REPORT_ONLY]
50
+ end
51
+ end
52
+
53
+ module Request
54
+ POLICY = "action_dispatch.content_security_policy"
55
+ POLICY_REPORT_ONLY = "action_dispatch.content_security_policy_report_only"
56
+ NONCE_GENERATOR = "action_dispatch.content_security_policy_nonce_generator"
57
+ NONCE = "action_dispatch.content_security_policy_nonce"
58
+ NONCE_DIRECTIVES = "action_dispatch.content_security_policy_nonce_directives"
59
+
60
+ def content_security_policy
61
+ get_header(POLICY)
62
+ end
63
+
64
+ def content_security_policy=(policy)
65
+ set_header(POLICY, policy)
66
+ end
67
+
68
+ def content_security_policy_report_only
69
+ get_header(POLICY_REPORT_ONLY)
70
+ end
71
+
72
+ def content_security_policy_report_only=(value)
73
+ set_header(POLICY_REPORT_ONLY, value)
74
+ end
75
+
76
+ def content_security_policy_nonce_generator
77
+ get_header(NONCE_GENERATOR)
78
+ end
79
+
80
+ def content_security_policy_nonce_generator=(generator)
81
+ set_header(NONCE_GENERATOR, generator)
82
+ end
83
+
84
+ def content_security_policy_nonce_directives
85
+ get_header(NONCE_DIRECTIVES)
86
+ end
87
+
88
+ def content_security_policy_nonce_directives=(generator)
89
+ set_header(NONCE_DIRECTIVES, generator)
90
+ end
91
+
92
+ def content_security_policy_nonce
93
+ if content_security_policy_nonce_generator
94
+ if nonce = get_header(NONCE)
95
+ nonce
96
+ else
97
+ set_header(NONCE, generate_content_security_policy_nonce)
98
+ end
99
+ end
100
+ end
101
+
102
+ private
103
+ def generate_content_security_policy_nonce
104
+ content_security_policy_nonce_generator.call(self)
105
+ end
106
+ end
107
+
108
+ MAPPINGS = {
109
+ self: "'self'",
110
+ unsafe_eval: "'unsafe-eval'",
111
+ unsafe_inline: "'unsafe-inline'",
112
+ none: "'none'",
113
+ http: "http:",
114
+ https: "https:",
115
+ data: "data:",
116
+ mediastream: "mediastream:",
117
+ blob: "blob:",
118
+ filesystem: "filesystem:",
119
+ report_sample: "'report-sample'",
120
+ strict_dynamic: "'strict-dynamic'",
121
+ ws: "ws:",
122
+ wss: "wss:"
123
+ }.freeze
124
+
125
+ DIRECTIVES = {
126
+ base_uri: "base-uri",
127
+ child_src: "child-src",
128
+ connect_src: "connect-src",
129
+ default_src: "default-src",
130
+ font_src: "font-src",
131
+ form_action: "form-action",
132
+ frame_ancestors: "frame-ancestors",
133
+ frame_src: "frame-src",
134
+ img_src: "img-src",
135
+ manifest_src: "manifest-src",
136
+ media_src: "media-src",
137
+ object_src: "object-src",
138
+ prefetch_src: "prefetch-src",
139
+ script_src: "script-src",
140
+ script_src_attr: "script-src-attr",
141
+ script_src_elem: "script-src-elem",
142
+ style_src: "style-src",
143
+ style_src_attr: "style-src-attr",
144
+ style_src_elem: "style-src-elem",
145
+ worker_src: "worker-src"
146
+ }.freeze
147
+
148
+ DEFAULT_NONCE_DIRECTIVES = %w[script-src style-src].freeze
149
+
150
+ private_constant :MAPPINGS, :DIRECTIVES, :DEFAULT_NONCE_DIRECTIVES
151
+
152
+ attr_reader :directives
153
+
154
+ def initialize
155
+ @directives = {}
156
+ yield self if block_given?
157
+ end
158
+
159
+ def initialize_copy(other)
160
+ @directives = other.directives.deep_dup
161
+ end
162
+
163
+ DIRECTIVES.each do |name, directive|
164
+ define_method(name) do |*sources|
165
+ if sources.first
166
+ @directives[directive] = apply_mappings(sources)
167
+ else
168
+ @directives.delete(directive)
169
+ end
170
+ end
171
+ end
172
+
173
+ def block_all_mixed_content(enabled = true)
174
+ if enabled
175
+ @directives["block-all-mixed-content"] = true
176
+ else
177
+ @directives.delete("block-all-mixed-content")
178
+ end
179
+ end
180
+
181
+ def plugin_types(*types)
182
+ if types.first
183
+ @directives["plugin-types"] = types
184
+ else
185
+ @directives.delete("plugin-types")
186
+ end
187
+ end
188
+
189
+ def report_uri(uri)
190
+ @directives["report-uri"] = [uri]
191
+ end
192
+
193
+ def require_sri_for(*types)
194
+ if types.first
195
+ @directives["require-sri-for"] = types
196
+ else
197
+ @directives.delete("require-sri-for")
198
+ end
199
+ end
200
+
201
+ def sandbox(*values)
202
+ if values.empty?
203
+ @directives["sandbox"] = true
204
+ elsif values.first
205
+ @directives["sandbox"] = values
206
+ else
207
+ @directives.delete("sandbox")
208
+ end
209
+ end
210
+
211
+ def upgrade_insecure_requests(enabled = true)
212
+ if enabled
213
+ @directives["upgrade-insecure-requests"] = true
214
+ else
215
+ @directives.delete("upgrade-insecure-requests")
216
+ end
217
+ end
218
+
219
+ def build(context = nil, nonce = nil, nonce_directives = nil)
220
+ nonce_directives = DEFAULT_NONCE_DIRECTIVES if nonce_directives.nil?
221
+ build_directives(context, nonce, nonce_directives).compact.join("; ")
222
+ end
223
+
224
+ private
225
+ def apply_mappings(sources)
226
+ sources.map do |source|
227
+ case source
228
+ when Symbol
229
+ apply_mapping(source)
230
+ when String, Proc
231
+ source
232
+ else
233
+ raise ArgumentError, "Invalid content security policy source: #{source.inspect}"
234
+ end
235
+ end
236
+ end
237
+
238
+ def apply_mapping(source)
239
+ MAPPINGS.fetch(source) do
240
+ raise ArgumentError, "Unknown content security policy source mapping: #{source.inspect}"
241
+ end
242
+ end
243
+
244
+ def build_directives(context, nonce, nonce_directives)
245
+ @directives.map do |directive, sources|
246
+ if sources.is_a?(Array)
247
+ if nonce && nonce_directive?(directive, nonce_directives)
248
+ "#{directive} #{build_directive(sources, context).join(' ')} 'nonce-#{nonce}'"
249
+ else
250
+ "#{directive} #{build_directive(sources, context).join(' ')}"
251
+ end
252
+ elsif sources
253
+ directive
254
+ else
255
+ nil
256
+ end
257
+ end
258
+ end
259
+
260
+ def build_directive(sources, context)
261
+ sources.map { |source| resolve_source(source, context) }
262
+ end
263
+
264
+ def resolve_source(source, context)
265
+ case source
266
+ when String
267
+ source
268
+ when Symbol
269
+ source.to_s
270
+ when Proc
271
+ if context.nil?
272
+ raise RuntimeError, "Missing context for the dynamic content security policy source: #{source.inspect}"
273
+ else
274
+ resolved = context.instance_exec(&source)
275
+ resolved.is_a?(Symbol) ? apply_mapping(resolved) : resolved
276
+ end
277
+ else
278
+ raise RuntimeError, "Unexpected content security policy source: #{source.inspect}"
279
+ end
280
+ end
281
+
282
+ def nonce_directive?(directive, nonce_directives)
283
+ nonce_directives.include?(directive)
284
+ end
285
+ end
286
+ end