aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_controller/metal
diff options
context:
space:
mode:
Diffstat (limited to 'actionpack/lib/action_controller/metal')
-rw-r--r--actionpack/lib/action_controller/metal/conditional_get.rb73
-rw-r--r--actionpack/lib/action_controller/metal/cookies.rb2
-rw-r--r--actionpack/lib/action_controller/metal/data_streaming.rb2
-rw-r--r--actionpack/lib/action_controller/metal/http_authentication.rb7
-rw-r--r--actionpack/lib/action_controller/metal/implicit_render.rb87
-rw-r--r--actionpack/lib/action_controller/metal/instrumentation.rb1
-rw-r--r--actionpack/lib/action_controller/metal/mime_responds.rb2
-rw-r--r--actionpack/lib/action_controller/metal/redirecting.rb2
-rw-r--r--actionpack/lib/action_controller/metal/request_forgery_protection.rb2
-rw-r--r--actionpack/lib/action_controller/metal/rescue.rb8
-rw-r--r--actionpack/lib/action_controller/metal/strong_parameters.rb33
11 files changed, 134 insertions, 85 deletions
diff --git a/actionpack/lib/action_controller/metal/conditional_get.rb b/actionpack/lib/action_controller/metal/conditional_get.rb
index f8e0d9cf6c..480e265e44 100644
--- a/actionpack/lib/action_controller/metal/conditional_get.rb
+++ b/actionpack/lib/action_controller/metal/conditional_get.rb
@@ -36,8 +36,23 @@ module ActionController
#
# === Parameters:
#
- # * <tt>:etag</tt>.
- # * <tt>:last_modified</tt>.
+ # * <tt>:etag</tt> Sets a "weak" ETag validator on the response. See the
+ # +:weak_etag+ option.
+ # * <tt>:weak_etag</tt> Sets a "weak" ETag validator on the response.
+ # Requests that set If-None-Match header may return a 304 Not Modified
+ # response if it matches the ETag exactly. A weak ETag indicates semantic
+ # equivalence, not byte-for-byte equality, so they're good for caching
+ # HTML pages in browser caches. They can't be used for responses that
+ # must be byte-identical, like serving Range requests within a PDF file.
+ # * <tt>:strong_etag</tt> Sets a "strong" ETag validator on the response.
+ # Requests that set If-None-Match header may return a 304 Not Modified
+ # response if it matches the ETag exactly. A strong ETag implies exact
+ # equality: the response must match byte for byte. This is necessary for
+ # doing Range requests within a large video or PDF file, for example, or
+ # for compatibility with some CDNs that don't support weak ETags.
+ # * <tt>:last_modified</tt> Sets a "weak" last-update validator on the
+ # response. Subsequent requests that set If-Modified-Since may return a
+ # 304 Not Modified response if last_modified <= If-Modified-Since.
# * <tt>:public</tt> By default the Cache-Control header is private, set this to
# +true+ if you want your application to be cacheable by other devices (proxy caches).
# * <tt>:template</tt> By default, the template digest for the current
@@ -86,12 +101,16 @@ module ActionController
#
# before_action { fresh_when @article, template: 'widgets/show' }
#
- def fresh_when(object = nil, etag: object, last_modified: nil, public: false, template: nil)
+ def fresh_when(object = nil, etag: nil, weak_etag: nil, strong_etag: nil, last_modified: nil, public: false, template: nil)
+ weak_etag ||= etag || object unless strong_etag
last_modified ||= object.try(:updated_at) || object.try(:maximum, :updated_at)
- if etag || template
- response.etag = combine_etags(etag: etag, last_modified: last_modified,
- public: public, template: template)
+ if strong_etag
+ response.strong_etag = combine_etags strong_etag,
+ last_modified: last_modified, public: public, template: template
+ elsif weak_etag || template
+ response.weak_etag = combine_etags weak_etag,
+ last_modified: last_modified, public: public, template: template
end
response.last_modified = last_modified if last_modified
@@ -107,8 +126,23 @@ module ActionController
#
# === Parameters:
#
- # * <tt>:etag</tt>.
- # * <tt>:last_modified</tt>.
+ # * <tt>:etag</tt> Sets a "weak" ETag validator on the response. See the
+ # +:weak_etag+ option.
+ # * <tt>:weak_etag</tt> Sets a "weak" ETag validator on the response.
+ # requests that set If-None-Match header may return a 304 Not Modified
+ # response if it matches the ETag exactly. A weak ETag indicates semantic
+ # equivalence, not byte-for-byte equality, so they're good for caching
+ # HTML pages in browser caches. They can't be used for responses that
+ # must be byte-identical, like serving Range requests within a PDF file.
+ # * <tt>:strong_etag</tt> Sets a "strong" ETag validator on the response.
+ # Requests that set If-None-Match header may return a 304 Not Modified
+ # response if it matches the ETag exactly. A strong ETag implies exact
+ # equality: the response must match byte for byte. This is necessary for
+ # doing Range requests within a large video or PDF file, for example, or
+ # for compatibility with some CDNs that don't support weak ETags.
+ # * <tt>:last_modified</tt> Sets a "weak" last-update validator on the
+ # response. Subsequent requests that set If-Modified-Since may return a
+ # 304 Not Modified response if last_modified <= If-Modified-Since.
# * <tt>:public</tt> By default the Cache-Control header is private, set this to
# +true+ if you want your application to be cacheable by other devices (proxy caches).
# * <tt>:template</tt> By default, the template digest for the current
@@ -180,12 +214,12 @@ module ActionController
# super if stale? @article, template: 'widgets/show'
# end
#
- def stale?(object = nil, etag: object, last_modified: nil, public: nil, template: nil)
- fresh_when(object, etag: etag, last_modified: last_modified, public: public, template: template)
+ def stale?(object = nil, **freshness_kwargs)
+ fresh_when(object, **freshness_kwargs)
!request.fresh?(response)
end
- # Sets a HTTP 1.1 Cache-Control header. Defaults to issuing a +private+
+ # Sets an HTTP 1.1 Cache-Control header. Defaults to issuing a +private+
# instruction, so that intermediate caches must not cache the response.
#
# expires_in 20.minutes
@@ -195,7 +229,7 @@ module ActionController
# This method will overwrite an existing Cache-Control header.
# See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html for more possibilities.
#
- # The method will also ensure a HTTP Date header for client compatibility.
+ # The method will also ensure an HTTP Date header for client compatibility.
def expires_in(seconds, options = {})
response.cache_control.merge!(
:max_age => seconds,
@@ -208,7 +242,7 @@ module ActionController
response.date = Time.now unless response.date?
end
- # Sets a HTTP 1.1 Cache-Control header of <tt>no-cache</tt> so no caching should
+ # Sets an HTTP 1.1 Cache-Control header of <tt>no-cache</tt> so no caching should
# occur by the browser or intermediate caches (like caching proxy servers).
def expires_now
response.cache_control.replace(:no_cache => true)
@@ -216,26 +250,23 @@ module ActionController
# Cache or yield the block. The cache is supposed to never expire.
#
- # You can use this method when you have a HTTP response that never changes,
+ # You can use this method when you have an HTTP response that never changes,
# and the browser and proxies should cache it indefinitely.
#
# * +public+: By default, HTTP responses are private, cached only on the
# user's web browser. To allow proxies to cache the response, set +true+ to
# indicate that they can serve the cached response to all users.
- #
- # * +version+: the version passed as a key for the cache.
- def http_cache_forever(public: false, version: 'v1')
+ def http_cache_forever(public: false)
expires_in 100.years, public: public
- yield if stale?(etag: "#{version}-#{request.fullpath}",
+ yield if stale?(etag: request.fullpath,
last_modified: Time.new(2011, 1, 1).utc,
public: public)
end
private
- def combine_etags(options)
- etags = etaggers.map { |etagger| instance_exec(options, &etagger) }.compact
- etags.unshift options[:etag]
+ def combine_etags(validator, options)
+ [validator, *etaggers.map { |etagger| instance_exec(options, &etagger) }].compact
end
end
end
diff --git a/actionpack/lib/action_controller/metal/cookies.rb b/actionpack/lib/action_controller/metal/cookies.rb
index f8efb2b076..44925641a1 100644
--- a/actionpack/lib/action_controller/metal/cookies.rb
+++ b/actionpack/lib/action_controller/metal/cookies.rb
@@ -3,7 +3,7 @@ module ActionController #:nodoc:
extend ActiveSupport::Concern
included do
- helper_method :cookies
+ helper_method :cookies if defined?(helper_method)
end
private
diff --git a/actionpack/lib/action_controller/metal/data_streaming.rb b/actionpack/lib/action_controller/metal/data_streaming.rb
index 957e7a3019..59984a0028 100644
--- a/actionpack/lib/action_controller/metal/data_streaming.rb
+++ b/actionpack/lib/action_controller/metal/data_streaming.rb
@@ -84,7 +84,7 @@ module ActionController #:nodoc:
# Options:
# * <tt>:filename</tt> - suggests a filename for the browser to use.
# * <tt>:type</tt> - specifies an HTTP content type. Defaults to 'application/octet-stream'. You can specify
- # either a string or a symbol for a registered type register with <tt>Mime::Type.register</tt>, for example :json
+ # either a string or a symbol for a registered type register with <tt>Mime::Type.register</tt>, for example :json.
# If omitted, type will be guessed from the file extension specified in <tt>:filename</tt>.
# If no content type is registered for the extension, default type 'application/octet-stream' will be used.
# * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded.
diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb
index 35be6d9300..53527c08b6 100644
--- a/actionpack/lib/action_controller/metal/http_authentication.rb
+++ b/actionpack/lib/action_controller/metal/http_authentication.rb
@@ -347,7 +347,12 @@ module ActionController
# private
# def authenticate
# authenticate_or_request_with_http_token do |token, options|
- # token == TOKEN
+ # # Compare the tokens in a time-constant manner, to mitigate
+ # # timing attacks.
+ # ActiveSupport::SecurityUtils.secure_compare(
+ # ::Digest::SHA256.hexdigest(token),
+ # ::Digest::SHA256.hexdigest(TOKEN)
+ # )
# end
# end
# end
diff --git a/actionpack/lib/action_controller/metal/implicit_render.rb b/actionpack/lib/action_controller/metal/implicit_render.rb
index 3a6f784507..6192fc0f9c 100644
--- a/actionpack/lib/action_controller/metal/implicit_render.rb
+++ b/actionpack/lib/action_controller/metal/implicit_render.rb
@@ -1,33 +1,31 @@
require 'active_support/core_ext/string/strip'
module ActionController
- # Handles implicit rendering for a controller action when it did not
- # explicitly indicate an appropiate response via methods such as +render+,
- # +respond_to+, +redirect+ or +head+.
+ # Handles implicit rendering for a controller action that does not
+ # explicitly respond with +render+, +respond_to+, +redirect+, or +head+.
#
- # For API controllers, the implicit render always renders "204 No Content"
- # and does not account for any templates.
+ # For API controllers, the implicit response is always 204 No Content.
#
- # For other controllers, the following conditions are checked:
+ # For all other controllers, we use these heuristics to decide whether to
+ # render a template, raise an error for a missing template, or respond with
+ # 204 No Content:
#
- # First, if a template exists for the controller action, it is rendered.
- # This template lookup takes into account the action name, locales, format,
- # variant, template handlers, etc. (see +render+ for details).
+ # First, if we DO find a template, it's rendered. Template lookup accounts
+ # for the action name, locales, format, variant, template handlers, and more
+ # (see +render+ for details).
#
- # Second, if other templates exist for the controller action but is not in
- # the right format (or variant, etc.), an <tt>ActionController::UnknownFormat</tt>
- # is raised. The list of available templates is assumed to be a complete
- # enumeration of all the possible formats (or variants, etc.); that is,
- # having only HTML and JSON templates indicate that the controller action is
- # not meant to handle XML requests.
+ # Second, if we DON'T find a template but the controller action does have
+ # templates for other formats, variants, etc., then we trust that you meant
+ # to provide a template for this response, too, and we raise
+ # <tt>ActionController::UnknownFormat</tt> with an explanation.
#
- # Third, if the current request is an "interactive" browser request (the user
- # navigated here by entering the URL in the address bar, submiting a form,
- # clicking on a link, etc. as opposed to an XHR or non-browser API request),
- # <tt>ActionView::UnknownFormat</tt> is raised to display a helpful error
- # message.
+ # Third, if we DON'T find a template AND the request is a page load in a web
+ # browser (technically, a non-XHR GET request for an HTML response) where
+ # you reasonably expect to have rendered a template, then we raise
+ # <tt>ActionView::UnknownFormat</tt> with an explanation.
#
- # Finally, it falls back to the same "204 No Content" behavior as API controllers.
+ # Finally, if we DON'T find a template AND the request isn't a browser page
+ # load, then we implicitly respond with 204 No Content.
module ImplicitRender
# :stopdoc:
@@ -37,39 +35,23 @@ module ActionController
if template_exists?(action_name.to_s, _prefixes, variants: request.variant)
render(*args)
elsif any_templates?(action_name.to_s, _prefixes)
- message = "#{self.class.name}\##{action_name} does not know how to respond " \
- "to this request. There are other templates available for this controller " \
- "action but none of them were suitable for this request.\n\n" \
- "This usually happens when the client requested an unsupported format " \
- "(e.g. requesting HTML content from a JSON endpoint or vice versa), but " \
- "it might also be failing due to other constraints, such as locales or " \
- "variants.\n"
-
- if request.formats.any?
- message << "\nRequested format(s): #{request.formats.join(", ")}"
- end
-
- if request.variant.any?
- message << "\nRequested variant(s): #{request.variant.join(", ")}"
- end
+ message = "#{self.class.name}\##{action_name} is missing a template " \
+ "for this request format and variant.\n" \
+ "\nrequest.formats: #{request.formats.map(&:to_s).inspect}" \
+ "\nrequest.variant: #{request.variant.inspect}"
raise ActionController::UnknownFormat, message
elsif interactive_browser_request?
- message = "You did not define any templates for #{self.class.name}\##{action_name}. " \
- "This is not necessarily a problem (e.g. you might be building an API endpoint " \
- "that does not require any templates), and the controller would usually respond " \
- "with `head :no_content` for your convenience.\n\n" \
- "However, you appear to have navigated here from an interactive browser request – " \
- "such as by navigating to this URL directly, clicking on a link or submitting a form. " \
- "Rendering a `head :no_content` in this case could have resulted in unexpected UI " \
- "behavior in the browser.\n\n" \
- "If you expected the `head :no_content` response, you do not need to take any " \
- "actions – requests coming from an XHR (AJAX) request or other non-browser clients " \
- "will receive the \"204 No Content\" response as expected.\n\n" \
- "If you did not expect this behavior, you can resolve this error by adding a " \
- "template for this controller action (usually `#{action_name}.html.erb`) or " \
- "otherwise indicate the appropriate response in the action using `render`, " \
- "`redirect_to`, `head`, etc.\n"
+ message = "#{self.class.name}\##{action_name} is missing a template " \
+ "for this request format and variant.\n\n" \
+ "request.formats: #{request.formats.map(&:to_s).inspect}\n" \
+ "request.variant: #{request.variant.inspect}\n\n" \
+ "NOTE! For XHR/Ajax or API requests, this action would normally " \
+ "respond with 204 No Content: an empty white screen. Since you're " \
+ "loading it in a web browser, we assume that you expected to " \
+ "actually render a template, not… nothing, so we're showing an " \
+ "error to be extra-clear. If you expect 204 No Content, carry on. " \
+ "That's what you'll get from an XHR or API request. Give it a shot."
raise ActionController::UnknownFormat, message
else
@@ -85,9 +67,8 @@ module ActionController
end
private
-
def interactive_browser_request?
- request.format == Mime[:html] && !request.xhr?
+ request.get? && request.format == Mime[:html] && !request.xhr?
end
end
end
diff --git a/actionpack/lib/action_controller/metal/instrumentation.rb b/actionpack/lib/action_controller/metal/instrumentation.rb
index bf74b39ac4..885ea3fefd 100644
--- a/actionpack/lib/action_controller/metal/instrumentation.rb
+++ b/actionpack/lib/action_controller/metal/instrumentation.rb
@@ -19,6 +19,7 @@ module ActionController
:controller => self.class.name,
:action => self.action_name,
:params => request.filtered_parameters,
+ :headers => request.headers,
:format => request.format.ref,
:method => request.request_method,
:path => request.fullpath
diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb
index 173a14a1d2..2e89af1a5e 100644
--- a/actionpack/lib/action_controller/metal/mime_responds.rb
+++ b/actionpack/lib/action_controller/metal/mime_responds.rb
@@ -198,7 +198,7 @@ module ActionController #:nodoc:
_process_format(format)
_set_rendered_content_type format
response = collector.response
- response ? response.call : render({})
+ response.call if response
else
raise ActionController::UnknownFormat
end
diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb
index b13ba06962..3c7cc15627 100644
--- a/actionpack/lib/action_controller/metal/redirecting.rb
+++ b/actionpack/lib/action_controller/metal/redirecting.rb
@@ -84,7 +84,7 @@ module ActionController
# redirect_back fallback_location: proc { edit_post_url(@post) }
#
# All options that can be passed to <tt>redirect_to</tt> are accepted as
- # options and the behavior is indetical.
+ # options and the behavior is identical.
def redirect_back(fallback_location:, **args)
if referer = request.headers["Referer"]
redirect_to referer, **args
diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb
index b2f0b382b9..5793e28175 100644
--- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb
+++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb
@@ -213,7 +213,7 @@ module ActionController #:nodoc:
if !verified_request?
if logger && log_warning_on_csrf_failure
- logger.warn "Can't verify CSRF token authenticity"
+ logger.warn "Can't verify CSRF token authenticity."
end
handle_unverified_request
end
diff --git a/actionpack/lib/action_controller/metal/rescue.rb b/actionpack/lib/action_controller/metal/rescue.rb
index 81b9a7b9ed..0621a7368c 100644
--- a/actionpack/lib/action_controller/metal/rescue.rb
+++ b/actionpack/lib/action_controller/metal/rescue.rb
@@ -7,8 +7,12 @@ module ActionController #:nodoc:
include ActiveSupport::Rescuable
def rescue_with_handler(exception)
- if exception.cause && handler_for_rescue(exception.cause)
- exception = exception.cause
+ if exception.cause
+ handler_index = index_of_handler_for_rescue(exception) || Float::INFINITY
+ cause_handler_index = index_of_handler_for_rescue(exception.cause)
+ if cause_handler_index && cause_handler_index <= handler_index
+ exception = exception.cause
+ end
end
super(exception)
end
diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb
index a01110d474..f9b80dd805 100644
--- a/actionpack/lib/action_controller/metal/strong_parameters.rb
+++ b/actionpack/lib/action_controller/metal/strong_parameters.rb
@@ -184,6 +184,13 @@ module ActionController
# Returns an unsafe, unfiltered
# <tt>ActiveSupport::HashWithIndifferentAccess</tt> representation of this
# parameter.
+ #
+ # params = ActionController::Parameters.new({
+ # name: 'Senjougahara Hitagi',
+ # oddity: 'Heavy stone crab'
+ # })
+ # params.to_unsafe_h
+ # # => {"name"=>"Senjougahara Hitagi", "oddity" => "Heavy stone crab"}
def to_unsafe_h
convert_parameters_to_hashes(@parameters, :to_unsafe_h)
end
@@ -430,6 +437,21 @@ module ActionController
)
end
+ if Hash.method_defined?(:dig)
+ # Extracts the nested parameter from the given +keys+ by calling +dig+
+ # at each step. Returns +nil+ if any intermediate step is +nil+.
+ #
+ # params = ActionController::Parameters.new(foo: { bar: { baz: 1 } })
+ # params.dig(:foo, :bar, :baz) # => 1
+ # params.dig(:foo, :zot, :xyz) # => nil
+ #
+ # params2 = ActionController::Parameters.new(foo: [10, 11, 12])
+ # params2.dig(:foo, 1) # => 11
+ def dig(*keys)
+ convert_value_to_parameters(@parameters.dig(*keys))
+ end
+ end
+
# Returns a new <tt>ActionController::Parameters</tt> instance that
# includes only the given +keys+. If the given +keys+
# don't exist, returns an empty hash.
@@ -734,6 +756,10 @@ module ActionController
end
end
+ def non_scalar?(value)
+ value.is_a?(Array) || value.is_a?(Parameters)
+ end
+
EMPTY_ARRAY = []
def hash_filter(params, filter)
filter = filter.with_indifferent_access
@@ -748,7 +774,7 @@ module ActionController
array_of_permitted_scalars?(self[key]) do |val|
params[key] = val
end
- else
+ elsif non_scalar?(value)
# Declaration { user: :name } or { user: [:name, :age, { address: ... }] }.
params[key] = each_element(value) do |element|
element.permit(*Array.wrap(filter[key]))
@@ -799,7 +825,8 @@ module ActionController
# end
#
# In order to use <tt>accepts_nested_attributes_for</tt> with Strong \Parameters, you
- # will need to specify which nested attributes should be whitelisted.
+ # will need to specify which nested attributes should be whitelisted. You might want
+ # to allow +:id+ and +:_destroy+, see ActiveRecord::NestedAttributes for more information.
#
# class Person
# has_many :pets
@@ -819,7 +846,7 @@ module ActionController
# # It's mandatory to specify the nested attributes that should be whitelisted.
# # If you use `permit` with just the key that points to the nested attributes hash,
# # it will return an empty hash.
- # params.require(:person).permit(:name, :age, pets_attributes: [ :name, :category ])
+ # params.require(:person).permit(:name, :age, pets_attributes: [ :id, :name, :category ])
# end
# end
#