actionpack 4.2.11.1 → 6.0.3

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 (182) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +212 -526
  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 +47 -50
  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 +59 -31
  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 +31 -30
  13. data/lib/abstract_controller/logger.rb +2 -0
  14. data/lib/abstract_controller/railties/routes_helpers.rb +5 -3
  15. data/lib/abstract_controller/rendering.rb +42 -41
  16. data/lib/abstract_controller/translation.rb +12 -9
  17. data/lib/abstract_controller/url_for.rb +2 -0
  18. data/lib/abstract_controller.rb +12 -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 +25 -22
  22. data/lib/action_controller/caching.rb +13 -57
  23. data/lib/action_controller/form_builder.rb +50 -0
  24. data/lib/action_controller/log_subscriber.rb +15 -17
  25. data/lib/action_controller/metal/basic_implicit_render.rb +13 -0
  26. data/lib/action_controller/metal/conditional_get.rb +124 -44
  27. data/lib/action_controller/metal/content_security_policy.rb +51 -0
  28. data/lib/action_controller/metal/cookies.rb +3 -3
  29. data/lib/action_controller/metal/data_streaming.rb +29 -49
  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 +20 -13
  33. data/lib/action_controller/metal/exceptions.rb +30 -15
  34. data/lib/action_controller/metal/flash.rb +9 -8
  35. data/lib/action_controller/metal/force_ssl.rb +23 -62
  36. data/lib/action_controller/metal/head.rb +22 -20
  37. data/lib/action_controller/metal/helpers.rb +26 -17
  38. data/lib/action_controller/metal/http_authentication.rb +76 -70
  39. data/lib/action_controller/metal/implicit_render.rb +53 -9
  40. data/lib/action_controller/metal/instrumentation.rb +22 -27
  41. data/lib/action_controller/metal/live.rb +101 -119
  42. data/lib/action_controller/metal/mime_responds.rb +44 -46
  43. data/lib/action_controller/metal/parameter_encoding.rb +51 -0
  44. data/lib/action_controller/metal/params_wrapper.rb +74 -63
  45. data/lib/action_controller/metal/redirecting.rb +53 -32
  46. data/lib/action_controller/metal/renderers.rb +87 -44
  47. data/lib/action_controller/metal/rendering.rb +72 -51
  48. data/lib/action_controller/metal/request_forgery_protection.rb +217 -97
  49. data/lib/action_controller/metal/rescue.rb +9 -16
  50. data/lib/action_controller/metal/streaming.rb +12 -11
  51. data/lib/action_controller/metal/strong_parameters.rb +619 -183
  52. data/lib/action_controller/metal/testing.rb +2 -17
  53. data/lib/action_controller/metal/url_for.rb +19 -10
  54. data/lib/action_controller/metal.rb +104 -87
  55. data/lib/action_controller/railtie.rb +28 -10
  56. data/lib/action_controller/railties/helpers.rb +3 -1
  57. data/lib/action_controller/renderer.rb +130 -0
  58. data/lib/action_controller/template_assertions.rb +11 -0
  59. data/lib/action_controller/test_case.rb +286 -418
  60. data/lib/action_controller.rb +33 -21
  61. data/lib/action_dispatch/http/cache.rb +100 -51
  62. data/lib/action_dispatch/http/content_disposition.rb +45 -0
  63. data/lib/action_dispatch/http/content_security_policy.rb +282 -0
  64. data/lib/action_dispatch/http/filter_parameters.rb +31 -24
  65. data/lib/action_dispatch/http/filter_redirect.rb +10 -12
  66. data/lib/action_dispatch/http/headers.rb +54 -22
  67. data/lib/action_dispatch/http/mime_negotiation.rb +61 -45
  68. data/lib/action_dispatch/http/mime_type.rb +141 -122
  69. data/lib/action_dispatch/http/mime_types.rb +20 -6
  70. data/lib/action_dispatch/http/parameter_filter.rb +8 -68
  71. data/lib/action_dispatch/http/parameters.rb +107 -39
  72. data/lib/action_dispatch/http/rack_cache.rb +2 -0
  73. data/lib/action_dispatch/http/request.rb +204 -117
  74. data/lib/action_dispatch/http/response.rb +248 -114
  75. data/lib/action_dispatch/http/upload.rb +21 -7
  76. data/lib/action_dispatch/http/url.rb +181 -100
  77. data/lib/action_dispatch/journey/formatter.rb +56 -34
  78. data/lib/action_dispatch/journey/gtg/builder.rb +7 -6
  79. data/lib/action_dispatch/journey/gtg/simulator.rb +3 -9
  80. data/lib/action_dispatch/journey/gtg/transition_table.rb +17 -17
  81. data/lib/action_dispatch/journey/nfa/builder.rb +5 -3
  82. data/lib/action_dispatch/journey/nfa/dot.rb +13 -13
  83. data/lib/action_dispatch/journey/nfa/simulator.rb +3 -3
  84. data/lib/action_dispatch/journey/nfa/transition_table.rb +5 -49
  85. data/lib/action_dispatch/journey/nodes/node.rb +25 -12
  86. data/lib/action_dispatch/journey/parser.rb +23 -22
  87. data/lib/action_dispatch/journey/parser.y +3 -2
  88. data/lib/action_dispatch/journey/parser_extras.rb +12 -4
  89. data/lib/action_dispatch/journey/path/pattern.rb +55 -46
  90. data/lib/action_dispatch/journey/route.rb +107 -28
  91. data/lib/action_dispatch/journey/router/utils.rb +25 -16
  92. data/lib/action_dispatch/journey/router.rb +35 -27
  93. data/lib/action_dispatch/journey/routes.rb +17 -17
  94. data/lib/action_dispatch/journey/scanner.rb +26 -17
  95. data/lib/action_dispatch/journey/visitors.rb +98 -54
  96. data/lib/action_dispatch/journey.rb +7 -5
  97. data/lib/action_dispatch/middleware/actionable_exceptions.rb +39 -0
  98. data/lib/action_dispatch/middleware/callbacks.rb +3 -6
  99. data/lib/action_dispatch/middleware/cookies.rb +292 -203
  100. data/lib/action_dispatch/middleware/debug_exceptions.rb +142 -63
  101. data/lib/action_dispatch/middleware/debug_locks.rb +124 -0
  102. data/lib/action_dispatch/middleware/debug_view.rb +66 -0
  103. data/lib/action_dispatch/middleware/exception_wrapper.rb +102 -70
  104. data/lib/action_dispatch/middleware/executor.rb +21 -0
  105. data/lib/action_dispatch/middleware/flash.rb +78 -54
  106. data/lib/action_dispatch/middleware/host_authorization.rb +101 -0
  107. data/lib/action_dispatch/middleware/public_exceptions.rb +32 -27
  108. data/lib/action_dispatch/middleware/reloader.rb +5 -91
  109. data/lib/action_dispatch/middleware/remote_ip.rb +48 -41
  110. data/lib/action_dispatch/middleware/request_id.rb +17 -9
  111. data/lib/action_dispatch/middleware/session/abstract_store.rb +41 -26
  112. data/lib/action_dispatch/middleware/session/cache_store.rb +24 -14
  113. data/lib/action_dispatch/middleware/session/cookie_store.rb +72 -73
  114. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +8 -2
  115. data/lib/action_dispatch/middleware/show_exceptions.rb +26 -23
  116. data/lib/action_dispatch/middleware/ssl.rb +113 -35
  117. data/lib/action_dispatch/middleware/stack.rb +64 -41
  118. data/lib/action_dispatch/middleware/static.rb +57 -51
  119. data/lib/action_dispatch/middleware/templates/rescues/_actions.html.erb +13 -0
  120. data/lib/action_dispatch/middleware/templates/rescues/_actions.text.erb +0 -0
  121. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +4 -14
  122. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb +1 -1
  123. data/lib/action_dispatch/middleware/templates/rescues/{_source.erb → _source.html.erb} +4 -2
  124. data/lib/action_dispatch/middleware/templates/rescues/_source.text.erb +8 -0
  125. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +45 -35
  126. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +7 -0
  127. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb +5 -0
  128. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +26 -4
  129. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +1 -1
  130. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +24 -0
  131. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +15 -0
  132. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +5 -0
  133. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +19 -0
  134. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb +3 -0
  135. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +2 -2
  136. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +1 -1
  137. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +3 -3
  138. data/lib/action_dispatch/middleware/templates/rescues/template_error.text.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 +67 -64
  141. data/lib/action_dispatch/railtie.rb +26 -13
  142. data/lib/action_dispatch/request/session.rb +114 -60
  143. data/lib/action_dispatch/request/utils.rb +67 -24
  144. data/lib/action_dispatch/routing/endpoint.rb +9 -2
  145. data/lib/action_dispatch/routing/inspector.rb +140 -102
  146. data/lib/action_dispatch/routing/mapper.rb +762 -455
  147. data/lib/action_dispatch/routing/polymorphic_routes.rb +161 -142
  148. data/lib/action_dispatch/routing/redirection.rb +36 -26
  149. data/lib/action_dispatch/routing/route_set.rb +322 -298
  150. data/lib/action_dispatch/routing/routes_proxy.rb +32 -5
  151. data/lib/action_dispatch/routing/url_for.rb +65 -26
  152. data/lib/action_dispatch/routing.rb +36 -36
  153. data/lib/action_dispatch/system_test_case.rb +185 -0
  154. data/lib/action_dispatch/system_testing/browser.rb +80 -0
  155. data/lib/action_dispatch/system_testing/driver.rb +68 -0
  156. data/lib/action_dispatch/system_testing/server.rb +31 -0
  157. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +97 -0
  158. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +32 -0
  159. data/lib/action_dispatch/testing/assertion_response.rb +46 -0
  160. data/lib/action_dispatch/testing/assertions/response.rb +44 -20
  161. data/lib/action_dispatch/testing/assertions/routing.rb +44 -28
  162. data/lib/action_dispatch/testing/assertions.rb +6 -4
  163. data/lib/action_dispatch/testing/integration.rb +375 -215
  164. data/lib/action_dispatch/testing/request_encoder.rb +55 -0
  165. data/lib/action_dispatch/testing/test_process.rb +28 -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 +33 -20
  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 +71 -40
  173. data/lib/action_controller/metal/hide_actions.rb +0 -40
  174. data/lib/action_controller/metal/rack_delegation.rb +0 -32
  175. data/lib/action_controller/middleware.rb +0 -39
  176. data/lib/action_controller/model_naming.rb +0 -12
  177. data/lib/action_dispatch/journey/backwards.rb +0 -5
  178. data/lib/action_dispatch/journey/router/strexp.rb +0 -27
  179. data/lib/action_dispatch/middleware/params_parser.rb +0 -60
  180. data/lib/action_dispatch/testing/assertions/dom.rb +0 -3
  181. data/lib/action_dispatch/testing/assertions/selector.rb +0 -3
  182. data/lib/action_dispatch/testing/assertions/tag.rb +0 -3
