actionpack 4.2.8 → 5.2.4.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 (166) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +285 -444
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +6 -7
  5. data/lib/abstract_controller.rb +12 -5
  6. data/lib/abstract_controller/asset_paths.rb +2 -0
  7. data/lib/abstract_controller/base.rb +45 -49
  8. data/lib/abstract_controller/caching.rb +66 -0
  9. data/lib/{action_controller → abstract_controller}/caching/fragments.rb +78 -15
  10. data/lib/abstract_controller/callbacks.rb +47 -31
  11. data/lib/abstract_controller/collector.rb +8 -11
  12. data/lib/abstract_controller/error.rb +6 -0
  13. data/lib/abstract_controller/helpers.rb +25 -25
  14. data/lib/abstract_controller/logger.rb +2 -0
  15. data/lib/abstract_controller/railties/routes_helpers.rb +4 -2
  16. data/lib/abstract_controller/rendering.rb +42 -41
  17. data/lib/abstract_controller/translation.rb +10 -7
  18. data/lib/abstract_controller/url_for.rb +2 -0
  19. data/lib/action_controller.rb +29 -21
  20. data/lib/action_controller/api.rb +149 -0
  21. data/lib/action_controller/api/api_rendering.rb +16 -0
  22. data/lib/action_controller/base.rb +27 -19
  23. data/lib/action_controller/caching.rb +14 -57
  24. data/lib/action_controller/form_builder.rb +50 -0
  25. data/lib/action_controller/log_subscriber.rb +10 -15
  26. data/lib/action_controller/metal.rb +98 -83
  27. data/lib/action_controller/metal/basic_implicit_render.rb +13 -0
  28. data/lib/action_controller/metal/conditional_get.rb +118 -44
  29. data/lib/action_controller/metal/content_security_policy.rb +52 -0
  30. data/lib/action_controller/metal/cookies.rb +3 -3
  31. data/lib/action_controller/metal/data_streaming.rb +27 -46
  32. data/lib/action_controller/metal/etag_with_flash.rb +18 -0
  33. data/lib/action_controller/metal/etag_with_template_digest.rb +20 -13
  34. data/lib/action_controller/metal/exceptions.rb +8 -14
  35. data/lib/action_controller/metal/flash.rb +4 -3
  36. data/lib/action_controller/metal/force_ssl.rb +23 -21
  37. data/lib/action_controller/metal/head.rb +21 -19
  38. data/lib/action_controller/metal/helpers.rb +24 -14
  39. data/lib/action_controller/metal/http_authentication.rb +64 -57
  40. data/lib/action_controller/metal/implicit_render.rb +62 -8
  41. data/lib/action_controller/metal/instrumentation.rb +19 -21
  42. data/lib/action_controller/metal/live.rb +90 -106
  43. data/lib/action_controller/metal/mime_responds.rb +33 -46
  44. data/lib/action_controller/metal/parameter_encoding.rb +51 -0
  45. data/lib/action_controller/metal/params_wrapper.rb +61 -53
  46. data/lib/action_controller/metal/redirecting.rb +49 -28
  47. data/lib/action_controller/metal/renderers.rb +87 -44
  48. data/lib/action_controller/metal/rendering.rb +72 -50
  49. data/lib/action_controller/metal/request_forgery_protection.rb +203 -92
  50. data/lib/action_controller/metal/rescue.rb +9 -16
  51. data/lib/action_controller/metal/streaming.rb +12 -10
  52. data/lib/action_controller/metal/strong_parameters.rb +582 -165
  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/railtie.rb +28 -10
  56. data/lib/action_controller/railties/helpers.rb +2 -0
  57. data/lib/action_controller/renderer.rb +117 -0
  58. data/lib/action_controller/template_assertions.rb +11 -0
  59. data/lib/action_controller/test_case.rb +280 -411
  60. data/lib/action_dispatch.rb +27 -19
  61. data/lib/action_dispatch/http/cache.rb +93 -47
  62. data/lib/action_dispatch/http/content_security_policy.rb +272 -0
  63. data/lib/action_dispatch/http/filter_parameters.rb +26 -20
  64. data/lib/action_dispatch/http/filter_redirect.rb +10 -11
  65. data/lib/action_dispatch/http/headers.rb +55 -22
  66. data/lib/action_dispatch/http/mime_negotiation.rb +60 -41
  67. data/lib/action_dispatch/http/mime_type.rb +134 -121
  68. data/lib/action_dispatch/http/mime_types.rb +20 -6
  69. data/lib/action_dispatch/http/parameter_filter.rb +25 -11
  70. data/lib/action_dispatch/http/parameters.rb +98 -39
  71. data/lib/action_dispatch/http/rack_cache.rb +2 -0
  72. data/lib/action_dispatch/http/request.rb +200 -118
  73. data/lib/action_dispatch/http/response.rb +225 -110
  74. data/lib/action_dispatch/http/upload.rb +12 -6
  75. data/lib/action_dispatch/http/url.rb +110 -28
  76. data/lib/action_dispatch/journey.rb +7 -5
  77. data/lib/action_dispatch/journey/formatter.rb +55 -32
  78. data/lib/action_dispatch/journey/gtg/builder.rb +7 -5
  79. data/lib/action_dispatch/journey/gtg/simulator.rb +3 -9
  80. data/lib/action_dispatch/journey/gtg/transition_table.rb +17 -16
  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 -1
  84. data/lib/action_dispatch/journey/nfa/transition_table.rb +5 -48
  85. data/lib/action_dispatch/journey/nodes/node.rb +18 -6
  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 +50 -44
  90. data/lib/action_dispatch/journey/route.rb +106 -28
  91. data/lib/action_dispatch/journey/router.rb +35 -23
  92. data/lib/action_dispatch/journey/router/utils.rb +20 -11
  93. data/lib/action_dispatch/journey/routes.rb +18 -16
  94. data/lib/action_dispatch/journey/scanner.rb +18 -15
  95. data/lib/action_dispatch/journey/visitors.rb +99 -52
  96. data/lib/action_dispatch/middleware/callbacks.rb +1 -2
  97. data/lib/action_dispatch/middleware/cookies.rb +304 -193
  98. data/lib/action_dispatch/middleware/debug_exceptions.rb +152 -57
  99. data/lib/action_dispatch/middleware/debug_locks.rb +124 -0
  100. data/lib/action_dispatch/middleware/exception_wrapper.rb +68 -69
  101. data/lib/action_dispatch/middleware/executor.rb +21 -0
  102. data/lib/action_dispatch/middleware/flash.rb +78 -54
  103. data/lib/action_dispatch/middleware/public_exceptions.rb +27 -25
  104. data/lib/action_dispatch/middleware/reloader.rb +5 -91
  105. data/lib/action_dispatch/middleware/remote_ip.rb +41 -31
  106. data/lib/action_dispatch/middleware/request_id.rb +17 -9
  107. data/lib/action_dispatch/middleware/session/abstract_store.rb +41 -25
  108. data/lib/action_dispatch/middleware/session/cache_store.rb +24 -14
  109. data/lib/action_dispatch/middleware/session/cookie_store.rb +72 -67
  110. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +8 -2
  111. data/lib/action_dispatch/middleware/show_exceptions.rb +26 -22
  112. data/lib/action_dispatch/middleware/ssl.rb +114 -36
  113. data/lib/action_dispatch/middleware/stack.rb +31 -44
  114. data/lib/action_dispatch/middleware/static.rb +57 -50
  115. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +2 -14
  116. data/lib/action_dispatch/middleware/templates/rescues/{_source.erb → _source.html.erb} +0 -0
  117. data/lib/action_dispatch/middleware/templates/rescues/_source.text.erb +8 -0
  118. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +21 -0
  119. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +13 -0
  120. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +1 -0
  121. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +1 -1
  122. data/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb +1 -1
  123. data/lib/action_dispatch/middleware/templates/routes/_route.html.erb +4 -4
  124. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +64 -64
  125. data/lib/action_dispatch/railtie.rb +19 -11
  126. data/lib/action_dispatch/request/session.rb +106 -59
  127. data/lib/action_dispatch/request/utils.rb +67 -24
  128. data/lib/action_dispatch/routing.rb +17 -18
  129. data/lib/action_dispatch/routing/endpoint.rb +9 -2
  130. data/lib/action_dispatch/routing/inspector.rb +58 -67
  131. data/lib/action_dispatch/routing/mapper.rb +734 -447
  132. data/lib/action_dispatch/routing/polymorphic_routes.rb +161 -139
  133. data/lib/action_dispatch/routing/redirection.rb +36 -26
  134. data/lib/action_dispatch/routing/route_set.rb +321 -291
  135. data/lib/action_dispatch/routing/routes_proxy.rb +32 -5
  136. data/lib/action_dispatch/routing/url_for.rb +65 -25
  137. data/lib/action_dispatch/system_test_case.rb +147 -0
  138. data/lib/action_dispatch/system_testing/browser.rb +49 -0
  139. data/lib/action_dispatch/system_testing/driver.rb +59 -0
  140. data/lib/action_dispatch/system_testing/server.rb +31 -0
  141. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +96 -0
  142. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +31 -0
  143. data/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb +26 -0
  144. data/lib/action_dispatch/testing/assertion_response.rb +47 -0
  145. data/lib/action_dispatch/testing/assertions.rb +6 -4
  146. data/lib/action_dispatch/testing/assertions/response.rb +45 -20
  147. data/lib/action_dispatch/testing/assertions/routing.rb +30 -26
  148. data/lib/action_dispatch/testing/integration.rb +347 -209
  149. data/lib/action_dispatch/testing/request_encoder.rb +55 -0
  150. data/lib/action_dispatch/testing/test_process.rb +28 -22
  151. data/lib/action_dispatch/testing/test_request.rb +27 -34
  152. data/lib/action_dispatch/testing/test_response.rb +35 -7
  153. data/lib/action_pack.rb +4 -2
  154. data/lib/action_pack/gem_version.rb +5 -3
  155. data/lib/action_pack/version.rb +3 -1
  156. metadata +56 -39
  157. data/lib/action_controller/metal/hide_actions.rb +0 -40
  158. data/lib/action_controller/metal/rack_delegation.rb +0 -32
  159. data/lib/action_controller/middleware.rb +0 -39
  160. data/lib/action_controller/model_naming.rb +0 -12
  161. data/lib/action_dispatch/journey/backwards.rb +0 -5
  162. data/lib/action_dispatch/journey/router/strexp.rb +0 -27
  163. data/lib/action_dispatch/middleware/params_parser.rb +0 -60
  164. data/lib/action_dispatch/testing/assertions/dom.rb +0 -3
  165. data/lib/action_dispatch/testing/assertions/selector.rb +0 -3
  166. data/lib/action_dispatch/testing/assertions/tag.rb +0 -3
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  #--
2
- # Copyright (c) 2004-2014 David Heinemeier Hansson
4
+ # Copyright (c) 2004-2018 David Heinemeier Hansson
3
5
  #
