actionpack 5.0.0.beta3 → 5.0.0.beta4

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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +101 -16
  3. data/lib/abstract_controller/base.rb +2 -4
  4. data/lib/abstract_controller/error.rb +4 -0
  5. data/lib/abstract_controller/helpers.rb +2 -1
  6. data/lib/abstract_controller/rendering.rb +1 -0
  7. data/lib/action_controller/api.rb +20 -19
  8. data/lib/action_controller/metal/basic_implicit_render.rb +1 -1
  9. data/lib/action_controller/metal/conditional_get.rb +52 -21
  10. data/lib/action_controller/metal/cookies.rb +1 -1
  11. data/lib/action_controller/metal/data_streaming.rb +9 -10
  12. data/lib/action_controller/metal/force_ssl.rb +4 -4
  13. data/lib/action_controller/metal/http_authentication.rb +8 -3
  14. data/lib/action_controller/metal/implicit_render.rb +55 -17
  15. data/lib/action_controller/metal/instrumentation.rb +3 -2
  16. data/lib/action_controller/metal/live.rb +2 -2
  17. data/lib/action_controller/metal/mime_responds.rb +1 -1
  18. data/lib/action_controller/metal/redirecting.rb +1 -1
  19. data/lib/action_controller/metal/request_forgery_protection.rb +3 -2
  20. data/lib/action_controller/metal/rescue.rb +6 -2
  21. data/lib/action_controller/metal/strong_parameters.rb +30 -3
  22. data/lib/action_controller/renderer.rb +1 -1
  23. data/lib/action_controller/test_case.rb +2 -2
  24. data/lib/action_dispatch.rb +1 -1
  25. data/lib/action_dispatch/http/cache.rb +49 -15
  26. data/lib/action_dispatch/http/filter_parameters.rb +9 -3
  27. data/lib/action_dispatch/http/headers.rb +2 -2
  28. data/lib/action_dispatch/http/mime_types.rb +1 -1
  29. data/lib/action_dispatch/http/request.rb +0 -1
  30. data/lib/action_dispatch/journey/formatter.rb +7 -2
  31. data/lib/action_dispatch/journey/route.rb +1 -1
  32. data/lib/action_dispatch/middleware/callbacks.rb +10 -1
  33. data/lib/action_dispatch/middleware/exception_wrapper.rb +0 -1
  34. data/lib/action_dispatch/middleware/executor.rb +19 -0
  35. data/lib/action_dispatch/middleware/flash.rb +5 -0
  36. data/lib/action_dispatch/middleware/params_parser.rb +1 -0
  37. data/lib/action_dispatch/middleware/reloader.rb +12 -54
  38. data/lib/action_dispatch/middleware/ssl.rb +19 -3
  39. data/lib/action_dispatch/railtie.rb +2 -0
  40. data/lib/action_dispatch/request/session.rb +16 -10
  41. data/lib/action_dispatch/routing.rb +12 -3
  42. data/lib/action_dispatch/routing/inspector.rb +3 -3
  43. data/lib/action_dispatch/routing/mapper.rb +6 -3
  44. data/lib/action_dispatch/routing/route_set.rb +16 -15
  45. data/lib/action_dispatch/routing/url_for.rb +1 -1
  46. data/lib/action_dispatch/testing/assertions/routing.rb +1 -1
  47. data/lib/action_dispatch/testing/integration.rb +43 -27
  48. data/lib/action_pack/gem_version.rb +1 -1
  49. metadata +12 -11
  50. data/lib/action_dispatch/middleware/load_interlock.rb +0 -21
@@ -25,14 +25,13 @@ module ActionController #:nodoc:
25
25
  # * <tt>:filename</tt> - suggests a filename for the browser to use.
26
26
  # Defaults to <tt>File.basename(path)</tt>.
27
27
  # * <tt>:type</tt> - specifies an HTTP content type.
28
- # You can specify either a string or a symbol for a registered type register with
29
- # <tt>Mime::Type.register</tt>, for example :json
30
- # If omitted, type will be guessed from the file extension specified in <tt>:filename</tt>.
31
- # If no content type is registered for the extension, default type 'application/octet-stream' will be used.
28
+ # You can specify either a string or a symbol for a registered type with <tt>Mime::Type.register</tt>, for example :json.
29
+ # If omitted, the type will be inferred from the file extension specified in <tt>:filename</tt>.
30
+ # If no content type is registered for the extension, the default type 'application/octet-stream' will be used.
32
31
  # * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded.