@@ -1,34 +1,46 @@
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 "active_support/rails"
4
+ require "abstract_controller"
5
+ require "action_dispatch"
6
+ require "action_controller/metal/strong_parameters"
6
7
 
7
8
  module ActionController
8
9
  extend ActiveSupport::Autoload
9
10
 
11
+ autoload :API
10
12
  autoload :Base
11
- autoload :Caching
12
13
  autoload :Metal
13
14
  autoload :Middleware
15
+ autoload :Renderer
16
+ autoload :FormBuilder
17
+
18
+ eager_autoload do
19
+ autoload :Caching
20
+ end
14
21
 
15
22
  autoload_under "metal" do
16
- autoload :Compatibility
23
+ eager_autoload do
24
+ autoload :Live
25
+ end
26
+
17
27
  autoload :ConditionalGet
28
+ autoload :ContentSecurityPolicy
18
29
  autoload :Cookies
19
30
  autoload :DataStreaming
31
+ autoload :DefaultHeaders
20
32
  autoload :EtagWithTemplateDigest
33
+ autoload :EtagWithFlash
21
34
  autoload :Flash
22
35
  autoload :ForceSSL
23
36
  autoload :Head
24
37
  autoload :Helpers
25
- autoload :HideActions
26
38
  autoload :HttpAuthentication