4
6
  # Permission is hereby granted, free of charge, to any person obtaining
5
7
  # a copy of this software and associated documentation files (the
@@ -21,15 +23,15 @@
21
23
  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
24
  #++
23
25
 
24
- require 'active_support'
25
- require 'active_support/rails'
26
- require 'active_support/core_ext/module/attribute_accessors'
26
+ require "active_support"
27
+ require "active_support/rails"
28
+ require "active_support/core_ext/module/attribute_accessors"
27
29
 
28
- require 'action_pack'
29
- require 'rack'
30
+ require "action_pack"
31
+ require "rack"
30
32
 
31
33
  module Rack
32
- autoload :Test, 'rack/test'
34
+ autoload :Test, "rack/test"
33
35
  end
34
36
 
35
37
  module ActionDispatch
@@ -39,20 +41,22 @@ module ActionDispatch
39
41
  end
40
42
 
41
43
  eager_autoload do
42
- autoload_under 'http' do
44
+ autoload_under "http" do
45
+ autoload :ContentSecurityPolicy
43
46
  autoload :Request
44
47
  autoload :Response
45
48
  end
46
49
  end
47
50
 
48
- autoload_under 'middleware' do
51
+ autoload_under "middleware" do
49
52
  autoload :RequestId
50
53
  autoload :Callbacks