33
32
  # Valid values are 'inline' and 'attachment' (default).
34
33
  # * <tt>:status</tt> - specifies the status code to send with the response. Defaults to 200.
35
- # * <tt>:url_based_filename</tt> - set to +true+ if you want the browser guess the filename from
34
+ # * <tt>:url_based_filename</tt> - set to +true+ if you want the browser to guess the filename from
36
35
  # the URL, which is necessary for i18n filenames on certain browsers
37
36
  # (setting <tt>:filename</tt> overrides this option).
38
37
  #
@@ -79,14 +78,14 @@ module ActionController #:nodoc:
79
78
  # <tt>render plain: data</tt>, but also allows you to specify whether
80
79
  # the browser should display the response as a file attachment (i.e. in a
81
80
  # download dialog) or as inline data. You may also set the content type,
82
- # the apparent file name, and other things.
81
+ # the file name, and other things.
83
82
  #
84
83
  # Options:
85
84
  # * <tt>:filename</tt> - suggests a filename for the browser to use.
86
- # * <tt>:type</tt> - specifies an HTTP content type. Defaults to 'application/octet-stream'. You can specify
87
- # either a string or a symbol for a registered type register with <tt>Mime::Type.register</tt>, for example :json
88
- # If omitted, type will be guessed from the file extension specified in <tt>:filename</tt>.
89
- # If no content type is registered for the extension, default type 'application/octet-stream' will be used.
85
+ # * <tt>:type</tt> - specifies an HTTP content type. Defaults to 'application/octet-stream'.
86
+ # You can specify either a string or a symbol for a registered type with <tt>Mime::Type.register</tt>, for example :json.
87
+ # If omitted, type will be inferred from the file extension specified in <tt>:filename</tt>.
88
+ # If no content type is registered for the extension, the default type 'application/octet-stream' will be used.
90
89
  # * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded.
91
90
  # Valid values are 'inline' and 'attachment' (default).
92
91
  # * <tt>:status</tt> - specifies the status code to send with the response. Defaults to 200.
@@ -2,17 +2,17 @@ require 'active_support/core_ext/hash/except'
2
2
  require 'active_support/core_ext/hash/slice'
3
3
 
4
4
  module ActionController
5
- # This module provides a method which will redirect browser to use HTTPS
5
+ # This module provides a method which will redirect the browser to use HTTPS
6
6
  # protocol. This will ensure that user's sensitive information will be
7
- # transferred safely over the internet. You _should_ always force browser
7
+ # transferred safely over the internet. You _should_ always force the browser
8
8
  # to use HTTPS when you're transferring sensitive information such as
9
9
  # user authentication, account information, or credit card information.
10
10
  #
11
11
  # Note that if you are really concerned about your application security,
12
12
  # you might consider using +config.force_ssl+ in your config file instead.
13
13
  # That will ensure all the data transferred via HTTPS protocol and prevent
14
- # user from getting session hijacked when accessing the site under unsecured
15
- # HTTP protocol.
14
+ # the user from getting their session hijacked when accessing the site over
15
+ # unsecured HTTP protocol.
16
16
  module ForceSSL
17
17
  extend ActiveSupport::Concern
18
18
  include AbstractController::Callbacks
@@ -310,9 +310,9 @@ module ActionController
310
310
  end
311
311
 
312
312
  # Might want a shorter timeout depending on whether the request
313
- # is a PATCH, PUT, or POST, and if client is browser or web service.
313
+ # is a PATCH, PUT, or POST, and if the client is a browser or web service.
314
314
  # Can be much shorter if the Stale directive is implemented. This would
315
- # allow a user to use new nonce without prompting user again for their
315
+ # allow a user to use new nonce without prompting the user again for their
316
316
  # username and password.
317
317
  def validate_nonce(secret_key, request, value, seconds_to_timeout=5*60)
318
318
  return false if value.nil?
@@ -347,7 +347,12 @@ module ActionController
347
347
  # private
348
348
  # def authenticate
349
349
  # authenticate_or_request_with_http_token do |token, options|
350
- # token == TOKEN
350
+ # # Compare the tokens in a time-constant manner, to mitigate
351
+ # # timing attacks.
352
+ # ActiveSupport::SecurityUtils.secure_compare(
353
+ # ::Digest::SHA256.hexdigest(token),
354
+ # ::Digest::SHA256.hexdigest(TOKEN)
355
+ # )
351
356
  # end