39
+ autoload :BasicImplicitRender
27
40
  autoload :ImplicitRender
28
41
  autoload :Instrumentation
29
42
  autoload :MimeResponds
30
43
  autoload :ParamsWrapper
31
- autoload :RackDelegation
32
44
  autoload :Redirecting
33
45
  autoload :Renderers
34
46
  autoload :Rendering
@@ -36,23 +48,23 @@ module ActionController
36
48
  autoload :Rescue
37
49
  autoload :Streaming
38
50
  autoload :StrongParameters
51
+ autoload :ParameterEncoding
39
52
  autoload :Testing
40
53
  autoload :UrlFor
41
54
  end
42
55
 
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!
56
+ autoload_under "api" do
57
+ autoload :ApiRendering
49
58
  end
59
+
60
+ autoload :TestCase, "action_controller/test_case"
61
+ autoload :TemplateAssertions, "action_controller/test_case"
50
62
  end
51
63
 
52
64
  # 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'
65
+ require "active_support/core_ext/module/attribute_accessors"
66
+ require "active_support/core_ext/load_error"
67
+ require "active_support/core_ext/module/attr_internal"
68
+ require "active_support/core_ext/name_error"
69
+ require "active_support/core_ext/uri"
70
+ 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.starts_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-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,10 +147,10 @@ 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('-', '_')
153
+ key = directive.tr("-", "_")
114
154
  cache_control[key.to_sym] = argument || true