51
54
  autoload :Cookies
52
55
  autoload :DebugExceptions
56
+ autoload :DebugLocks
53
57
  autoload :ExceptionWrapper
58
+ autoload :Executor
54
59
  autoload :Flash
55
- autoload :ParamsParser
56
60
  autoload :PublicExceptions
57
61
  autoload :Reloader
58
62
  autoload :RemoteIp
@@ -62,7 +66,7 @@ module ActionDispatch
62
66
  end
63
67
 
64
68
  autoload :Journey
65
- autoload :MiddlewareStack, 'action_dispatch/middleware/stack'
69
+ autoload :MiddlewareStack, "action_dispatch/middleware/stack"
66
70
  autoload :Routing
67
71
 
68
72
  module Http
@@ -74,30 +78,34 @@ module ActionDispatch
74
78
  autoload :Parameters
75
79
  autoload :ParameterFilter
76
80
  autoload :Upload
77
- autoload :UploadedFile, 'action_dispatch/http/upload'
81
+ autoload :UploadedFile, "action_dispatch/http/upload"
78
82
  autoload :URL
79
83
  end
80
84
 
81
85
  module Session
82
- autoload :AbstractStore, 'action_dispatch/middleware/session/abstract_store'
83
- autoload :CookieStore, 'action_dispatch/middleware/session/cookie_store'
84
- autoload :MemCacheStore, 'action_dispatch/middleware/session/mem_cache_store'
85
- autoload :CacheStore, 'action_dispatch/middleware/session/cache_store'
86
+ autoload :AbstractStore, "action_dispatch/middleware/session/abstract_store"
87
+ autoload :AbstractSecureStore, "action_dispatch/middleware/session/abstract_store"
88
+ autoload :CookieStore, "action_dispatch/middleware/session/cookie_store"
89
+ autoload :MemCacheStore, "action_dispatch/middleware/session/mem_cache_store"
90
+ autoload :CacheStore, "action_dispatch/middleware/session/cache_store"
86
91
  end