352
357
  # end
353
358
  # end
@@ -1,29 +1,62 @@
1
+ require 'active_support/core_ext/string/strip'
2
+
1
3
  module ActionController
4
+ # Handles implicit rendering for a controller action that does not
5
+ # explicitly respond with +render+, +respond_to+, +redirect+, or +head+.
6
+ #
7
+ # For API controllers, the implicit response is always 204 No Content.
8
+ #
9
+ # For all other controllers, we use these heuristics to decide whether to
10
+ # render a template, raise an error for a missing template, or respond with
11
+ # 204 No Content:
12
+ #
13
+ # First, if we DO find a template, it's rendered. Template lookup accounts
14
+ # for the action name, locales, format, variant, template handlers, and more
15
+ # (see +render+ for details).
16
+ #
17
+ # Second, if we DON'T find a template but the controller action does have
18
+ # templates for other formats, variants, etc., then we trust that you meant
19
+ # to provide a template for this response, too, and we raise
20
+ # <tt>ActionController::UnknownFormat</tt> with an explanation.
21
+ #
22
+ # Third, if we DON'T find a template AND the request is a page load in a web
23
+ # browser (technically, a non-XHR GET request for an HTML response) where
24
+ # you reasonably expect to have rendered a template, then we raise
25
+ # <tt>ActionView::UnknownFormat</tt> with an explanation.
26
+ #
27
+ # Finally, if we DON'T find a template AND the request isn't a browser page
28
+ # load, then we implicitly respond with 204 No Content.
2
29
  module ImplicitRender
3
30
 
31
+ # :stopdoc:
4
32
  include BasicImplicitRender
5
33
 
6
- # Renders the template corresponding to the controller action, if it exists.
7
- # The action name, format, and variant are all taken into account.
8
- # For example, the "new" action with an HTML format and variant "phone"
9
- # would try to render the <tt>new.html+phone.erb</tt> template.
10
- #
11
- # If no template is found <tt>ActionController::BasicImplicitRender</tt>'s implementation is called, unless
12
- # a block is passed. In that case, it will override the super implementation.
13
- #
14
- # default_render do
15
- # head 404 # No template was found
16
- # end
17
34
  def default_render(*args)
18
35
  if template_exists?(action_name.to_s, _prefixes, variants: request.variant)
19
36
  render(*args)
37
+ elsif any_templates?(action_name.to_s, _prefixes)
38
+ message = "#{self.class.name}\##{action_name} is missing a template " \
39
+ "for this request format and variant.\n" \
40
+ "\nrequest.formats: #{request.formats.map(&:to_s).inspect}" \
41
+ "\nrequest.variant: #{request.variant.inspect}"
42
+
43
+ raise ActionController::UnknownFormat, message
44
+ elsif interactive_browser_request?
45
+ message = "#{self.class.name}\##{action_name} is missing a template " \
46
+ "for this request format and variant.\n\n" \
47
+ "request.formats: #{request.formats.map(&:to_s).inspect}\n" \
48
+ "request.variant: #{request.variant.inspect}\n\n" \
49
+ "NOTE! For XHR/Ajax or API requests, this action would normally " \
50
+ "respond with 204 No Content: an empty white screen. Since you're " \
51
+ "loading it in a web browser, we assume that you expected to " \
52
+ "actually render a template, not… nothing, so we're showing an " \
53
+ "error to be extra-clear. If you expect 204 No Content, carry on. " \
54
+ "That's what you'll get from an XHR or API request. Give it a shot."
55
+
56
+ raise ActionController::UnknownFormat, message
20
57
  else
21
- if block_given?
22
- yield(*args)
23
- else
24
- logger.info "No template found for #{self.class.name}\##{action_name}, rendering head :no_content" if logger
25
- super
26
- end
58
+ logger.info "No template found for #{self.class.name}\##{action_name}, rendering head :no_content" if logger
59
+ super
27
60
  end
28
61
  end
29
62
 
@@ -32,5 +65,10 @@ module ActionController
32
65
  "default_render"
33
66
  end
34
67
  end
68
+
69
+ private
70
+ def interactive_browser_request?
71
+ request.get? && request.format == Mime[:html] && !request.xhr?
72
+ end
35
73
  end
36
74
  end