115
155
  else
116
156
  cache_control[:extras] ||= []
@@ -123,51 +163,60 @@ 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_CACHE = "no-cache"
170
+ PUBLIC = "public"
171
+ PRIVATE = "private"
172
+ MUST_REVALIDATE = "must-revalidate"
173
+
129
174
  def handle_conditional_get!
130
- if etag? || last_modified? || !@cache_control.empty?
131
- set_conditional_cache_control!
175
+ # Normally default cache control setting is handled by ETag
176
+ # middleware. But, if an etag is already set, the middleware
177
+ # defaults to `no-cache` unless a default `Cache-Control` value is
178
+ # previously set. So, set a default one here.
179
+ if (etag? || last_modified?) && !self._cache_control
180
+ self._cache_control = DEFAULT_CACHE_CONTROL
132
181
  end
133
182
  end
134
183
 
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
140
-
141
- def set_conditional_cache_control!
184
+ def merge_and_normalize_cache_control!(cache_control)
142
185
  control = {}
143
186
  cc_headers = cache_control_headers
144
187
  if extras = cc_headers.delete(:extras)
145
- @cache_control[:extras] ||= []
146
- @cache_control[:extras] += extras
147
- @cache_control[:extras].uniq!
188
+ cache_control[:extras] ||= []
189
+ cache_control[:extras] += extras
190
+ cache_control[:extras].uniq!
148
191
  end
149
192
 
150
193
  control.merge! cc_headers
151
- control.merge! @cache_control
194
+ control.merge! cache_control
152
195
 
153
196
  if control.empty?
154
- headers[CACHE_CONTROL] = DEFAULT_CACHE_CONTROL
197
+ # Let middleware handle default behavior
155
198
  elsif control[:no_cache]
156
- headers[CACHE_CONTROL] = NO_CACHE
157
- if control[:extras]
158
- headers[CACHE_CONTROL] += ", #{control[:extras].join(', ')}"
159
- end
199
+ options = []
200
+ options << PUBLIC if control[:public]
201
+ options << NO_CACHE
202
+ options.concat(control[:extras]) if control[:extras]
203
+
204
+ self._cache_control = options.join(", ")
160
205
  else
161
- extras = control[:extras]
206
+ extras = control[:extras]
162
207
  max_age = control[:max_age]
208
+ stale_while_revalidate = control[:stale_while_revalidate]
209
+ stale_if_error = control[:stale_if_error]
163
210
 
164
211
  options = []
165
212
  options << "max-age=#{max_age.to_i}" if max_age
166
213
  options << (control[:public] ? PUBLIC : PRIVATE)
167
214
  options << MUST_REVALIDATE if control[:must_revalidate]
215
+ options << "stale-while-revalidate=#{stale_while_revalidate.to_i}" if stale_while_revalidate
216
+ options << "stale-if-error=#{stale_if_error.to_i}" if stale_if_error
168
217
  options.concat(extras) if extras
169
218
 
170
- headers[CACHE_CONTROL] = options.join(", ")
219
+ self._cache_control = options.join(", ")
171
220
  end
172
221
  end