87
92
 
88
93
  mattr_accessor :test_app
89
94
 
90
- autoload_under 'testing' do
95
+ autoload_under "testing" do
91
96
  autoload :Assertions
92
97
  autoload :Integration
93
- autoload :IntegrationTest, 'action_dispatch/testing/integration'
98
+ autoload :IntegrationTest, "action_dispatch/testing/integration"
94
99
  autoload :TestProcess
95
100
  autoload :TestRequest
96
101
  autoload :TestResponse
102
+ autoload :AssertionResponse
97
103
  end
104
+
105
+ autoload :SystemTestCase, "action_dispatch/system_test_case"
98
106
  end
99
107
 
100
- autoload :Mime, 'action_dispatch/http/mime_type'
108
+ autoload :Mime, "action_dispatch/http/mime_type"
101
109
 
102
110
  ActiveSupport.on_load(:action_view) do
103
111
  ActionView::Base.default_formats ||= Mime::SET.symbols
@@ -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".freeze
8
+ HTTP_IF_NONE_MATCH = "HTTP_IF_NONE_MATCH".freeze
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,96 @@ 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
83
103
  end
84
104
 
85
- def etag=(etag)
86
- key = ActiveSupport::Cache.expand_cache_key(etag)
87
- @etag = self[ETAG] = %("#{Digest::MD5.hexdigest(key)}")
105
+ def weak_etag=(weak_validators)
106
+ set_header "ETag", generate_weak_etag(weak_validators)
107
+ end
108
+
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
91
126
 