@@ -19,6 +19,7 @@ module ActionController
19
19
  :controller => self.class.name,
20
20
  :action => self.action_name,
21
21
  :params => request.filtered_parameters,
22
+ :headers => request.headers,
22
23
  :format => request.format.ref,
23
24
  :method => request.request_method,
24
25
  :path => request.fullpath
@@ -74,8 +75,8 @@ module ActionController
74
75
  ActiveSupport::Notifications.instrument("halted_callback.action_controller", :filter => filter)
75
76
  end
76
77
 
77
- # A hook which allows you to clean up any time taken into account in
78
- # views wrongly, like database querying time.
78
+ # A hook which allows you to clean up any time, wrongly taken into account in
79
+ # views, like database querying time.
79
80
  #
80
81
  # def cleanup_view_runtime
81
82
  # super - time_taken_in_something_expensive
@@ -3,7 +3,7 @@ require 'delegate'
3
3
  require 'active_support/json'
4
4
 
5
5
  module ActionController
6
- # Mix this module in to your controller, and all actions in that controller
6
+ # Mix this module into your controller, and all actions in that controller
7
7
  # will be able to stream data to the client as it's written.
8
8
  #
9
9
  # class MyController < ActionController::Base
@@ -20,7 +20,7 @@ module ActionController
20
20
  # end
21
21
  # end
22
22
  #
23
- # There are a few caveats with this use. You *cannot* write headers after the
23
+ # There are a few caveats with this module. You *cannot* write headers after the
24
24
  # response has been committed (Response#committed? will return truthy).
25
25
  # Calling +write+ or +close+ on the response stream will cause the response
26
26
  # object to be committed. Make sure all headers are set before calling write
@@ -198,7 +198,7 @@ module ActionController #:nodoc:
198
198
  _process_format(format)
199
199
  _set_rendered_content_type format
200
200
  response = collector.response
201
- response ? response.call : render({})
201
+ response.call if response
202
202
  else
203
203
  raise ActionController::UnknownFormat
204
204
  end
@@ -84,7 +84,7 @@ module ActionController
84
84
  # redirect_back fallback_location: proc { edit_post_url(@post) }
85
85
  #
86
86
  # All options that can be passed to <tt>redirect_to</tt> are accepted as
87
- # options and the behavior is indetical.
87
+ # options and the behavior is identical.
88
88
  def redirect_back(fallback_location:, **args)
89
89
  if referer = request.headers["Referer"]
90
90
  redirect_to referer, **args
@@ -213,7 +213,7 @@ module ActionController #:nodoc:
213
213
 
214
214
  if !verified_request?
215
215
  if logger && log_warning_on_csrf_failure
216
- logger.warn "Can't verify CSRF token authenticity"
216
+ logger.warn "Can't verify CSRF token authenticity."
217
217
  end
218
218
  handle_unverified_request
219
219
  end
@@ -405,7 +405,8 @@ module ActionController #:nodoc:
405
405
  end
406
406
 
407
407
  def normalize_action_path(action_path)
408
- action_path.split('?').first.to_s.chomp('/')
408
+ uri = URI.parse(action_path)
409
+ uri.path.chomp('/')
409
410
  end
410
411
  end
411
412
  end
@@ -7,8 +7,12 @@ module ActionController #:nodoc:
7
7
  include ActiveSupport::Rescuable
8
8
 
9
9
  def rescue_with_handler(exception)
10
- if exception.cause && handler_for_rescue(exception.cause)
11
- exception = exception.cause
10
+ if exception.cause
11
+ handler_index = index_of_handler_for_rescue(exception) || Float::INFINITY
12
+ cause_handler_index = index_of_handler_for_rescue(exception.cause)
13
+ if cause_handler_index && cause_handler_index <= handler_index
14
+ exception = exception.cause
15
+ end
12
16
  end
13
17
  super(exception)
14
18
  end
@@ -184,6 +184,13 @@ module ActionController
184
184
  # Returns an unsafe, unfiltered
185
185
  # <tt>ActiveSupport::HashWithIndifferentAccess</tt> representation of this
186
186
  # parameter.
187
+ #
188
+ # params = ActionController::Parameters.new({
189
+ # name: 'Senjougahara Hitagi',
190
+ # oddity: 'Heavy stone crab'
191
+ # })
192
+ # params.to_unsafe_h
193
+ # # => {"name"=>"Senjougahara Hitagi", "oddity" => "Heavy stone crab"}
187
194
  def to_unsafe_h