173
222
  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,282 @@
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
+ content_type =~ /html/
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
+ style_src: "style-src",
141
+ worker_src: "worker-src"
142
+ }.freeze
143
+
144
+ DEFAULT_NONCE_DIRECTIVES = %w[script-src style-src].freeze
145
+
146
+ private_constant :MAPPINGS, :DIRECTIVES, :DEFAULT_NONCE_DIRECTIVES
147
+
148
+ attr_reader :directives
149
+
150
+ def initialize
151
+ @directives = {}
152
+ yield self if block_given?
153
+ end
154
+
155
+ def initialize_copy(other)
156
+ @directives = other.directives.deep_dup
157
+ end
158
+
159
+ DIRECTIVES.each do |name, directive|
160
+ define_method(name) do |*sources|
161
+ if sources.first
162
+ @directives[directive] = apply_mappings(sources)
163
+ else
164
+ @directives.delete(directive)
165
+ end
166
+ end
167
+ end
168
+
169
+ def block_all_mixed_content(enabled = true)
170
+ if enabled
171
+ @directives["block-all-mixed-content"] = true
172
+ else
173
+ @directives.delete("block-all-mixed-content")
174
+ end
175
+ end
176
+
177
+ def plugin_types(*types)
178
+ if types.first
179
+ @directives["plugin-types"] = types
180
+ else
181
+ @directives.delete("plugin-types")
182
+ end
183
+ end
184
+
185
+ def report_uri(uri)
186
+ @directives["report-uri"] = [uri]
187
+ end
188
+
189
+ def require_sri_for(*types)
190
+ if types.first
191
+ @directives["require-sri-for"] = types
192
+ else
193
+ @directives.delete("require-sri-for")
194
+ end
195
+ end
196
+
197
+ def sandbox(*values)
198
+ if values.empty?
199
+ @directives["sandbox"] = true
200
+ elsif values.first
201
+ @directives["sandbox"] = values
202
+ else
203
+ @directives.delete("sandbox")
204
+ end
205
+ end
206
+
207
+ def upgrade_insecure_requests(enabled = true)
208
+ if enabled
209
+ @directives["upgrade-insecure-requests"] = true
210
+ else
211
+ @directives.delete("upgrade-insecure-requests")
212
+ end
213
+ end
214
+
215
+ def build(context = nil, nonce = nil, nonce_directives = nil)
216
+ nonce_directives = DEFAULT_NONCE_DIRECTIVES if nonce_directives.nil?
217
+ build_directives(context, nonce, nonce_directives).compact.join("; ")
218
+ end
219
+
220
+ private
221
+ def apply_mappings(sources)
222
+ sources.map do |source|
223
+ case source
224
+ when Symbol
225
+ apply_mapping(source)
226
+ when String, Proc
227
+ source
228
+ else
229
+ raise ArgumentError, "Invalid content security policy source: #{source.inspect}"
230
+ end
231
+ end
232
+ end
233
+
234
+ def apply_mapping(source)
235
+ MAPPINGS.fetch(source) do
236
+ raise ArgumentError, "Unknown content security policy source mapping: #{source.inspect}"
237
+ end
238
+ end
239
+
240
+ def build_directives(context, nonce, nonce_directives)
241
+ @directives.map do |directive, sources|
242
+ if sources.is_a?(Array)
243
+ if nonce && nonce_directive?(directive, nonce_directives)
244
+ "#{directive} #{build_directive(sources, context).join(' ')} 'nonce-#{nonce}'"
245
+ else
246
+ "#{directive} #{build_directive(sources, context).join(' ')}"
247
+ end
248
+ elsif sources
249
+ directive
250
+ else
251
+ nil
252
+ end
253
+ end
254
+ end
255
+
256
+ def build_directive(sources, context)
257
+ sources.map { |source| resolve_source(source, context) }
258
+ end
259
+
260
+ def resolve_source(source, context)
261
+ case source
262
+ when String
263
+ source
264
+ when Symbol
265
+ source.to_s
266
+ when Proc
267
+ if context.nil?
268
+ raise RuntimeError, "Missing context for the dynamic content security policy source: #{source.inspect}"
269
+ else
270
+ resolved = context.instance_exec(&source)
271
+ resolved.is_a?(Symbol) ? apply_mapping(resolved) : resolved
272
+ end
273
+ else
274
+ raise RuntimeError, "Unexpected content security policy source: #{source.inspect}"
275
+ end
276
+ end
277
+
278
+ def nonce_directive?(directive, nonce_directives)
279
+ nonce_directives.include?(directive)
280
+ end
281
+ end
282
+ end