92
- DATE = 'Date'.freeze
127
+ DATE = "Date".freeze
93
128
  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])
129
+ SPECIAL_KEYS = Set.new(%w[extras no-cache max-age public private must-revalidate])
130
+
131
+ def generate_weak_etag(validators)
132
+ "W/#{generate_strong_etag(validators)}"
133
+ end
134
+
135
+ def generate_strong_etag(validators)
136
+ %("#{ActiveSupport::Digest.hexdigest(ActiveSupport::Cache.expand_cache_key(validators))}")
137
+ end
97
138
 
98
139
  def cache_control_segments
99
- if cache_control = self[CACHE_CONTROL]
100
- cache_control.delete(' ').split(',')
140
+ if cache_control = _cache_control
141
+ cache_control.delete(" ").split(",")
101
142
  else
102
143
  []
103
144
  end
@@ -107,10 +148,10 @@ module ActionDispatch
107
148
  cache_control = {}
108
149
 
109
150
  cache_control_segments.each do |segment|
110
- directive, argument = segment.split('=', 2)
151
+ directive, argument = segment.split("=", 2)
111
152
 
112
153
  if SPECIAL_KEYS.include? directive
113
- key = directive.tr('-', '_')
154
+ key = directive.tr("-", "_")
114
155
  cache_control[key.to_sym] = argument || true
115
156
  else
116
157
  cache_control[:extras] ||= []
@@ -123,13 +164,6 @@ module ActionDispatch
123
164
 
124
165
  def prepare_cache_control!
125
166
  @cache_control = cache_control_headers
126
- @etag = self[ETAG]
127
- end
128
-
129
- def handle_conditional_get!
130
- if etag? || last_modified? || !@cache_control.empty?
131
- set_conditional_cache_control!
132
- end
133
167
  end
134
168
 
135
169
  DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate".freeze
@@ -138,25 +172,37 @@ module ActionDispatch
138
172
  PRIVATE = "private".freeze
139
173
  MUST_REVALIDATE = "must-revalidate".freeze
140
174
 
141
- def set_conditional_cache_control!
175
+ def handle_conditional_get!
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
182
+ end
183
+ end
184
+
185
+ def merge_and_normalize_cache_control!(cache_control)
142
186
  control = {}
143
187
  cc_headers = cache_control_headers
144
188
  if extras = cc_headers.delete(:extras)
145
- @cache_control[:extras] ||= []
146
- @cache_control[:extras] += extras
147
- @cache_control[:extras].uniq!
189
+ cache_control[:extras] ||= []
190
+ cache_control[:extras] += extras
191
+ cache_control[:extras].uniq!
148
192
  end
149
193
 
150
194
  control.merge! cc_headers
151
- control.merge! @cache_control
195
+ control.merge! cache_control
152
196
 
153
197
  if control.empty?
154
- headers[CACHE_CONTROL] = DEFAULT_CACHE_CONTROL
198
+ # Let middleware handle default behavior
155
199
  elsif control[:no_cache]
156
- headers[CACHE_CONTROL] = NO_CACHE
157
- if control[:extras]
158
- headers[CACHE_CONTROL] += ", #{control[:extras].join(', ')}"
159
- end
200
+ options = []
201
+ options << PUBLIC if control[:public]
202
+ options << NO_CACHE
203
+ options.concat(control[:extras]) if control[:extras]
204
+
205
+ self._cache_control = options.join(", ")
160
206
  else
161
207
  extras = control[:extras]
162
208
  max_age = control[:max_age]