188
195
  convert_parameters_to_hashes(@parameters, :to_unsafe_h)
189
196
  end
@@ -430,6 +437,21 @@ module ActionController
430
437
  )
431
438
  end
432
439
 
440
+ if Hash.method_defined?(:dig)
441
+ # Extracts the nested parameter from the given +keys+ by calling +dig+
442
+ # at each step. Returns +nil+ if any intermediate step is +nil+.
443
+ #
444
+ # params = ActionController::Parameters.new(foo: { bar: { baz: 1 } })
445
+ # params.dig(:foo, :bar, :baz) # => 1
446
+ # params.dig(:foo, :zot, :xyz) # => nil
447
+ #
448
+ # params2 = ActionController::Parameters.new(foo: [10, 11, 12])
449
+ # params2.dig(:foo, 1) # => 11
450
+ def dig(*keys)
451
+ convert_value_to_parameters(@parameters.dig(*keys))
452
+ end
453
+ end
454
+
433
455
  # Returns a new <tt>ActionController::Parameters</tt> instance that
434
456
  # includes only the given +keys+. If the given +keys+
435
457
  # don't exist, returns an empty hash.
@@ -734,6 +756,10 @@ module ActionController
734
756
  end
735
757
  end
736
758
 
759
+ def non_scalar?(value)
760
+ value.is_a?(Array) || value.is_a?(Parameters)
761
+ end
762
+
737
763
  EMPTY_ARRAY = []
738
764
  def hash_filter(params, filter)
739
765
  filter = filter.with_indifferent_access
@@ -748,7 +774,7 @@ module ActionController
748
774
  array_of_permitted_scalars?(self[key]) do |val|
749
775
  params[key] = val
750
776
  end
751
- else
777
+ elsif non_scalar?(value)
752
778
  # Declaration { user: :name } or { user: [:name, :age, { address: ... }] }.
753
779
  params[key] = each_element(value) do |element|
754
780
  element.permit(*Array.wrap(filter[key]))
@@ -799,7 +825,8 @@ module ActionController
799
825
  # end
800
826
  #
801
827
  # In order to use <tt>accepts_nested_attributes_for</tt> with Strong \Parameters, you
802
- # will need to specify which nested attributes should be whitelisted.
828
+ # will need to specify which nested attributes should be whitelisted. You might want
829
+ # to allow +:id+ and +:_destroy+, see ActiveRecord::NestedAttributes for more information.
803
830
  #
804
831
  # class Person
805
832
  # has_many :pets
@@ -819,7 +846,7 @@ module ActionController
819
846
  # # It's mandatory to specify the nested attributes that should be whitelisted.
820
847
  # # If you use `permit` with just the key that points to the nested attributes hash,
821
848
  # # it will return an empty hash.
822
- # params.require(:person).permit(:name, :age, pets_attributes: [ :name, :category ])
849
+ # params.require(:person).permit(:name, :age, pets_attributes: [ :id, :name, :category ])
823
850
  # end
824
851
  # end
825
852
  #
@@ -45,7 +45,7 @@ module ActionController
45
45
  }.freeze
46
46
 
47
47
  # Create a new renderer instance for a specific controller class.
48
- def self.for(controller, env = {}, defaults = DEFAULTS)
48
+ def self.for(controller, env = {}, defaults = DEFAULTS.dup)
49
49
  new(controller, env, defaults)
50
50
  end
51
51
 
@@ -176,7 +176,7 @@ module ActionController
176
176
  def initialize(session = {})
177
177
  super(nil, nil)
178
178
  @id = SecureRandom.hex(16)
179
- @data = session.with_indifferent_access
179
+ @data = stringify_keys(session)
180
180
  @loaded = true
181
181
  end
182
182
 
@@ -428,7 +428,7 @@ module ActionController
428
428
  end
429
429
  alias xhr :xml_http_request
430
430
 
431
- # Simulate a HTTP request to +action+ by specifying request method,
431
+ # Simulate an HTTP request to +action+ by specifying request method,
432
432
  # parameters and set/volley the response.
433
433
  #
434
434
  # - +action+: The controller action to call.
@@ -51,8 +51,8 @@ module ActionDispatch
51
51
  autoload :Cookies
52
52
  autoload :DebugExceptions
53
53
  autoload :ExceptionWrapper