@@ -167,7 +213,7 @@ module ActionDispatch
167
213
  options << MUST_REVALIDATE if control[:must_revalidate]
168
214
  options.concat(extras) if extras
169
215
 
170
- headers[CACHE_CONTROL] = options.join(", ")
216
+ self._cache_control = options.join(", ")
171
217
  end
172
218
  end
173
219
  end
@@ -0,0 +1,272 @@
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".freeze
9
+ POLICY = "Content-Security-Policy".freeze
10
+ POLICY_REPORT_ONLY = "Content-Security-Policy-Report-Only".freeze
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
+ context = request.controller_instance || request
26
+ headers[header_name(request)] = policy.build(context, nonce)
27
+ end
28
+
29
+ response
30
+ end
31
+
32
+ private
33
+
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".freeze
55
+ POLICY_REPORT_ONLY = "action_dispatch.content_security_policy_report_only".freeze
56
+ NONCE_GENERATOR = "action_dispatch.content_security_policy_nonce_generator".freeze
57
+ NONCE = "action_dispatch.content_security_policy_nonce".freeze
58
+
59
+ def content_security_policy
60
+ get_header(POLICY)
61
+ end
62
+
63
+ def content_security_policy=(policy)
64
+ set_header(POLICY, policy)
65
+ end
66
+
67
+ def content_security_policy_report_only
68
+ get_header(POLICY_REPORT_ONLY)
69
+ end
70
+
71
+ def content_security_policy_report_only=(value)
72
+ set_header(POLICY_REPORT_ONLY, value)
73
+ end
74
+
75
+ def content_security_policy_nonce_generator
76
+ get_header(NONCE_GENERATOR)
77
+ end
78
+
79
+ def content_security_policy_nonce_generator=(generator)
80
+ set_header(NONCE_GENERATOR, generator)
81
+ end
82
+
83
+ def content_security_policy_nonce
84
+ if content_security_policy_nonce_generator
85
+ if nonce = get_header(NONCE)
86
+ nonce
87
+ else
88
+ set_header(NONCE, generate_content_security_policy_nonce)
89
+ end
90
+ end
91
+ end
92
+
93
+ private
94
+
95
+ def generate_content_security_policy_nonce
96
+ content_security_policy_nonce_generator.call(self)
97
+ end
98
+ end
99
+
100
+ MAPPINGS = {
101
+ self: "'self'",
102
+ unsafe_eval: "'unsafe-eval'",
103
+ unsafe_inline: "'unsafe-inline'",
104
+ none: "'none'",
105
+ http: "http:",
106
+ https: "https:",
107
+ data: "data:",
108
+ mediastream: "mediastream:",
109
+ blob: "blob:",
110
+ filesystem: "filesystem:",
111
+ report_sample: "'report-sample'",
112
+ strict_dynamic: "'strict-dynamic'",
113
+ ws: "ws:",
114
+ wss: "wss:"
115
+ }.freeze
116
+
117
+ DIRECTIVES = {
118
+ base_uri: "base-uri",
119
+ child_src: "child-src",
120
+ connect_src: "connect-src",
121
+ default_src: "default-src",
122
+ font_src: "font-src",
123
+ form_action: "form-action",
124
+ frame_ancestors: "frame-ancestors",
125
+ frame_src: "frame-src",
126
+ img_src: "img-src",
127
+ manifest_src: "manifest-src",
128
+ media_src: "media-src",
129
+ object_src: "object-src",
130
+ script_src: "script-src",
131
+ style_src: "style-src",
132
+ worker_src: "worker-src"
133
+ }.freeze
134
+
135
+ NONCE_DIRECTIVES = %w[script-src].freeze
136
+
137
+ private_constant :MAPPINGS, :DIRECTIVES, :NONCE_DIRECTIVES
138
+
139
+ attr_reader :directives
140
+
141
+ def initialize
142
+ @directives = {}
143
+ yield self if block_given?
144
+ end
145
+
146
+ def initialize_copy(other)
147
+ @directives = other.directives.deep_dup
148
+ end
149
+
150
+ DIRECTIVES.each do |name, directive|
151
+ define_method(name) do |*sources|
152
+ if sources.first
153
+ @directives[directive] = apply_mappings(sources)
154
+ else
155
+ @directives.delete(directive)
156
+ end
157
+ end
158
+ end
159
+
160
+ def block_all_mixed_content(enabled = true)
161
+ if enabled
162
+ @directives["block-all-mixed-content"] = true
163
+ else
164
+ @directives.delete("block-all-mixed-content")
165
+ end
166
+ end
167
+
168
+ def plugin_types(*types)
169
+ if types.first
170
+ @directives["plugin-types"] = types
171
+ else
172
+ @directives.delete("plugin-types")
173
+ end
174
+ end
175
+
176
+ def report_uri(uri)
177
+ @directives["report-uri"] = [uri]
178
+ end
179
+
180
+ def require_sri_for(*types)
181
+ if types.first
182
+ @directives["require-sri-for"] = types
183
+ else
184
+ @directives.delete("require-sri-for")
185
+ end
186
+ end
187
+
188
+ def sandbox(*values)
189
+ if values.empty?
190
+ @directives["sandbox"] = true
191
+ elsif values.first
192
+ @directives["sandbox"] = values
193
+ else
194
+ @directives.delete("sandbox")
195
+ end
196
+ end
197
+
198
+ def upgrade_insecure_requests(enabled = true)
199
+ if enabled
200
+ @directives["upgrade-insecure-requests"] = true
201
+ else
202
+ @directives.delete("upgrade-insecure-requests")
203
+ end
204
+ end
205
+
206
+ def build(context = nil, nonce = nil)
207
+ build_directives(context, nonce).compact.join("; ")
208
+ end
209
+
210
+ private
211
+ def apply_mappings(sources)
212
+ sources.map do |source|
213
+ case source
214
+ when Symbol
215
+ apply_mapping(source)
216
+ when String, Proc
217
+ source
218
+ else
219
+ raise ArgumentError, "Invalid content security policy source: #{source.inspect}"
220
+ end
221
+ end
222
+ end
223
+
224
+ def apply_mapping(source)
225
+ MAPPINGS.fetch(source) do
226
+ raise ArgumentError, "Unknown content security policy source mapping: #{source.inspect}"
227
+ end
228
+ end
229
+
230
+ def build_directives(context, nonce)
231
+ @directives.map do |directive, sources|
232
+ if sources.is_a?(Array)
233
+ if nonce && nonce_directive?(directive)
234
+ "#{directive} #{build_directive(sources, context).join(' ')} 'nonce-#{nonce}'"
235
+ else
236
+ "#{directive} #{build_directive(sources, context).join(' ')}"
237
+ end
238
+ elsif sources
239
+ directive
240
+ else
241
+ nil
242
+ end
243
+ end
244
+ end
245
+
246
+ def build_directive(sources, context)
247
+ sources.map { |source| resolve_source(source, context) }
248
+ end
249
+
250
+ def resolve_source(source, context)
251
+ case source
252
+ when String
253
+ source
254
+ when Symbol
255
+ source.to_s
256
+ when Proc
257
+ if context.nil?
258
+ raise RuntimeError, "Missing context for the dynamic content security policy source: #{source.inspect}"
259
+ else
260
+ resolved = context.instance_exec(&source)
261
+ resolved.is_a?(Symbol) ? apply_mapping(resolved) : resolved
262
+ end
263
+ else
264
+ raise RuntimeError, "Unexpected content security policy source: #{source.inspect}"
265
+ end
266
+ end
267
+
268
+ def nonce_directive?(directive)
269
+ NONCE_DIRECTIVES.include?(directive)
270
+ end
271
+ end
272
+ end