54
+ autoload :Executor
54
55
  autoload :Flash
55
- autoload :LoadInterlock
56
56
  autoload :ParamsParser
57
57
  autoload :PublicExceptions
58
58
  autoload :Reloader
@@ -17,9 +17,7 @@ module ActionDispatch
17
17
  end
18
18
 
19
19
  def if_none_match_etags
20
- (if_none_match ? if_none_match.split(/\s*,\s*/) : []).collect do |etag|
21
- etag.gsub(/^\"|\"$/, "")
22
- end
20
+ if_none_match ? if_none_match.split(/\s*,\s*/) : []
23
21
  end
24
22
 
25
23
  def not_modified?(modified_at)
@@ -28,8 +26,8 @@ module ActionDispatch
28
26
 
29
27
  def etag_matches?(etag)
30
28
  if etag
31
- etag = etag.gsub(/^\"|\"$/, "")
32
- if_none_match_etags.include?(etag)
29
+ validators = if_none_match_etags
30
+ validators.include?(etag) || validators.include?('*')
33
31
  end
34
32
  end
35
33
 
@@ -80,27 +78,63 @@ module ActionDispatch
80
78
  set_header DATE, utc_time.httpdate
81
79
  end
82
80
 
83
- # This method allows you to set the ETag for cached content, which
84
- # will be returned to the end user.
81
+ # This method sets a weak ETag validator on the response so browsers
82
+ # and proxies may cache the response, keyed on the ETag. On subsequent
83
+ # requests, the If-None-Match header is set to the cached ETag. If it
84
+ # matches the current ETag, we can return a 304 Not Modified response
85
+ # with no body, letting the browser or proxy know that their cache is
86
+ # current. Big savings in request time and network bandwidth.
87
+ #
88
+ # Weak ETags are considered to be semantically equivalent but not
89
+ # byte-for-byte identical. This is perfect for browser caching of HTML
90
+ # pages where we don't care about exact equality, just what the user
91
+ # is viewing.
85
92
  #
86
- # By default, Action Dispatch sets all ETags to be weak.
87
- # This ensures that if the content changes only semantically,
88
- # the whole page doesn't have to be regenerated from scratch
89
- # by the web server. With strong ETags, pages are compared
90
- # byte by byte, and are regenerated only if they are not exactly equal.
91
- def etag=(etag)
92
- key = ActiveSupport::Cache.expand_cache_key(etag)
93
- super %(W/"#{Digest::MD5.hexdigest(key)}")
93
+ # Strong ETags are considered byte-for-byte identical. They allow a
94
+ # browser or proxy cache to support Range requests, useful for paging
95
+ # through a PDF file or scrubbing through a video. Some CDNs only
96
+ # support strong ETags and will ignore weak ETags entirely.
97
+ #
98
+ # Weak ETags are what we almost always need, so they're the default.
99
+ # Check out `#strong_etag=` to provide a strong ETag validator.
100
+ def etag=(weak_validators)
101
+ self.weak_etag = weak_validators
102
+ end
103
+
104
+ def weak_etag=(weak_validators)
105
+ set_header 'ETag', generate_weak_etag(weak_validators)
106
+ end
107
+
108
+ def strong_etag=(strong_validators)
109
+ set_header 'ETag', generate_strong_etag(strong_validators)
94
110
  end
95
111
 
96
112
  def etag?; etag; end
97
113
 
114
+ # True if an ETag is set and it's a weak validator (preceded with W/)
115
+ def weak_etag?
116
+ etag? && etag.starts_with?('W/"')
117
+ end
118
+
119
+ # True if an ETag is set and it isn't a weak validator (not preceded with W/)
120
+ def strong_etag?
121
+ etag? && !weak_etag?
122
+ end
123
+
98
124
  private
99
125
 
100
126
  DATE = 'Date'.freeze
101
127
  LAST_MODIFIED = "Last-Modified".freeze
102
128
  SPECIAL_KEYS = Set.new(%w[extras no-cache max-age public private must-revalidate])
103
129
 
130
+ def generate_weak_etag(validators)
131
+ "W/#{generate_strong_etag(validators)}"
132
+ end
133
+
134
+ def generate_strong_etag(validators)
135
+ %("#{Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key(validators))}")
136
+ end
137
+
104
138
  def cache_control_segments
105
139
  if cache_control = _cache_control
106
140
  cache_control.delete(' ').split(',')