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/basic_implicit_render.rb13
-rw-r--r--actionpack/lib/action_controller/metal/conditional_get.rb274
-rw-r--r--actionpack/lib/action_controller/metal/cookies.rb16
-rw-r--r--actionpack/lib/action_controller/metal/data_streaming.rb152
-rw-r--r--actionpack/lib/action_controller/metal/etag_with_flash.rb18
-rw-r--r--actionpack/lib/action_controller/metal/etag_with_template_digest.rb57
-rw-r--r--actionpack/lib/action_controller/metal/exceptions.rb56
-rw-r--r--actionpack/lib/action_controller/metal/flash.rb61
-rw-r--r--actionpack/lib/action_controller/metal/force_ssl.rb99
-rw-r--r--actionpack/lib/action_controller/metal/head.rb60
-rw-r--r--actionpack/lib/action_controller/metal/helpers.rb123
-rw-r--r--actionpack/lib/action_controller/metal/http_authentication.rb522
-rw-r--r--actionpack/lib/action_controller/metal/implicit_render.rb73
-rw-r--r--actionpack/lib/action_controller/metal/instrumentation.rb111
-rw-r--r--actionpack/lib/action_controller/metal/live.rb312
-rw-r--r--actionpack/lib/action_controller/metal/mime_responds.rb313
-rw-r--r--actionpack/lib/action_controller/metal/parameter_encoding.rb51
-rw-r--r--actionpack/lib/action_controller/metal/params_wrapper.rb285
-rw-r--r--actionpack/lib/action_controller/metal/redirecting.rb124
-rw-r--r--actionpack/lib/action_controller/metal/renderers.rb181
-rw-r--r--actionpack/lib/action_controller/metal/rendering.rb124
-rw-r--r--actionpack/lib/action_controller/metal/request_forgery_protection.rb433
-rw-r--r--actionpack/lib/action_controller/metal/rescue.rb28
-rw-r--r--actionpack/lib/action_controller/metal/streaming.rb223
-rw-r--r--actionpack/lib/action_controller/metal/strong_parameters.rb1085
-rw-r--r--actionpack/lib/action_controller/metal/testing.rb22
-rw-r--r--actionpack/lib/action_controller/metal/url_for.rb58
27 files changed, 4874 insertions, 0 deletions
diff --git a/actionpack/lib/action_controller/metal/basic_implicit_render.rb b/actionpack/lib/action_controller/metal/basic_implicit_render.rb
new file mode 100644
index 0000000000..2dc990f303
--- /dev/null
+++ b/actionpack/lib/action_controller/metal/basic_implicit_render.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module ActionController
+ module BasicImplicitRender # :nodoc:
+ def send_action(method, *args)
+ super.tap { default_render unless performed? }
+ end
+
+ def default_render(*args)
+ head :no_content
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/metal/conditional_get.rb b/actionpack/lib/action_controller/metal/conditional_get.rb
new file mode 100644
index 0000000000..0d9a0c8c56
--- /dev/null
+++ b/actionpack/lib/action_controller/metal/conditional_get.rb
@@ -0,0 +1,274 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/keys"
+
+module ActionController
+ module ConditionalGet
+ extend ActiveSupport::Concern
+
+ include Head
+
+ included do
+ class_attribute :etaggers, default: []
+ end
+
+ module ClassMethods
+ # Allows you to consider additional controller-wide information when generating an ETag.
+ # For example, if you serve pages tailored depending on who's logged in at the moment, you
+ # may want to add the current user id to be part of the ETag to prevent unauthorized displaying
+ # of cached pages.
+ #
+ # class InvoicesController < ApplicationController
+ # etag { current_user.try :id }
+ #
+ # def show
+ # # Etag will differ even for the same invoice when it's viewed by a different current_user
+ # @invoice = Invoice.find(params[:id])
+ # fresh_when(@invoice)
+ # end
+ # end
+ def etag(&etagger)
+ self.etaggers += [etagger]
+ end
+ end
+
+ # Sets the +etag+, +last_modified+, or both on the response and renders a
+ # <tt>304 Not Modified</tt> response if the request is already fresh.
+ #
+ # === Parameters:
+ #
+ # * <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
+ # controller/action is included in ETags. If the action renders a
+ # different template, you can include its digest instead. If the action
+ # doesn't render a template at all, you can pass <tt>template: false</tt>
+ # to skip any attempt to check for a template digest.
+ #
+ # === Example:
+ #
+ # def show
+ # @article = Article.find(params[:id])
+ # fresh_when(etag: @article, last_modified: @article.updated_at, public: true)
+ # end
+ #
+ # This will render the show template if the request isn't sending a matching ETag or
+ # If-Modified-Since header and just a <tt>304 Not Modified</tt> response if there's a match.
+ #
+ # You can also just pass a record. In this case +last_modified+ will be set
+ # by calling +updated_at+ and +etag+ by passing the object itself.
+ #
+ # def show
+ # @article = Article.find(params[:id])
+ # fresh_when(@article)
+ # end
+ #
+ # You can also pass an object that responds to +maximum+, such as a
+ # collection of active records. In this case +last_modified+ will be set by
+ # calling <tt>maximum(:updated_at)</tt> on the collection (the timestamp of the
+ # most recently updated record) and the +etag+ by passing the object itself.
+ #
+ # def index
+ # @articles = Article.all
+ # fresh_when(@articles)
+ # end
+ #
+ # When passing a record or a collection, you can still set the public header:
+ #
+ # def show
+ # @article = Article.find(params[:id])
+ # fresh_when(@article, public: true)
+ # end
+ #
+ # When rendering a different template than the default controller/action
+ # style, you can indicate which digest to include in the ETag:
+ #
+ # before_action { fresh_when @article, template: 'widgets/show' }
+ #
+ 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 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
+ response.cache_control[:public] = true if public
+
+ head :not_modified if request.fresh?(response)
+ end
+
+ # Sets the +etag+ and/or +last_modified+ on the response and checks it against
+ # the client request. If the request doesn't match the options provided, the
+ # request is considered stale and should be generated from scratch. Otherwise,
+ # it's fresh and we don't need to generate anything and a reply of <tt>304 Not Modified</tt> is sent.
+ #
+ # === Parameters:
+ #
+ # * <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
+ # controller/action is included in ETags. If the action renders a
+ # different template, you can include its digest instead. If the action
+ # doesn't render a template at all, you can pass <tt>template: false</tt>
+ # to skip any attempt to check for a template digest.
+ #
+ # === Example:
+ #
+ # def show
+ # @article = Article.find(params[:id])
+ #
+ # if stale?(etag: @article, last_modified: @article.updated_at)
+ # @statistics = @article.really_expensive_call
+ # respond_to do |format|
+ # # all the supported formats
+ # end
+ # end
+ # end
+ #
+ # You can also just pass a record. In this case +last_modified+ will be set
+ # by calling +updated_at+ and +etag+ by passing the object itself.
+ #
+ # def show
+ # @article = Article.find(params[:id])
+ #
+ # if stale?(@article)
+ # @statistics = @article.really_expensive_call
+ # respond_to do |format|
+ # # all the supported formats
+ # end
+ # end
+ # end
+ #
+ # You can also pass an object that responds to +maximum+, such as a
+ # collection of active records. In this case +last_modified+ will be set by
+ # calling +maximum(:updated_at)+ on the collection (the timestamp of the
+ # most recently updated record) and the +etag+ by passing the object itself.
+ #
+ # def index
+ # @articles = Article.all
+ #
+ # if stale?(@articles)
+ # @statistics = @articles.really_expensive_call
+ # respond_to do |format|
+ # # all the supported formats
+ # end
+ # end
+ # end
+ #
+ # When passing a record or a collection, you can still set the public header:
+ #
+ # def show
+ # @article = Article.find(params[:id])
+ #
+ # if stale?(@article, public: true)
+ # @statistics = @article.really_expensive_call
+ # respond_to do |format|
+ # # all the supported formats
+ # end
+ # end
+ # end
+ #
+ # When rendering a different template than the default controller/action
+ # style, you can indicate which digest to include in the ETag:
+ #
+ # def show
+ # super if stale? @article, template: 'widgets/show'
+ # end
+ #
+ def stale?(object = nil, **freshness_kwargs)
+ fresh_when(object, **freshness_kwargs)
+ !request.fresh?(response)
+ end
+
+ # 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
+ # expires_in 3.hours, public: true
+ # expires_in 3.hours, public: true, must_revalidate: true
+ #
+ # 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 an HTTP Date header for client compatibility.
+ def expires_in(seconds, options = {})
+ response.cache_control.merge!(
+ max_age: seconds,
+ public: options.delete(:public),
+ must_revalidate: options.delete(:must_revalidate)
+ )
+ options.delete(:private)
+
+ response.cache_control[:extras] = options.map { |k, v| "#{k}=#{v}" }
+ response.date = Time.now unless response.date?
+ end
+
+ # Sets an HTTP 1.1 Cache-Control header of <tt>no-cache</tt>. This means the
+ # resource will be marked as stale, so clients must always revalidate.
+ # Intermediate/browser caches may still store the asset.
+ def expires_now
+ response.cache_control.replace(no_cache: true)
+ end
+
+ # Cache or yield the block. The cache is supposed to never expire.
+ #
+ # 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.
+ def http_cache_forever(public: false)
+ expires_in 100.years, public: public
+
+ yield if stale?(etag: request.fullpath,
+ last_modified: Time.new(2011, 1, 1).utc,
+ public: public)
+ end
+
+ private
+ 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
new file mode 100644
index 0000000000..ff46966693
--- /dev/null
+++ b/actionpack/lib/action_controller/metal/cookies.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module ActionController #:nodoc:
+ module Cookies
+ extend ActiveSupport::Concern
+
+ included do
+ helper_method :cookies if defined?(helper_method)
+ end
+
+ private
+ def cookies
+ request.cookie_jar
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/metal/data_streaming.rb b/actionpack/lib/action_controller/metal/data_streaming.rb
new file mode 100644
index 0000000000..30234ee304
--- /dev/null
+++ b/actionpack/lib/action_controller/metal/data_streaming.rb
@@ -0,0 +1,152 @@
+# frozen_string_literal: true
+
+require_relative "exceptions"
+
+module ActionController #:nodoc:
+ # Methods for sending arbitrary data and for streaming files to the browser,
+ # instead of rendering.
+ module DataStreaming
+ extend ActiveSupport::Concern
+
+ include ActionController::Rendering
+
+ DEFAULT_SEND_FILE_TYPE = "application/octet-stream".freeze #:nodoc:
+ DEFAULT_SEND_FILE_DISPOSITION = "attachment".freeze #:nodoc:
+
+ private
+ # Sends the file. This uses a server-appropriate method (such as X-Sendfile)
+ # via the Rack::Sendfile middleware. The header to use is set via
+ # +config.action_dispatch.x_sendfile_header+.
+ # Your server can also configure this for you by setting the X-Sendfile-Type header.
+ #
+ # Be careful to sanitize the path parameter if it is coming from a web
+ # page. <tt>send_file(params[:path])</tt> allows a malicious user to
+ # download any file on your server.
+ #
+ # Options:
+ # * <tt>:filename</tt> - suggests a filename for the browser to use.
+ # Defaults to <tt>File.basename(path)</tt>.
+ # * <tt>:type</tt> - specifies an HTTP content type.
+ # You can specify either a string or a symbol for a registered type with <tt>Mime::Type.register</tt>, for example :json.
+ # If omitted, the type will be inferred from the file extension specified in <tt>:filename</tt>.
+ # If no content type is registered for the extension, the default type 'application/octet-stream' will be used.
+ # * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded.
+ # Valid values are 'inline' and 'attachment' (default).
+ # * <tt>:status</tt> - specifies the status code to send with the response. Defaults to 200.
+ # * <tt>:url_based_filename</tt> - set to +true+ if you want the browser to guess the filename from
+ # the URL, which is necessary for i18n filenames on certain browsers
+ # (setting <tt>:filename</tt> overrides this option).
+ #
+ # The default Content-Type and Content-Disposition headers are
+ # set to download arbitrary binary files in as many browsers as
+ # possible. IE versions 4, 5, 5.5, and 6 are all known to have
+ # a variety of quirks (especially when downloading over SSL).
+ #
+ # Simple download:
+ #
+ # send_file '/path/to.zip'
+ #
+ # Show a JPEG in the browser:
+ #
+ # send_file '/path/to.jpeg', type: 'image/jpeg', disposition: 'inline'
+ #
+ # Show a 404 page in the browser:
+ #
+ # send_file '/path/to/404.html', type: 'text/html; charset=utf-8', status: 404
+ #
+ # Read about the other Content-* HTTP headers if you'd like to
+ # provide the user with more information (such as Content-Description) in
+ # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11.
+ #
+ # Also be aware that the document may be cached by proxies and browsers.
+ # The Pragma and Cache-Control headers declare how the file may be cached
+ # by intermediaries. They default to require clients to validate with
+ # the server before releasing cached responses. See
+ # http://www.mnot.net/cache_docs/ for an overview of web caching and
+ # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9
+ # for the Cache-Control header spec.
+ def send_file(path, options = {}) #:doc:
+ raise MissingFile, "Cannot read file #{path}" unless File.file?(path) && File.readable?(path)
+
+ options[:filename] ||= File.basename(path) unless options[:url_based_filename]
+ send_file_headers! options
+
+ self.status = options[:status] || 200
+ self.content_type = options[:content_type] if options.key?(:content_type)
+ response.send_file path
+ end
+
+ # Sends the given binary data to the browser. This method is similar to
+ # <tt>render plain: data</tt>, but also allows you to specify whether
+ # the browser should display the response as a file attachment (i.e. in a
+ # download dialog) or as inline data. You may also set the content type,
+ # the file name, and other things.
+ #
+ # 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 with <tt>Mime::Type.register</tt>, for example :json.
+ # If omitted, type will be inferred from the file extension specified in <tt>:filename</tt>.
+ # If no content type is registered for the extension, the default type 'application/octet-stream' will be used.
+ # * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded.
+ # Valid values are 'inline' and 'attachment' (default).
+ # * <tt>:status</tt> - specifies the status code to send with the response. Defaults to 200.
+ #
+ # Generic data download:
+ #
+ # send_data buffer
+ #
+ # Download a dynamically-generated tarball:
+ #
+ # send_data generate_tgz('dir'), filename: 'dir.tgz'
+ #
+ # Display an image Active Record in the browser:
+ #
+ # send_data image.data, type: image.content_type, disposition: 'inline'
+ #
+ # See +send_file+ for more information on HTTP Content-* headers and caching.
+ def send_data(data, options = {}) #:doc:
+ send_file_headers! options
+ render options.slice(:status, :content_type).merge(body: data)
+ end
+
+ def send_file_headers!(options)
+ type_provided = options.has_key?(:type)
+
+ content_type = options.fetch(:type, DEFAULT_SEND_FILE_TYPE)
+ self.content_type = content_type
+ response.sending_file = true
+
+ raise ArgumentError, ":type option required" if content_type.nil?
+
+ if content_type.is_a?(Symbol)
+ extension = Mime[content_type]
+ raise ArgumentError, "Unknown MIME type #{options[:type]}" unless extension
+ self.content_type = extension
+ else
+ if !type_provided && options[:filename]
+ # If type wasn't provided, try guessing from file extension.
+ content_type = Mime::Type.lookup_by_extension(File.extname(options[:filename]).downcase.delete(".")) || content_type
+ end
+ self.content_type = content_type
+ end
+
+ disposition = options.fetch(:disposition, DEFAULT_SEND_FILE_DISPOSITION)
+ unless disposition.nil?
+ disposition = disposition.to_s
+ disposition += %(; filename="#{options[:filename]}") if options[:filename]
+ headers["Content-Disposition"] = disposition
+ end
+
+ headers["Content-Transfer-Encoding"] = "binary"
+
+ # Fix a problem with IE 6.0 on opening downloaded files:
+ # If Cache-Control: no-cache is set (which Rails does by default),
+ # IE removes the file it just downloaded from its cache immediately
+ # after it displays the "open/save" dialog, which means that if you
+ # hit "open" the file isn't there anymore when the application that
+ # is called for handling the download is run, so let's workaround that
+ response.cache_control[:public] ||= false
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/metal/etag_with_flash.rb b/actionpack/lib/action_controller/metal/etag_with_flash.rb
new file mode 100644
index 0000000000..38899e2f16
--- /dev/null
+++ b/actionpack/lib/action_controller/metal/etag_with_flash.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module ActionController
+ # When you're using the flash, it's generally used as a conditional on the view.
+ # This means the content of the view depends on the flash. Which in turn means
+ # that the ETag for a response should be computed with the content of the flash
+ # in mind. This does that by including the content of the flash as a component
+ # in the ETag that's generated for a response.
+ module EtagWithFlash
+ extend ActiveSupport::Concern
+
+ include ActionController::ConditionalGet
+
+ included do
+ etag { flash unless flash.empty? }
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/metal/etag_with_template_digest.rb b/actionpack/lib/action_controller/metal/etag_with_template_digest.rb
new file mode 100644
index 0000000000..640c75536e
--- /dev/null
+++ b/actionpack/lib/action_controller/metal/etag_with_template_digest.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+module ActionController
+ # When our views change, they should bubble up into HTTP cache freshness
+ # and bust browser caches. So the template digest for the current action
+ # is automatically included in the ETag.
+ #
+ # Enabled by default for apps that use Action View. Disable by setting
+ #
+ # config.action_controller.etag_with_template_digest = false
+ #
+ # Override the template to digest by passing +:template+ to +fresh_when+
+ # and +stale?+ calls. For example:
+ #
+ # # We're going to render widgets/show, not posts/show
+ # fresh_when @post, template: 'widgets/show'
+ #
+ # # We're not going to render a template, so omit it from the ETag.
+ # fresh_when @post, template: false
+ #
+ module EtagWithTemplateDigest
+ extend ActiveSupport::Concern
+
+ include ActionController::ConditionalGet
+
+ included do
+ class_attribute :etag_with_template_digest, default: true
+
+ ActiveSupport.on_load :action_view, yield: true do
+ etag do |options|
+ determine_template_etag(options) if etag_with_template_digest
+ end
+ end
+ end
+
+ private
+ def determine_template_etag(options)
+ if template = pick_template_for_etag(options)
+ lookup_and_digest_template(template)
+ end
+ end
+
+ # Pick the template digest to include in the ETag. If the +:template+ option
+ # is present, use the named template. If +:template+ is +nil+ or absent, use
+ # the default controller/action template. If +:template+ is false, omit the
+ # template digest from the ETag.
+ def pick_template_for_etag(options)
+ unless options[:template] == false
+ options[:template] || "#{controller_path}/#{action_name}"
+ end
+ end
+
+ def lookup_and_digest_template(template)
+ ActionView::Digestor.digest name: template, finder: lookup_context
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/metal/exceptions.rb b/actionpack/lib/action_controller/metal/exceptions.rb
new file mode 100644
index 0000000000..f808295720
--- /dev/null
+++ b/actionpack/lib/action_controller/metal/exceptions.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+module ActionController
+ class ActionControllerError < StandardError #:nodoc:
+ end
+
+ class BadRequest < ActionControllerError #:nodoc:
+ def initialize(msg = nil)
+ super(msg)
+ set_backtrace $!.backtrace if $!
+ end
+ end
+
+ class RenderError < ActionControllerError #:nodoc:
+ end
+
+ class RoutingError < ActionControllerError #:nodoc:
+ attr_reader :failures
+ def initialize(message, failures = [])
+ super(message)
+ @failures = failures
+ end
+ end
+
+ class ActionController::UrlGenerationError < ActionControllerError #:nodoc:
+ end
+
+ class MethodNotAllowed < ActionControllerError #:nodoc:
+ def initialize(*allowed_methods)
+ super("Only #{allowed_methods.to_sentence(locale: :en)} requests are allowed.")
+ end
+ end
+
+ class NotImplemented < MethodNotAllowed #:nodoc:
+ end
+
+ class UnknownController < ActionControllerError #:nodoc:
+ end
+
+ class MissingFile < ActionControllerError #:nodoc:
+ end
+
+ class SessionOverflowError < ActionControllerError #:nodoc:
+ DEFAULT_MESSAGE = "Your session data is larger than the data column in which it is to be stored. You must increase the size of your data column if you intend to store large data."
+
+ def initialize(message = nil)
+ super(message || DEFAULT_MESSAGE)
+ end
+ end
+
+ class UnknownHttpMethod < ActionControllerError #:nodoc:
+ end
+
+ class UnknownFormat < ActionControllerError #:nodoc:
+ end
+end
diff --git a/actionpack/lib/action_controller/metal/flash.rb b/actionpack/lib/action_controller/metal/flash.rb
new file mode 100644
index 0000000000..5115c2fadf
--- /dev/null
+++ b/actionpack/lib/action_controller/metal/flash.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+module ActionController #:nodoc:
+ module Flash
+ extend ActiveSupport::Concern
+
+ included do
+ class_attribute :_flash_types, instance_accessor: false, default: []
+
+ delegate :flash, to: :request
+ add_flash_types(:alert, :notice)
+ end
+
+ module ClassMethods
+ # Creates new flash types. You can pass as many types as you want to create
+ # flash types other than the default <tt>alert</tt> and <tt>notice</tt> in
+ # your controllers and views. For instance:
+ #
+ # # in application_controller.rb
+ # class ApplicationController < ActionController::Base
+ # add_flash_types :warning
+ # end
+ #
+ # # in your controller
+ # redirect_to user_path(@user), warning: "Incomplete profile"
+ #
+ # # in your view
+ # <%= warning %>
+ #
+ # This method will automatically define a new method for each of the given
+ # names, and it will be available in your views.
+ def add_flash_types(*types)
+ types.each do |type|
+ next if _flash_types.include?(type)
+
+ define_method(type) do
+ request.flash[type]
+ end
+ helper_method type
+
+ self._flash_types += [type]
+ end
+ end
+ end
+
+ private
+ def redirect_to(options = {}, response_status_and_flash = {}) #:doc:
+ self.class._flash_types.each do |flash_type|
+ if type = response_status_and_flash.delete(flash_type)
+ flash[flash_type] = type
+ end
+ end
+
+ if other_flashes = response_status_and_flash.delete(:flash)
+ flash.update(other_flashes)
+ end
+
+ super(options, response_status_and_flash)
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/metal/force_ssl.rb b/actionpack/lib/action_controller/metal/force_ssl.rb
new file mode 100644
index 0000000000..0ba1f9f783
--- /dev/null
+++ b/actionpack/lib/action_controller/metal/force_ssl.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/except"
+require "active_support/core_ext/hash/slice"
+
+module ActionController
+ # This module provides a method which will redirect the browser to use the secured HTTPS
+ # protocol. This will ensure that users' sensitive information will be
+ # transferred safely over the internet. You _should_ always force the browser
+ # to use HTTPS when you're transferring sensitive information such as
+ # user authentication, account information, or credit card information.
+ #
+ # Note that if you are really concerned about your application security,
+ # you might consider using +config.force_ssl+ in your config file instead.
+ # That will ensure all the data is transferred via HTTPS, and will
+ # prevent the user from getting their session hijacked when accessing the
+ # site over unsecured HTTP protocol.
+ module ForceSSL
+ extend ActiveSupport::Concern
+ include AbstractController::Callbacks
+
+ ACTION_OPTIONS = [:only, :except, :if, :unless]
+ URL_OPTIONS = [:protocol, :host, :domain, :subdomain, :port, :path]
+ REDIRECT_OPTIONS = [:status, :flash, :alert, :notice]
+
+ module ClassMethods
+ # Force the request to this particular controller or specified actions to be
+ # through the HTTPS protocol.
+ #
+ # If you need to disable this for any reason (e.g. development) then you can use
+ # an +:if+ or +:unless+ condition.
+ #
+ # class AccountsController < ApplicationController
+ # force_ssl if: :ssl_configured?
+ #
+ # def ssl_configured?
+ # !Rails.env.development?
+ # end
+ # end
+ #
+ # ==== URL Options
+ # You can pass any of the following options to affect the redirect url
+ # * <tt>host</tt> - Redirect to a different host name
+ # * <tt>subdomain</tt> - Redirect to a different subdomain
+ # * <tt>domain</tt> - Redirect to a different domain
+ # * <tt>port</tt> - Redirect to a non-standard port
+ # * <tt>path</tt> - Redirect to a different path
+ #
+ # ==== Redirect Options
+ # You can pass any of the following options to affect the redirect status and response
+ # * <tt>status</tt> - Redirect with a custom status (default is 301 Moved Permanently)
+ # * <tt>flash</tt> - Set a flash message when redirecting
+ # * <tt>alert</tt> - Set an alert message when redirecting
+ # * <tt>notice</tt> - Set a notice message when redirecting
+ #
+ # ==== Action Options
+ # You can pass any of the following options to affect the before_action callback
+ # * <tt>only</tt> - The callback should be run only for this action
+ # * <tt>except</tt> - The callback should be run for all actions except this action
+ # * <tt>if</tt> - A symbol naming an instance method or a proc; the
+ # callback will be called only when it returns a true value.
+ # * <tt>unless</tt> - A symbol naming an instance method or a proc; the
+ # callback will be called only when it returns a false value.
+ def force_ssl(options = {})
+ action_options = options.slice(*ACTION_OPTIONS)
+ redirect_options = options.except(*ACTION_OPTIONS)
+ before_action(action_options) do
+ force_ssl_redirect(redirect_options)
+ end
+ end
+ end
+
+ # Redirect the existing request to use the HTTPS protocol.
+ #
+ # ==== Parameters
+ # * <tt>host_or_options</tt> - Either a host name or any of the url and
+ # redirect options available to the <tt>force_ssl</tt> method.
+ def force_ssl_redirect(host_or_options = nil)
+ unless request.ssl?
+ options = {
+ protocol: "https://",
+ host: request.host,
+ path: request.fullpath,
+ status: :moved_permanently
+ }
+
+ if host_or_options.is_a?(Hash)
+ options.merge!(host_or_options)
+ elsif host_or_options
+ options[:host] = host_or_options
+ end
+
+ secure_url = ActionDispatch::Http::URL.url_for(options.slice(*URL_OPTIONS))
+ flash.keep if respond_to?(:flash) && request.respond_to?(:flash)
+ redirect_to secure_url, options.slice(*REDIRECT_OPTIONS)
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/metal/head.rb b/actionpack/lib/action_controller/metal/head.rb
new file mode 100644
index 0000000000..bac9bc5e5f
--- /dev/null
+++ b/actionpack/lib/action_controller/metal/head.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+module ActionController
+ module Head
+ # Returns a response that has no content (merely headers). The options
+ # argument is interpreted to be a hash of header names and values.
+ # This allows you to easily return a response that consists only of
+ # significant headers:
+ #
+ # head :created, location: person_path(@person)
+ #
+ # head :created, location: @person
+ #
+ # It can also be used to return exceptional conditions:
+ #
+ # return head(:method_not_allowed) unless request.post?
+ # return head(:bad_request) unless valid_request?
+ # render
+ #
+ # See Rack::Utils::SYMBOL_TO_STATUS_CODE for a full list of valid +status+ symbols.
+ def head(status, options = {})
+ if status.is_a?(Hash)
+ raise ArgumentError, "#{status.inspect} is not a valid value for `status`."
+ end
+
+ status ||= :ok
+
+ location = options.delete(:location)
+ content_type = options.delete(:content_type)
+
+ options.each do |key, value|
+ headers[key.to_s.dasherize.split("-").each { |v| v[0] = v[0].chr.upcase }.join("-")] = value.to_s
+ end
+
+ self.status = status
+ self.location = url_for(location) if location
+
+ self.response_body = ""
+
+ if include_content?(response_code)
+ self.content_type = content_type || (Mime[formats.first] if formats)
+ response.charset = false
+ end
+
+ true
+ end
+
+ private
+ def include_content?(status)
+ case status
+ when 100..199
+ false
+ when 204, 205, 304
+ false
+ else
+ true
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/metal/helpers.rb b/actionpack/lib/action_controller/metal/helpers.rb
new file mode 100644
index 0000000000..22c84e440b
--- /dev/null
+++ b/actionpack/lib/action_controller/metal/helpers.rb
@@ -0,0 +1,123 @@
+# frozen_string_literal: true
+
+module ActionController
+ # The \Rails framework provides a large number of helpers for working with assets, dates, forms,
+ # numbers and model objects, to name a few. These helpers are available to all templates
+ # by default.
+ #
+ # In addition to using the standard template helpers provided, creating custom helpers to
+ # extract complicated logic or reusable functionality is strongly encouraged. By default, each controller
+ # will include all helpers. These helpers are only accessible on the controller through <tt>#helpers</tt>
+ #
+ # In previous versions of \Rails the controller will include a helper which
+ # matches the name of the controller, e.g., <tt>MyController</tt> will automatically
+ # include <tt>MyHelper</tt>. To return old behavior set +config.action_controller.include_all_helpers+ to +false+.
+ #
+ # Additional helpers can be specified using the +helper+ class method in ActionController::Base or any
+ # controller which inherits from it.
+ #
+ # The +to_s+ method from the \Time class can be wrapped in a helper method to display a custom message if
+ # a \Time object is blank:
+ #
+ # module FormattedTimeHelper
+ # def format_time(time, format=:long, blank_message="&nbsp;")
+ # time.blank? ? blank_message : time.to_s(format)
+ # end
+ # end
+ #
+ # FormattedTimeHelper can now be included in a controller, using the +helper+ class method:
+ #
+ # class EventsController < ActionController::Base
+ # helper FormattedTimeHelper
+ # def index
+ # @events = Event.all
+ # end
+ # end
+ #
+ # Then, in any view rendered by <tt>EventController</tt>, the <tt>format_time</tt> method can be called:
+ #
+ # <% @events.each do |event| -%>
+ # <p>
+ # <%= format_time(event.time, :short, "N/A") %> | <%= event.name %>
+ # </p>
+ # <% end -%>
+ #
+ # Finally, assuming we have two event instances, one which has a time and one which does not,
+ # the output might look like this:
+ #
+ # 23 Aug 11:30 | Carolina Railhawks Soccer Match
+ # N/A | Carolina Railhawks Training Workshop
+ #
+ module Helpers
+ extend ActiveSupport::Concern
+
+ class << self; attr_accessor :helpers_path; end
+ include AbstractController::Helpers
+
+ included do
+ class_attribute :helpers_path, default: []
+ class_attribute :include_all_helpers, default: true
+ end
+
+ module ClassMethods
+ # Declares helper accessors for controller attributes. For example, the
+ # following adds new +name+ and <tt>name=</tt> instance methods to a
+ # controller and makes them available to the view:
+ # attr_accessor :name
+ # helper_attr :name
+ #
+ # ==== Parameters
+ # * <tt>attrs</tt> - Names of attributes to be converted into helpers.
+ def helper_attr(*attrs)
+ attrs.flatten.each { |attr| helper_method(attr, "#{attr}=") }
+ end
+
+ # Provides a proxy to access helper methods from outside the view.
+ def helpers
+ @helper_proxy ||= begin
+ proxy = ActionView::Base.new
+ proxy.config = config.inheritable_copy
+ proxy.extend(_helpers)
+ end
+ end
+
+ # Overwrite modules_for_helpers to accept :all as argument, which loads
+ # all helpers in helpers_path.
+ #
+ # ==== Parameters
+ # * <tt>args</tt> - A list of helpers
+ #
+ # ==== Returns
+ # * <tt>array</tt> - A normalized list of modules for the list of helpers provided.
+ def modules_for_helpers(args)
+ args += all_application_helpers if args.delete(:all)
+ super(args)
+ end
+
+ # Returns a list of helper names in a given path.
+ #
+ # ActionController::Base.all_helpers_from_path 'app/helpers'
+ # # => ["application", "chart", "rubygems"]
+ def all_helpers_from_path(path)
+ helpers = Array(path).flat_map do |_path|
+ extract = /^#{Regexp.quote(_path.to_s)}\/?(.*)_helper.rb$/
+ names = Dir["#{_path}/**/*_helper.rb"].map { |file| file.sub(extract, '\1'.freeze) }
+ names.sort!
+ end
+ helpers.uniq!
+ helpers
+ end
+
+ private
+ # Extract helper names from files in <tt>app/helpers/**/*_helper.rb</tt>
+ def all_application_helpers
+ all_helpers_from_path(helpers_path)
+ end
+ end
+
+ # Provides a proxy to access helper methods from outside the view.
+ def helpers
+ @_helper_proxy ||= view_context
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb
new file mode 100644
index 0000000000..08d9b094f3
--- /dev/null
+++ b/actionpack/lib/action_controller/metal/http_authentication.rb
@@ -0,0 +1,522 @@
+# frozen_string_literal: true
+
+require "base64"
+require "active_support/security_utils"
+
+module ActionController
+ # Makes it dead easy to do HTTP Basic, Digest and Token authentication.
+ module HttpAuthentication
+ # Makes it dead easy to do HTTP \Basic authentication.
+ #
+ # === Simple \Basic example
+ #
+ # class PostsController < ApplicationController
+ # http_basic_authenticate_with name: "dhh", password: "secret", except: :index
+ #
+ # def index
+ # render plain: "Everyone can see me!"
+ # end
+ #
+ # def edit
+ # render plain: "I'm only accessible if you know the password"
+ # end
+ # end
+ #
+ # === Advanced \Basic example
+ #
+ # Here is a more advanced \Basic example where only Atom feeds and the XML API is protected by HTTP authentication,
+ # the regular HTML interface is protected by a session approach:
+ #
+ # class ApplicationController < ActionController::Base
+ # before_action :set_account, :authenticate
+ #
+ # private
+ # def set_account
+ # @account = Account.find_by(url_name: request.subdomains.first)
+ # end
+ #
+ # def authenticate
+ # case request.format
+ # when Mime[:xml], Mime[:atom]
+ # if user = authenticate_with_http_basic { |u, p| @account.users.authenticate(u, p) }
+ # @current_user = user
+ # else
+ # request_http_basic_authentication
+ # end
+ # else
+ # if session_authenticated?
+ # @current_user = @account.users.find(session[:authenticated][:user_id])
+ # else
+ # redirect_to(login_url) and return false
+ # end
+ # end
+ # end
+ # end
+ #
+ # In your integration tests, you can do something like this:
+ #
+ # def test_access_granted_from_xml
+ # @request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials(users(:dhh).name, users(:dhh).password)
+ # get "/notes/1.xml"
+ #
+ # assert_equal 200, status
+ # end
+ module Basic
+ extend self
+
+ module ControllerMethods
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ def http_basic_authenticate_with(options = {})
+ before_action(options.except(:name, :password, :realm)) do
+ authenticate_or_request_with_http_basic(options[:realm] || "Application") do |name, password|
+ # This comparison uses & so that it doesn't short circuit and
+ # uses `variable_size_secure_compare` so that length information
+ # isn't leaked.
+ ActiveSupport::SecurityUtils.variable_size_secure_compare(name, options[:name]) &
+ ActiveSupport::SecurityUtils.variable_size_secure_compare(password, options[:password])
+ end
+ end
+ end
+ end
+
+ def authenticate_or_request_with_http_basic(realm = "Application", message = nil, &login_procedure)
+ authenticate_with_http_basic(&login_procedure) || request_http_basic_authentication(realm, message)
+ end
+
+ def authenticate_with_http_basic(&login_procedure)
+ HttpAuthentication::Basic.authenticate(request, &login_procedure)
+ end
+
+ def request_http_basic_authentication(realm = "Application", message = nil)
+ HttpAuthentication::Basic.authentication_request(self, realm, message)
+ end
+ end
+
+ def authenticate(request, &login_procedure)
+ if has_basic_credentials?(request)
+ login_procedure.call(*user_name_and_password(request))
+ end
+ end
+
+ def has_basic_credentials?(request)
+ request.authorization.present? && (auth_scheme(request).downcase == "basic")
+ end
+
+ def user_name_and_password(request)
+ decode_credentials(request).split(":", 2)
+ end
+
+ def decode_credentials(request)
+ ::Base64.decode64(auth_param(request) || "")
+ end
+
+ def auth_scheme(request)
+ request.authorization.to_s.split(" ", 2).first
+ end
+
+ def auth_param(request)
+ request.authorization.to_s.split(" ", 2).second
+ end
+
+ def encode_credentials(user_name, password)
+ "Basic #{::Base64.strict_encode64("#{user_name}:#{password}")}"
+ end
+
+ def authentication_request(controller, realm, message)
+ message ||= "HTTP Basic: Access denied.\n"
+ controller.headers["WWW-Authenticate"] = %(Basic realm="#{realm.tr('"'.freeze, "".freeze)}")
+ controller.status = 401
+ controller.response_body = message
+ end
+ end
+
+ # Makes it dead easy to do HTTP \Digest authentication.
+ #
+ # === Simple \Digest example
+ #
+ # require 'digest/md5'
+ # class PostsController < ApplicationController
+ # REALM = "SuperSecret"
+ # USERS = {"dhh" => "secret", #plain text password
+ # "dap" => Digest::MD5.hexdigest(["dap",REALM,"secret"].join(":"))} #ha1 digest password
+ #
+ # before_action :authenticate, except: [:index]
+ #
+ # def index
+ # render plain: "Everyone can see me!"
+ # end
+ #
+ # def edit
+ # render plain: "I'm only accessible if you know the password"
+ # end
+ #
+ # private
+ # def authenticate
+ # authenticate_or_request_with_http_digest(REALM) do |username|
+ # USERS[username]
+ # end
+ # end
+ # end
+ #
+ # === Notes
+ #
+ # The +authenticate_or_request_with_http_digest+ block must return the user's password
+ # or the ha1 digest hash so the framework can appropriately hash to check the user's
+ # credentials. Returning +nil+ will cause authentication to fail.
+ #
+ # Storing the ha1 hash: MD5(username:realm:password), is better than storing a plain password. If
+ # the password file or database is compromised, the attacker would be able to use the ha1 hash to
+ # authenticate as the user at this +realm+, but would not have the user's password to try using at
+ # other sites.
+ #
+ # In rare instances, web servers or front proxies strip authorization headers before
+ # they reach your application. You can debug this situation by logging all environment
+ # variables, and check for HTTP_AUTHORIZATION, amongst others.
+ module Digest
+ extend self
+
+ module ControllerMethods
+ def authenticate_or_request_with_http_digest(realm = "Application", message = nil, &password_procedure)
+ authenticate_with_http_digest(realm, &password_procedure) || request_http_digest_authentication(realm, message)
+ end
+
+ # Authenticate with HTTP Digest, returns true or false
+ def authenticate_with_http_digest(realm = "Application", &password_procedure)
+ HttpAuthentication::Digest.authenticate(request, realm, &password_procedure)
+ end
+
+ # Render output including the HTTP Digest authentication header
+ def request_http_digest_authentication(realm = "Application", message = nil)
+ HttpAuthentication::Digest.authentication_request(self, realm, message)
+ end
+ end
+
+ # Returns false on a valid response, true otherwise
+ def authenticate(request, realm, &password_procedure)
+ request.authorization && validate_digest_response(request, realm, &password_procedure)
+ end
+
+ # Returns false unless the request credentials response value matches the expected value.
+ # First try the password as a ha1 digest password. If this fails, then try it as a plain
+ # text password.
+ def validate_digest_response(request, realm, &password_procedure)
+ secret_key = secret_token(request)
+ credentials = decode_credentials_header(request)
+ valid_nonce = validate_nonce(secret_key, request, credentials[:nonce])
+
+ if valid_nonce && realm == credentials[:realm] && opaque(secret_key) == credentials[:opaque]
+ password = password_procedure.call(credentials[:username])
+ return false unless password
+
+ method = request.get_header("rack.methodoverride.original_method") || request.get_header("REQUEST_METHOD")
+ uri = credentials[:uri]
+
+ [true, false].any? do |trailing_question_mark|
+ [true, false].any? do |password_is_ha1|
+ _uri = trailing_question_mark ? uri + "?" : uri
+ expected = expected_response(method, _uri, credentials, password, password_is_ha1)
+ expected == credentials[:response]
+ end
+ end
+ end
+ end
+
+ # Returns the expected response for a request of +http_method+ to +uri+ with the decoded +credentials+ and the expected +password+
+ # Optional parameter +password_is_ha1+ is set to +true+ by default, since best practice is to store ha1 digest instead
+ # of a plain-text password.
+ def expected_response(http_method, uri, credentials, password, password_is_ha1 = true)
+ ha1 = password_is_ha1 ? password : ha1(credentials, password)
+ ha2 = ::Digest::MD5.hexdigest([http_method.to_s.upcase, uri].join(":"))
+ ::Digest::MD5.hexdigest([ha1, credentials[:nonce], credentials[:nc], credentials[:cnonce], credentials[:qop], ha2].join(":"))
+ end
+
+ def ha1(credentials, password)
+ ::Digest::MD5.hexdigest([credentials[:username], credentials[:realm], password].join(":"))
+ end
+
+ def encode_credentials(http_method, credentials, password, password_is_ha1)
+ credentials[:response] = expected_response(http_method, credentials[:uri], credentials, password, password_is_ha1)
+ "Digest " + credentials.sort_by { |x| x[0].to_s }.map { |v| "#{v[0]}='#{v[1]}'" }.join(", ")
+ end
+
+ def decode_credentials_header(request)
+ decode_credentials(request.authorization)
+ end
+
+ def decode_credentials(header)
+ ActiveSupport::HashWithIndifferentAccess[header.to_s.gsub(/^Digest\s+/, "").split(",").map do |pair|
+ key, value = pair.split("=", 2)
+ [key.strip, value.to_s.gsub(/^"|"$/, "").delete('\'')]
+ end]
+ end
+
+ def authentication_header(controller, realm)
+ secret_key = secret_token(controller.request)
+ nonce = self.nonce(secret_key)
+ opaque = opaque(secret_key)
+ controller.headers["WWW-Authenticate"] = %(Digest realm="#{realm}", qop="auth", algorithm=MD5, nonce="#{nonce}", opaque="#{opaque}")
+ end
+
+ def authentication_request(controller, realm, message = nil)
+ message ||= "HTTP Digest: Access denied.\n"
+ authentication_header(controller, realm)
+ controller.status = 401
+ controller.response_body = message
+ end
+
+ def secret_token(request)
+ key_generator = request.key_generator
+ http_auth_salt = request.http_auth_salt
+ key_generator.generate_key(http_auth_salt)
+ end
+
+ # Uses an MD5 digest based on time to generate a value to be used only once.
+ #
+ # A server-specified data string which should be uniquely generated each time a 401 response is made.
+ # It is recommended that this string be base64 or hexadecimal data.
+ # Specifically, since the string is passed in the header lines as a quoted string, the double-quote character is not allowed.
+ #
+ # The contents of the nonce are implementation dependent.
+ # The quality of the implementation depends on a good choice.
+ # A nonce might, for example, be constructed as the base 64 encoding of
+ #
+ # time-stamp H(time-stamp ":" ETag ":" private-key)
+ #
+ # where time-stamp is a server-generated time or other non-repeating value,
+ # ETag is the value of the HTTP ETag header associated with the requested entity,
+ # and private-key is data known only to the server.
+ # With a nonce of this form a server would recalculate the hash portion after receiving the client authentication header and
+ # reject the request if it did not match the nonce from that header or
+ # if the time-stamp value is not recent enough. In this way the server can limit the time of the nonce's validity.
+ # The inclusion of the ETag prevents a replay request for an updated version of the resource.
+ # (Note: including the IP address of the client in the nonce would appear to offer the server the ability
+ # to limit the reuse of the nonce to the same client that originally got it.
+ # However, that would break proxy farms, where requests from a single user often go through different proxies in the farm.
+ # Also, IP address spoofing is not that hard.)
+ #
+ # An implementation might choose not to accept a previously used nonce or a previously used digest, in order to
+ # protect against a replay attack. Or, an implementation might choose to use one-time nonces or digests for
+ # POST, PUT, or PATCH requests and a time-stamp for GET requests. For more details on the issues involved see Section 4
+ # of this document.
+ #
+ # The nonce is opaque to the client. Composed of Time, and hash of Time with secret
+ # key from the Rails session secret generated upon creation of project. Ensures
+ # the time cannot be modified by client.
+ def nonce(secret_key, time = Time.now)
+ t = time.to_i
+ hashed = [t, secret_key]
+ digest = ::Digest::MD5.hexdigest(hashed.join(":"))
+ ::Base64.strict_encode64("#{t}:#{digest}")
+ end
+
+ # Might want a shorter timeout depending on whether the request
+ # is a PATCH, PUT, or POST, and if the client is a browser or web service.
+ # Can be much shorter if the Stale directive is implemented. This would
+ # allow a user to use new nonce without prompting the user again for their
+ # username and password.
+ def validate_nonce(secret_key, request, value, seconds_to_timeout = 5 * 60)
+ return false if value.nil?
+ t = ::Base64.decode64(value).split(":").first.to_i
+ nonce(secret_key, t) == value && (t - Time.now.to_i).abs <= seconds_to_timeout
+ end
+
+ # Opaque based on digest of secret key
+ def opaque(secret_key)
+ ::Digest::MD5.hexdigest(secret_key)
+ end
+ end
+
+ # Makes it dead easy to do HTTP Token authentication.
+ #
+ # Simple Token example:
+ #
+ # class PostsController < ApplicationController
+ # TOKEN = "secret"
+ #
+ # before_action :authenticate, except: [ :index ]
+ #
+ # def index
+ # render plain: "Everyone can see me!"
+ # end
+ #
+ # def edit
+ # render plain: "I'm only accessible if you know the password"
+ # end
+ #
+ # private
+ # def authenticate
+ # authenticate_or_request_with_http_token do |token, options|
+ # # 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
+ #
+ #
+ # Here is a more advanced Token example where only Atom feeds and the XML API is protected by HTTP token authentication,
+ # the regular HTML interface is protected by a session approach:
+ #
+ # class ApplicationController < ActionController::Base
+ # before_action :set_account, :authenticate
+ #
+ # private
+ # def set_account
+ # @account = Account.find_by(url_name: request.subdomains.first)
+ # end
+ #
+ # def authenticate
+ # case request.format
+ # when Mime[:xml], Mime[:atom]
+ # if user = authenticate_with_http_token { |t, o| @account.users.authenticate(t, o) }
+ # @current_user = user
+ # else
+ # request_http_token_authentication
+ # end
+ # else
+ # if session_authenticated?
+ # @current_user = @account.users.find(session[:authenticated][:user_id])
+ # else
+ # redirect_to(login_url) and return false
+ # end
+ # end
+ # end
+ # end
+ #
+ #
+ # In your integration tests, you can do something like this:
+ #
+ # def test_access_granted_from_xml
+ # get(
+ # "/notes/1.xml", nil,
+ # 'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Token.encode_credentials(users(:dhh).token)
+ # )
+ #
+ # assert_equal 200, status
+ # end
+ #
+ #
+ # On shared hosts, Apache sometimes doesn't pass authentication headers to
+ # FCGI instances. If your environment matches this description and you cannot
+ # authenticate, try this rule in your Apache setup:
+ #
+ # RewriteRule ^(.*)$ dispatch.fcgi [E=X-HTTP_AUTHORIZATION:%{HTTP:Authorization},QSA,L]
+ module Token
+ TOKEN_KEY = "token="
+ TOKEN_REGEX = /^(Token|Bearer)\s+/
+ AUTHN_PAIR_DELIMITERS = /(?:,|;|\t+)/
+ extend self
+
+ module ControllerMethods
+ def authenticate_or_request_with_http_token(realm = "Application", message = nil, &login_procedure)
+ authenticate_with_http_token(&login_procedure) || request_http_token_authentication(realm, message)
+ end
+
+ def authenticate_with_http_token(&login_procedure)
+ Token.authenticate(self, &login_procedure)
+ end
+
+ def request_http_token_authentication(realm = "Application", message = nil)
+ Token.authentication_request(self, realm, message)
+ end
+ end
+
+ # If token Authorization header is present, call the login
+ # procedure with the present token and options.
+ #
+ # [controller]
+ # ActionController::Base instance for the current request.
+ #
+ # [login_procedure]
+ # Proc to call if a token is present. The Proc should take two arguments:
+ #
+ # authenticate(controller) { |token, options| ... }
+ #
+ # Returns the return value of <tt>login_procedure</tt> if a
+ # token is found. Returns <tt>nil</tt> if no token is found.
+
+ def authenticate(controller, &login_procedure)
+ token, options = token_and_options(controller.request)
+ unless token.blank?
+ login_procedure.call(token, options)
+ end
+ end
+
+ # Parses the token and options out of the token Authorization header.
+ # The value for the Authorization header is expected to have the prefix
+ # <tt>"Token"</tt> or <tt>"Bearer"</tt>. If the header looks like this:
+ # Authorization: Token token="abc", nonce="def"
+ # Then the returned token is <tt>"abc"</tt>, and the options are
+ # <tt>{nonce: "def"}</tt>
+ #
+ # request - ActionDispatch::Request instance with the current headers.
+ #
+ # Returns an +Array+ of <tt>[String, Hash]</tt> if a token is present.
+ # Returns +nil+ if no token is found.
+ def token_and_options(request)
+ authorization_request = request.authorization.to_s
+ if authorization_request[TOKEN_REGEX]
+ params = token_params_from authorization_request
+ [params.shift[1], Hash[params].with_indifferent_access]
+ end
+ end
+
+ def token_params_from(auth)
+ rewrite_param_values params_array_from raw_params auth
+ end
+
+ # Takes raw_params and turns it into an array of parameters
+ def params_array_from(raw_params)
+ raw_params.map { |param| param.split %r/=(.+)?/ }
+ end
+
+ # This removes the <tt>"</tt> characters wrapping the value.
+ def rewrite_param_values(array_params)
+ array_params.each { |param| (param[1] || "".dup).gsub! %r/^"|"$/, "" }
+ end
+
+ # This method takes an authorization body and splits up the key-value
+ # pairs by the standardized <tt>:</tt>, <tt>;</tt>, or <tt>\t</tt>
+ # delimiters defined in +AUTHN_PAIR_DELIMITERS+.
+ def raw_params(auth)
+ _raw_params = auth.sub(TOKEN_REGEX, "").split(/\s*#{AUTHN_PAIR_DELIMITERS}\s*/)
+
+ if !(_raw_params.first =~ %r{\A#{TOKEN_KEY}})
+ _raw_params[0] = "#{TOKEN_KEY}#{_raw_params.first}"
+ end
+
+ _raw_params
+ end
+
+ # Encodes the given token and options into an Authorization header value.
+ #
+ # token - String token.
+ # options - optional Hash of the options.
+ #
+ # Returns String.
+ def encode_credentials(token, options = {})
+ values = ["#{TOKEN_KEY}#{token.to_s.inspect}"] + options.map do |key, value|
+ "#{key}=#{value.to_s.inspect}"
+ end
+ "Token #{values * ", "}"
+ end
+
+ # Sets a WWW-Authenticate header to let the client know a token is desired.
+ #
+ # controller - ActionController::Base instance for the outgoing response.
+ # realm - String realm to use in the header.
+ #
+ # Returns nothing.
+ def authentication_request(controller, realm, message = nil)
+ message ||= "HTTP Token: Access denied.\n"
+ controller.headers["WWW-Authenticate"] = %(Token realm="#{realm.tr('"'.freeze, "".freeze)}")
+ controller.__send__ :render, plain: message, status: :unauthorized
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/metal/implicit_render.rb b/actionpack/lib/action_controller/metal/implicit_render.rb
new file mode 100644
index 0000000000..ac0c127cdc
--- /dev/null
+++ b/actionpack/lib/action_controller/metal/implicit_render.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+module ActionController
+ # Handles implicit rendering for a controller action that does not
+ # explicitly respond with +render+, +respond_to+, +redirect+, or +head+.
+ #
+ # For API controllers, the implicit response is always <tt>204 No Content</tt>.
+ #
+ # 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
+ # <tt>204 No Content</tt>:
+ #
+ # 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 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 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, if we DON'T find a template AND the request isn't a browser page
+ # load, then we implicitly respond with <tt>204 No Content</tt>.
+ module ImplicitRender
+ # :stopdoc:
+ include BasicImplicitRender
+
+ def default_render(*args)
+ 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} 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 = "#{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
+ logger.info "No template found for #{self.class.name}\##{action_name}, rendering head :no_content" if logger
+ super
+ end
+ end
+
+ def method_for_action(action_name)
+ super || if template_exists?(action_name.to_s, _prefixes)
+ "default_render"
+ end
+ end
+
+ private
+ def interactive_browser_request?
+ 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
new file mode 100644
index 0000000000..476f0843b2
--- /dev/null
+++ b/actionpack/lib/action_controller/metal/instrumentation.rb
@@ -0,0 +1,111 @@
+# frozen_string_literal: true
+
+require "benchmark"
+require "abstract_controller/logger"
+
+module ActionController
+ # Adds instrumentation to several ends in ActionController::Base. It also provides
+ # some hooks related with process_action. This allows an ORM like Active Record
+ # and/or DataMapper to plug in ActionController and show related information.
+ #
+ # Check ActiveRecord::Railties::ControllerRuntime for an example.
+ module Instrumentation
+ extend ActiveSupport::Concern
+
+ include AbstractController::Logger
+
+ attr_internal :view_runtime
+
+ def process_action(*args)
+ raw_payload = {
+ controller: self.class.name,
+ action: action_name,
+ params: request.filtered_parameters,
+ headers: request.headers,
+ format: request.format.ref,
+ method: request.request_method,
+ path: request.fullpath
+ }
+
+ ActiveSupport::Notifications.instrument("start_processing.action_controller", raw_payload.dup)
+
+ ActiveSupport::Notifications.instrument("process_action.action_controller", raw_payload) do |payload|
+ begin
+ result = super
+ payload[:status] = response.status
+ result
+ ensure
+ append_info_to_payload(payload)
+ end
+ end
+ end
+
+ def render(*args)
+ render_output = nil
+ self.view_runtime = cleanup_view_runtime do
+ Benchmark.ms { render_output = super }
+ end
+ render_output
+ end
+
+ def send_file(path, options = {})
+ ActiveSupport::Notifications.instrument("send_file.action_controller",
+ options.merge(path: path)) do
+ super
+ end
+ end
+
+ def send_data(data, options = {})
+ ActiveSupport::Notifications.instrument("send_data.action_controller", options) do
+ super
+ end
+ end
+
+ def redirect_to(*args)
+ ActiveSupport::Notifications.instrument("redirect_to.action_controller") do |payload|
+ result = super
+ payload[:status] = response.status
+ payload[:location] = response.filtered_location
+ result
+ end
+ end
+
+ private
+
+ # A hook invoked every time a before callback is halted.
+ def halted_callback_hook(filter)
+ ActiveSupport::Notifications.instrument("halted_callback.action_controller", filter: filter)
+ end
+
+ # A hook which allows you to clean up any time, wrongly taken into account in
+ # views, like database querying time.
+ #
+ # def cleanup_view_runtime
+ # super - time_taken_in_something_expensive
+ # end
+ #
+ # :api: plugin
+ def cleanup_view_runtime
+ yield
+ end
+
+ # Every time after an action is processed, this method is invoked
+ # with the payload, so you can add more information.
+ # :api: plugin
+ def append_info_to_payload(payload)
+ payload[:view_runtime] = view_runtime
+ end
+
+ module ClassMethods
+ # A hook which allows other frameworks to log what happened during
+ # controller process action. This method should return an array
+ # with the messages to be added.
+ # :api: plugin
+ def log_process_action(payload) #:nodoc:
+ messages, view_runtime = [], payload[:view_runtime]
+ messages << ("Views: %.1fms" % view_runtime.to_f) if view_runtime
+ messages
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/metal/live.rb b/actionpack/lib/action_controller/metal/live.rb
new file mode 100644
index 0000000000..2f4c8fb83c
--- /dev/null
+++ b/actionpack/lib/action_controller/metal/live.rb
@@ -0,0 +1,312 @@
+# frozen_string_literal: true
+
+require "action_dispatch/http/response"
+require "delegate"
+require "active_support/json"
+
+module ActionController
+ # Mix this module into your controller, and all actions in that controller
+ # will be able to stream data to the client as it's written.
+ #
+ # class MyController < ActionController::Base
+ # include ActionController::Live
+ #
+ # def stream
+ # response.headers['Content-Type'] = 'text/event-stream'
+ # 100.times {
+ # response.stream.write "hello world\n"
+ # sleep 1
+ # }
+ # ensure
+ # response.stream.close
+ # end
+ # end
+ #
+ # There are a few caveats with this module. You *cannot* write headers after the
+ # response has been committed (Response#committed? will return truthy).
+ # Calling +write+ or +close+ on the response stream will cause the response
+ # object to be committed. Make sure all headers are set before calling write
+ # or close on your stream.
+ #
+ # You *must* call close on your stream when you're finished, otherwise the
+ # socket may be left open forever.
+ #
+ # The final caveat is that your actions are executed in a separate thread than
+ # the main thread. Make sure your actions are thread safe, and this shouldn't
+ # be a problem (don't share state across threads, etc).
+ module Live
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ def make_response!(request)
+ if request.get_header("HTTP_VERSION") == "HTTP/1.0"
+ super
+ else
+ Live::Response.new.tap do |res|
+ res.request = request
+ end
+ end
+ end
+ end
+
+ # This class provides the ability to write an SSE (Server Sent Event)
+ # to an IO stream. The class is initialized with a stream and can be used
+ # to either write a JSON string or an object which can be converted to JSON.
+ #
+ # Writing an object will convert it into standard SSE format with whatever
+ # options you have configured. You may choose to set the following options:
+ #
+ # 1) Event. If specified, an event with this name will be dispatched on
+ # the browser.
+ # 2) Retry. The reconnection time in milliseconds used when attempting
+ # to send the event.
+ # 3) Id. If the connection dies while sending an SSE to the browser, then
+ # the server will receive a +Last-Event-ID+ header with value equal to +id+.
+ #
+ # After setting an option in the constructor of the SSE object, all future
+ # SSEs sent across the stream will use those options unless overridden.
+ #
+ # Example Usage:
+ #
+ # class MyController < ActionController::Base
+ # include ActionController::Live
+ #
+ # def index
+ # response.headers['Content-Type'] = 'text/event-stream'
+ # sse = SSE.new(response.stream, retry: 300, event: "event-name")
+ # sse.write({ name: 'John'})
+ # sse.write({ name: 'John'}, id: 10)
+ # sse.write({ name: 'John'}, id: 10, event: "other-event")
+ # sse.write({ name: 'John'}, id: 10, event: "other-event", retry: 500)
+ # ensure
+ # sse.close
+ # end
+ # end
+ #
+ # Note: SSEs are not currently supported by IE. However, they are supported
+ # by Chrome, Firefox, Opera, and Safari.
+ class SSE
+ WHITELISTED_OPTIONS = %w( retry event id )
+
+ def initialize(stream, options = {})
+ @stream = stream
+ @options = options
+ end
+
+ def close
+ @stream.close
+ end
+
+ def write(object, options = {})
+ case object
+ when String
+ perform_write(object, options)
+ else
+ perform_write(ActiveSupport::JSON.encode(object), options)
+ end
+ end
+
+ private
+
+ def perform_write(json, options)
+ current_options = @options.merge(options).stringify_keys
+
+ WHITELISTED_OPTIONS.each do |option_name|
+ if (option_value = current_options[option_name])
+ @stream.write "#{option_name}: #{option_value}\n"
+ end
+ end
+
+ message = json.gsub("\n".freeze, "\ndata: ".freeze)
+ @stream.write "data: #{message}\n\n"
+ end
+ end
+
+ class ClientDisconnected < RuntimeError
+ end
+
+ class Buffer < ActionDispatch::Response::Buffer #:nodoc:
+ include MonitorMixin
+
+ # Ignore that the client has disconnected.
+ #
+ # If this value is `true`, calling `write` after the client
+ # disconnects will result in the written content being silently
+ # discarded. If this value is `false` (the default), a
+ # ClientDisconnected exception will be raised.
+ attr_accessor :ignore_disconnect
+
+ def initialize(response)
+ @error_callback = lambda { true }
+ @cv = new_cond
+ @aborted = false
+ @ignore_disconnect = false
+ super(response, SizedQueue.new(10))
+ end
+
+ def write(string)
+ unless @response.committed?
+ @response.set_header "Cache-Control", "no-cache"
+ @response.delete_header "Content-Length"
+ end
+
+ super
+
+ unless connected?
+ @buf.clear
+
+ unless @ignore_disconnect
+ # Raise ClientDisconnected, which is a RuntimeError (not an
+ # IOError), because that's more appropriate for something beyond
+ # the developer's control.
+ raise ClientDisconnected, "client disconnected"
+ end
+ end
+ end
+
+ # Write a 'close' event to the buffer; the producer/writing thread
+ # uses this to notify us that it's finished supplying content.
+ #
+ # See also #abort.
+ def close
+ synchronize do
+ super
+ @buf.push nil
+ @cv.broadcast
+ end
+ end
+
+ # Inform the producer/writing thread that the client has
+ # disconnected; the reading thread is no longer interested in
+ # anything that's being written.
+ #
+ # See also #close.
+ def abort
+ synchronize do
+ @aborted = true
+ @buf.clear
+ end
+ end
+
+ # Is the client still connected and waiting for content?
+ #
+ # The result of calling `write` when this is `false` is determined
+ # by `ignore_disconnect`.
+ def connected?
+ !@aborted
+ end
+
+ def on_error(&block)
+ @error_callback = block
+ end
+
+ def call_on_error
+ @error_callback.call
+ end
+
+ private
+
+ def each_chunk(&block)
+ loop do
+ str = nil
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
+ str = @buf.pop
+ end
+ break unless str
+ yield str
+ end
+ end
+ end
+
+ class Response < ActionDispatch::Response #:nodoc: all
+ private
+
+ def before_committed
+ super
+ jar = request.cookie_jar
+ # The response can be committed multiple times
+ jar.write self unless committed?
+ end
+
+ def build_buffer(response, body)
+ buf = Live::Buffer.new response
+ body.each { |part| buf.write part }
+ buf
+ end
+ end
+
+ def process(name)
+ t1 = Thread.current
+ locals = t1.keys.map { |key| [key, t1[key]] }
+
+ error = nil
+ # This processes the action in a child thread. It lets us return the
+ # response code and headers back up the Rack stack, and still process
+ # the body in parallel with sending data to the client.
+ new_controller_thread {
+ ActiveSupport::Dependencies.interlock.running do
+ t2 = Thread.current
+
+ # Since we're processing the view in a different thread, copy the
+ # thread locals from the main thread to the child thread. :'(
+ locals.each { |k, v| t2[k] = v }
+
+ begin
+ super(name)
+ rescue => e
+ if @_response.committed?
+ begin
+ @_response.stream.write(ActionView::Base.streaming_completion_on_exception) if request.format == :html
+ @_response.stream.call_on_error
+ rescue => exception
+ log_error(exception)
+ ensure
+ log_error(e)
+ @_response.stream.close
+ end
+ else
+ error = e
+ end
+ ensure
+ @_response.commit!
+ end
+ end
+ }
+
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
+ @_response.await_commit
+ end
+
+ raise error if error
+ end
+
+ # Spawn a new thread to serve up the controller in. This is to get
+ # around the fact that Rack isn't based around IOs and we need to use
+ # a thread to stream data from the response bodies. Nobody should call
+ # this method except in Rails internals. Seriously!
+ def new_controller_thread # :nodoc:
+ Thread.new {
+ t2 = Thread.current
+ t2.abort_on_exception = true
+ yield
+ }
+ end
+
+ def log_error(exception)
+ logger = ActionController::Base.logger
+ return unless logger
+
+ logger.fatal do
+ message = "\n#{exception.class} (#{exception.message}):\n".dup
+ message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code)
+ message << " " << exception.backtrace.join("\n ")
+ "#{message}\n\n"
+ end
+ end
+
+ def response_body=(body)
+ super
+ response.close if response
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb
new file mode 100644
index 0000000000..2233b93406
--- /dev/null
+++ b/actionpack/lib/action_controller/metal/mime_responds.rb
@@ -0,0 +1,313 @@
+# frozen_string_literal: true
+
+require "abstract_controller/collector"
+
+module ActionController #:nodoc:
+ module MimeResponds
+ # Without web-service support, an action which collects the data for displaying a list of people
+ # might look something like this:
+ #
+ # def index
+ # @people = Person.all
+ # end
+ #
+ # That action implicitly responds to all formats, but formats can also be whitelisted:
+ #
+ # def index
+ # @people = Person.all
+ # respond_to :html, :js
+ # end
+ #
+ # Here's the same action, with web-service support baked in:
+ #
+ # def index
+ # @people = Person.all
+ #
+ # respond_to do |format|
+ # format.html
+ # format.js
+ # format.xml { render xml: @people }
+ # end
+ # end
+ #
+ # What that says is, "if the client wants HTML or JS in response to this action, just respond as we
+ # would have before, but if the client wants XML, return them the list of people in XML format."
+ # (Rails determines the desired response format from the HTTP Accept header submitted by the client.)
+ #
+ # Supposing you have an action that adds a new person, optionally creating their company
+ # (by name) if it does not already exist, without web-services, it might look like this:
+ #
+ # def create
+ # @company = Company.find_or_create_by(name: params[:company][:name])
+ # @person = @company.people.create(params[:person])
+ #
+ # redirect_to(person_list_url)
+ # end
+ #
+ # Here's the same action, with web-service support baked in:
+ #
+ # def create
+ # company = params[:person].delete(:company)
+ # @company = Company.find_or_create_by(name: company[:name])
+ # @person = @company.people.create(params[:person])
+ #
+ # respond_to do |format|
+ # format.html { redirect_to(person_list_url) }
+ # format.js
+ # format.xml { render xml: @person.to_xml(include: @company) }
+ # end
+ # end
+ #
+ # If the client wants HTML, we just redirect them back to the person list. If they want JavaScript,
+ # then it is an Ajax request and we render the JavaScript template associated with this action.
+ # Lastly, if the client wants XML, we render the created person as XML, but with a twist: we also
+ # include the person's company in the rendered XML, so you get something like this:
+ #
+ # <person>
+ # <id>...</id>
+ # ...
+ # <company>
+ # <id>...</id>
+ # <name>...</name>
+ # ...
+ # </company>
+ # </person>
+ #
+ # Note, however, the extra bit at the top of that action:
+ #
+ # company = params[:person].delete(:company)
+ # @company = Company.find_or_create_by(name: company[:name])
+ #
+ # This is because the incoming XML document (if a web-service request is in process) can only contain a
+ # single root-node. So, we have to rearrange things so that the request looks like this (url-encoded):
+ #
+ # person[name]=...&person[company][name]=...&...
+ #
+ # And, like this (xml-encoded):
+ #
+ # <person>
+ # <name>...</name>
+ # <company>
+ # <name>...</name>
+ # </company>
+ # </person>
+ #
+ # In other words, we make the request so that it operates on a single entity's person. Then, in the action,
+ # we extract the company data from the request, find or create the company, and then create the new person
+ # with the remaining data.
+ #
+ # Note that you can define your own XML parameter parser which would allow you to describe multiple entities
+ # in a single request (i.e., by wrapping them all in a single root node), but if you just go with the flow
+ # and accept Rails' defaults, life will be much easier.
+ #
+ # If you need to use a MIME type which isn't supported by default, you can register your own handlers in
+ # +config/initializers/mime_types.rb+ as follows.
+ #
+ # Mime::Type.register "image/jpg", :jpg
+ #
+ # Respond to also allows you to specify a common block for different formats by using +any+:
+ #
+ # def index
+ # @people = Person.all
+ #
+ # respond_to do |format|
+ # format.html
+ # format.any(:xml, :json) { render request.format.to_sym => @people }
+ # end
+ # end
+ #
+ # In the example above, if the format is xml, it will render:
+ #
+ # render xml: @people
+ #
+ # Or if the format is json:
+ #
+ # render json: @people
+ #
+ # Formats can have different variants.
+ #
+ # The request variant is a specialization of the request format, like <tt>:tablet</tt>,
+ # <tt>:phone</tt>, or <tt>:desktop</tt>.
+ #
+ # We often want to render different html/json/xml templates for phones,
+ # tablets, and desktop browsers. Variants make it easy.
+ #
+ # You can set the variant in a +before_action+:
+ #
+ # request.variant = :tablet if request.user_agent =~ /iPad/
+ #
+ # Respond to variants in the action just like you respond to formats:
+ #
+ # respond_to do |format|
+ # format.html do |variant|
+ # variant.tablet # renders app/views/projects/show.html+tablet.erb
+ # variant.phone { extra_setup; render ... }
+ # variant.none { special_setup } # executed only if there is no variant set
+ # end
+ # end
+ #
+ # Provide separate templates for each format and variant:
+ #
+ # app/views/projects/show.html.erb
+ # app/views/projects/show.html+tablet.erb
+ # app/views/projects/show.html+phone.erb
+ #
+ # When you're not sharing any code within the format, you can simplify defining variants
+ # using the inline syntax:
+ #
+ # respond_to do |format|
+ # format.js { render "trash" }
+ # format.html.phone { redirect_to progress_path }
+ # format.html.none { render "trash" }
+ # end
+ #
+ # Variants also support common +any+/+all+ block that formats have.
+ #
+ # It works for both inline:
+ #
+ # respond_to do |format|
+ # format.html.any { render html: "any" }
+ # format.html.phone { render html: "phone" }
+ # end
+ #
+ # and block syntax:
+ #
+ # respond_to do |format|
+ # format.html do |variant|
+ # variant.any(:tablet, :phablet){ render html: "any" }
+ # variant.phone { render html: "phone" }
+ # end
+ # end
+ #
+ # You can also set an array of variants:
+ #
+ # request.variant = [:tablet, :phone]
+ #
+ # This will work similarly to formats and MIME types negotiation. If there
+ # is no +:tablet+ variant declared, the +:phone+ variant will be used:
+ #
+ # respond_to do |format|
+ # format.html.none
+ # format.html.phone # this gets rendered
+ # end
+ def respond_to(*mimes)
+ raise ArgumentError, "respond_to takes either types or a block, never both" if mimes.any? && block_given?
+
+ collector = Collector.new(mimes, request.variant)
+ yield collector if block_given?
+
+ if format = collector.negotiate_format(request)
+ _process_format(format)
+ _set_rendered_content_type format
+ response = collector.response
+ response.call if response
+ else
+ raise ActionController::UnknownFormat
+ end
+ end
+
+ # A container for responses available from the current controller for
+ # requests for different mime-types sent to a particular action.
+ #
+ # The public controller methods +respond_to+ may be called with a block
+ # that is used to define responses to different mime-types, e.g.
+ # for +respond_to+ :
+ #
+ # respond_to do |format|
+ # format.html
+ # format.xml { render xml: @people }
+ # end
+ #
+ # In this usage, the argument passed to the block (+format+ above) is an
+ # instance of the ActionController::MimeResponds::Collector class. This
+ # object serves as a container in which available responses can be stored by
+ # calling any of the dynamically generated, mime-type-specific methods such
+ # as +html+, +xml+ etc on the Collector. Each response is represented by a
+ # corresponding block if present.
+ #
+ # A subsequent call to #negotiate_format(request) will enable the Collector
+ # to determine which specific mime-type it should respond with for the current
+ # request, with this response then being accessible by calling #response.
+ class Collector
+ include AbstractController::Collector
+ attr_accessor :format
+
+ def initialize(mimes, variant = nil)
+ @responses = {}
+ @variant = variant
+
+ mimes.each { |mime| @responses[Mime[mime]] = nil }
+ end
+
+ def any(*args, &block)
+ if args.any?
+ args.each { |type| send(type, &block) }
+ else
+ custom(Mime::ALL, &block)
+ end
+ end
+ alias :all :any
+
+ def custom(mime_type, &block)
+ mime_type = Mime::Type.lookup(mime_type.to_s) unless mime_type.is_a?(Mime::Type)
+ @responses[mime_type] ||= if block_given?
+ block
+ else
+ VariantCollector.new(@variant)
+ end
+ end
+
+ def response
+ response = @responses.fetch(format, @responses[Mime::ALL])
+ if response.is_a?(VariantCollector) # `format.html.phone` - variant inline syntax
+ response.variant
+ elsif response.nil? || response.arity == 0 # `format.html` - just a format, call its block
+ response
+ else # `format.html{ |variant| variant.phone }` - variant block syntax
+ variant_collector = VariantCollector.new(@variant)
+ response.call(variant_collector) # call format block with variants collector
+ variant_collector.variant
+ end
+ end
+
+ def negotiate_format(request)
+ @format = request.negotiate_mime(@responses.keys)
+ end
+
+ class VariantCollector #:nodoc:
+ def initialize(variant = nil)
+ @variant = variant
+ @variants = {}
+ end
+
+ def any(*args, &block)
+ if block_given?
+ if args.any? && args.none? { |a| a == @variant }
+ args.each { |v| @variants[v] = block }
+ else
+ @variants[:any] = block
+ end
+ end
+ end
+ alias :all :any
+
+ def method_missing(name, *args, &block)
+ @variants[name] = block if block_given?
+ end
+
+ def variant
+ if @variant.empty?
+ @variants[:none] || @variants[:any]
+ else
+ @variants[variant_key]
+ end
+ end
+
+ private
+ def variant_key
+ @variant.find { |variant| @variants.key?(variant) } || :any
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/metal/parameter_encoding.rb b/actionpack/lib/action_controller/metal/parameter_encoding.rb
new file mode 100644
index 0000000000..7a45732d31
--- /dev/null
+++ b/actionpack/lib/action_controller/metal/parameter_encoding.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+module ActionController
+ # Specify binary encoding for parameters for a given action.
+ module ParameterEncoding
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ def inherited(klass) # :nodoc:
+ super
+ klass.setup_param_encode
+ end
+
+ def setup_param_encode # :nodoc:
+ @_parameter_encodings = {}
+ end
+
+ def binary_params_for?(action) # :nodoc:
+ @_parameter_encodings[action.to_s]
+ end
+
+ # Specify that a given action's parameters should all be encoded as
+ # ASCII-8BIT (it "skips" the encoding default of UTF-8).
+ #
+ # For example, a controller would use it like this:
+ #
+ # class RepositoryController < ActionController::Base
+ # skip_parameter_encoding :show
+ #
+ # def show
+ # @repo = Repository.find_by_filesystem_path params[:file_path]
+ #
+ # # `repo_name` is guaranteed to be UTF-8, but was ASCII-8BIT, so
+ # # tag it as such
+ # @repo_name = params[:repo_name].force_encoding 'UTF-8'
+ # end
+ #
+ # def index
+ # @repositories = Repository.all
+ # end
+ # end
+ #
+ # The show action in the above controller would have all parameter values
+ # encoded as ASCII-8BIT. This is useful in the case where an application
+ # must handle data but encoding of the data is unknown, like file system data.
+ def skip_parameter_encoding(action)
+ @_parameter_encodings[action.to_s] = true
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/metal/params_wrapper.rb b/actionpack/lib/action_controller/metal/params_wrapper.rb
new file mode 100644
index 0000000000..f4f2381286
--- /dev/null
+++ b/actionpack/lib/action_controller/metal/params_wrapper.rb
@@ -0,0 +1,285 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/slice"
+require "active_support/core_ext/hash/except"
+require "active_support/core_ext/module/anonymous"
+require "action_dispatch/http/mime_type"
+
+module ActionController
+ # Wraps the parameters hash into a nested hash. This will allow clients to
+ # submit requests without having to specify any root elements.
+ #
+ # This functionality is enabled in +config/initializers/wrap_parameters.rb+
+ # and can be customized.
+ #
+ # You could also turn it on per controller by setting the format array to
+ # a non-empty array:
+ #
+ # class UsersController < ApplicationController
+ # wrap_parameters format: [:json, :xml, :url_encoded_form, :multipart_form]
+ # end
+ #
+ # If you enable +ParamsWrapper+ for +:json+ format, instead of having to
+ # send JSON parameters like this:
+ #
+ # {"user": {"name": "Konata"}}
+ #
+ # You can send parameters like this:
+ #
+ # {"name": "Konata"}
+ #
+ # And it will be wrapped into a nested hash with the key name matching the
+ # controller's name. For example, if you're posting to +UsersController+,
+ # your new +params+ hash will look like this:
+ #
+ # {"name" => "Konata", "user" => {"name" => "Konata"}}
+ #
+ # You can also specify the key in which the parameters should be wrapped to,
+ # and also the list of attributes it should wrap by using either +:include+ or
+ # +:exclude+ options like this:
+ #
+ # class UsersController < ApplicationController
+ # wrap_parameters :person, include: [:username, :password]
+ # end
+ #
+ # On Active Record models with no +:include+ or +:exclude+ option set,
+ # it will only wrap the parameters returned by the class method
+ # <tt>attribute_names</tt>.
+ #
+ # If you're going to pass the parameters to an +ActiveModel+ object (such as
+ # <tt>User.new(params[:user])</tt>), you might consider passing the model class to
+ # the method instead. The +ParamsWrapper+ will actually try to determine the
+ # list of attribute names from the model and only wrap those attributes:
+ #
+ # class UsersController < ApplicationController
+ # wrap_parameters Person
+ # end
+ #
+ # You still could pass +:include+ and +:exclude+ to set the list of attributes
+ # you want to wrap.
+ #
+ # By default, if you don't specify the key in which the parameters would be
+ # wrapped to, +ParamsWrapper+ will actually try to determine if there's
+ # a model related to it or not. This controller, for example:
+ #
+ # class Admin::UsersController < ApplicationController
+ # end
+ #
+ # will try to check if <tt>Admin::User</tt> or +User+ model exists, and use it to
+ # determine the wrapper key respectively. If both models don't exist,
+ # it will then fallback to use +user+ as the key.
+ module ParamsWrapper
+ extend ActiveSupport::Concern
+
+ EXCLUDE_PARAMETERS = %w(authenticity_token _method utf8)
+
+ require "mutex_m"
+
+ class Options < Struct.new(:name, :format, :include, :exclude, :klass, :model) # :nodoc:
+ include Mutex_m
+
+ def self.from_hash(hash)
+ name = hash[:name]
+ format = Array(hash[:format])
+ include = hash[:include] && Array(hash[:include]).collect(&:to_s)
+ exclude = hash[:exclude] && Array(hash[:exclude]).collect(&:to_s)
+ new name, format, include, exclude, nil, nil
+ end
+
+ def initialize(name, format, include, exclude, klass, model) # :nodoc:
+ super
+ @include_set = include
+ @name_set = name
+ end
+
+ def model
+ super || synchronize { super || self.model = _default_wrap_model }
+ end
+
+ def include
+ return super if @include_set
+
+ m = model
+ synchronize do
+ return super if @include_set
+
+ @include_set = true
+
+ unless super || exclude
+ if m.respond_to?(:attribute_names) && m.attribute_names.any?
+ if m.respond_to?(:stored_attributes) && !m.stored_attributes.empty?
+ self.include = m.attribute_names + m.stored_attributes.values.flatten.map(&:to_s)
+ else
+ self.include = m.attribute_names
+ end
+ end
+ end
+ end
+ end
+
+ def name
+ return super if @name_set
+
+ m = model
+ synchronize do
+ return super if @name_set
+
+ @name_set = true
+
+ unless super || klass.anonymous?
+ self.name = m ? m.to_s.demodulize.underscore :
+ klass.controller_name.singularize
+ end
+ end
+ end
+
+ private
+ # Determine the wrapper model from the controller's name. By convention,
+ # this could be done by trying to find the defined model that has the
+ # same singular name as the controller. For example, +UsersController+
+ # will try to find if the +User+ model exists.
+ #
+ # This method also does namespace lookup. Foo::Bar::UsersController will
+ # try to find Foo::Bar::User, Foo::User and finally User.
+ def _default_wrap_model
+ return nil if klass.anonymous?
+ model_name = klass.name.sub(/Controller$/, "").classify
+
+ begin
+ if model_klass = model_name.safe_constantize
+ model_klass
+ else
+ namespaces = model_name.split("::")
+ namespaces.delete_at(-2)
+ break if namespaces.last == model_name
+ model_name = namespaces.join("::")
+ end
+ end until model_klass
+
+ model_klass
+ end
+ end
+
+ included do
+ class_attribute :_wrapper_options, default: Options.from_hash(format: [])
+ end
+
+ module ClassMethods
+ def _set_wrapper_options(options)
+ self._wrapper_options = Options.from_hash(options)
+ end
+
+ # Sets the name of the wrapper key, or the model which +ParamsWrapper+
+ # would use to determine the attribute names from.
+ #
+ # ==== Examples
+ # wrap_parameters format: :xml
+ # # enables the parameter wrapper for XML format
+ #
+ # wrap_parameters :person
+ # # wraps parameters into +params[:person]+ hash
+ #
+ # wrap_parameters Person
+ # # wraps parameters by determining the wrapper key from Person class
+ # (+person+, in this case) and the list of attribute names
+ #
+ # wrap_parameters include: [:username, :title]
+ # # wraps only +:username+ and +:title+ attributes from parameters.
+ #
+ # wrap_parameters false
+ # # disables parameters wrapping for this controller altogether.
+ #
+ # ==== Options
+ # * <tt>:format</tt> - The list of formats in which the parameters wrapper
+ # will be enabled.
+ # * <tt>:include</tt> - The list of attribute names which parameters wrapper
+ # will wrap into a nested hash.
+ # * <tt>:exclude</tt> - The list of attribute names which parameters wrapper
+ # will exclude from a nested hash.
+ def wrap_parameters(name_or_model_or_options, options = {})
+ model = nil
+
+ case name_or_model_or_options
+ when Hash
+ options = name_or_model_or_options
+ when false
+ options = options.merge(format: [])
+ when Symbol, String
+ options = options.merge(name: name_or_model_or_options)
+ else
+ model = name_or_model_or_options
+ end
+
+ opts = Options.from_hash _wrapper_options.to_h.slice(:format).merge(options)
+ opts.model = model
+ opts.klass = self
+
+ self._wrapper_options = opts
+ end
+
+ # Sets the default wrapper key or model which will be used to determine
+ # wrapper key and attribute names. Called automatically when the
+ # module is inherited.
+ def inherited(klass)
+ if klass._wrapper_options.format.any?
+ params = klass._wrapper_options.dup
+ params.klass = klass
+ klass._wrapper_options = params
+ end
+ super
+ end
+ end
+
+ # Performs parameters wrapping upon the request. Called automatically
+ # by the metal call stack.
+ def process_action(*args)
+ if _wrapper_enabled?
+ wrapped_hash = _wrap_parameters request.request_parameters
+ wrapped_keys = request.request_parameters.keys
+ wrapped_filtered_hash = _wrap_parameters request.filtered_parameters.slice(*wrapped_keys)
+
+ # This will make the wrapped hash accessible from controller and view.
+ request.parameters.merge! wrapped_hash
+ request.request_parameters.merge! wrapped_hash
+
+ # This will display the wrapped hash in the log file.
+ request.filtered_parameters.merge! wrapped_filtered_hash
+ end
+ super
+ end
+
+ private
+
+ # Returns the wrapper key which will be used to store wrapped parameters.
+ def _wrapper_key
+ _wrapper_options.name
+ end
+
+ # Returns the list of enabled formats.
+ def _wrapper_formats
+ _wrapper_options.format
+ end
+
+ # Returns the list of parameters which will be selected for wrapped.
+ def _wrap_parameters(parameters)
+ { _wrapper_key => _extract_parameters(parameters) }
+ end
+
+ def _extract_parameters(parameters)
+ if include_only = _wrapper_options.include
+ parameters.slice(*include_only)
+ else
+ exclude = _wrapper_options.exclude || []
+ parameters.except(*(exclude + EXCLUDE_PARAMETERS))
+ end
+ end
+
+ # Checks if we should perform parameters wrapping.
+ def _wrapper_enabled?
+ return false unless request.has_content_type?
+
+ ref = request.content_mime_type.ref
+ _wrapper_formats.include?(ref) && _wrapper_key && !request.parameters.key?(_wrapper_key)
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb
new file mode 100644
index 0000000000..2db2c78622
--- /dev/null
+++ b/actionpack/lib/action_controller/metal/redirecting.rb
@@ -0,0 +1,124 @@
+# frozen_string_literal: true
+
+module ActionController
+ module Redirecting
+ extend ActiveSupport::Concern
+
+ include AbstractController::Logger
+ include ActionController::UrlFor
+
+ # Redirects the browser to the target specified in +options+. This parameter can be any one of:
+ #
+ # * <tt>Hash</tt> - The URL will be generated by calling url_for with the +options+.
+ # * <tt>Record</tt> - The URL will be generated by calling url_for with the +options+, which will reference a named URL for that record.
+ # * <tt>String</tt> starting with <tt>protocol://</tt> (like <tt>http://</tt>) or a protocol relative reference (like <tt>//</tt>) - Is passed straight through as the target for redirection.
+ # * <tt>String</tt> not containing a protocol - The current protocol and host is prepended to the string.
+ # * <tt>Proc</tt> - A block that will be executed in the controller's context. Should return any option accepted by +redirect_to+.
+ #
+ # === Examples:
+ #
+ # redirect_to action: "show", id: 5
+ # redirect_to @post
+ # redirect_to "http://www.rubyonrails.org"
+ # redirect_to "/images/screenshot.jpg"
+ # redirect_to posts_url
+ # redirect_to proc { edit_post_url(@post) }
+ #
+ # The redirection happens as a <tt>302 Found</tt> header unless otherwise specified using the <tt>:status</tt> option:
+ #
+ # redirect_to post_url(@post), status: :found
+ # redirect_to action: 'atom', status: :moved_permanently
+ # redirect_to post_url(@post), status: 301
+ # redirect_to action: 'atom', status: 302
+ #
+ # The status code can either be a standard {HTTP Status code}[http://www.iana.org/assignments/http-status-codes] as an
+ # integer, or a symbol representing the downcased, underscored and symbolized description.
+ # Note that the status code must be a 3xx HTTP code, or redirection will not occur.
+ #
+ # If you are using XHR requests other than GET or POST and redirecting after the
+ # request then some browsers will follow the redirect using the original request
+ # method. This may lead to undesirable behavior such as a double DELETE. To work
+ # around this you can return a <tt>303 See Other</tt> status code which will be
+ # followed using a GET request.
+ #
+ # redirect_to posts_url, status: :see_other
+ # redirect_to action: 'index', status: 303
+ #
+ # It is also possible to assign a flash message as part of the redirection. There are two special accessors for the commonly used flash names
+ # +alert+ and +notice+ as well as a general purpose +flash+ bucket.
+ #
+ # redirect_to post_url(@post), alert: "Watch it, mister!"
+ # redirect_to post_url(@post), status: :found, notice: "Pay attention to the road"
+ # redirect_to post_url(@post), status: 301, flash: { updated_post_id: @post.id }
+ # redirect_to({ action: 'atom' }, alert: "Something serious happened")
+ #
+ # Statements after +redirect_to+ in our controller get executed, so +redirect_to+ doesn't stop the execution of the function.
+ # To terminate the execution of the function immediately after the +redirect_to+, use return.
+ # redirect_to post_url(@post) and return
+ def redirect_to(options = {}, response_status = {})
+ raise ActionControllerError.new("Cannot redirect to nil!") unless options
+ raise AbstractController::DoubleRenderError if response_body
+
+ self.status = _extract_redirect_to_status(options, response_status)
+ self.location = _compute_redirect_to_location(request, options)
+ self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(response.location)}\">redirected</a>.</body></html>"
+ end
+
+ # Redirects the browser to the page that issued the request (the referrer)
+ # if possible, otherwise redirects to the provided default fallback
+ # location.
+ #
+ # The referrer information is pulled from the HTTP `Referer` (sic) header on
+ # the request. This is an optional header and its presence on the request is
+ # subject to browser security settings and user preferences. If the request
+ # is missing this header, the <tt>fallback_location</tt> will be used.
+ #
+ # redirect_back fallback_location: { action: "show", id: 5 }
+ # redirect_back fallback_location: @post
+ # redirect_back fallback_location: "http://www.rubyonrails.org"
+ # redirect_back fallback_location: "/images/screenshot.jpg"
+ # redirect_back fallback_location: posts_url
+ # 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 identical.
+ def redirect_back(fallback_location:, **args)
+ if referer = request.headers["Referer"]
+ redirect_to referer, **args
+ else
+ redirect_to fallback_location, **args
+ end
+ end
+
+ def _compute_redirect_to_location(request, options) #:nodoc:
+ case options
+ # The scheme name consist of a letter followed by any combination of
+ # letters, digits, and the plus ("+"), period ("."), or hyphen ("-")
+ # characters; and is terminated by a colon (":").
+ # See http://tools.ietf.org/html/rfc3986#section-3.1
+ # The protocol relative scheme starts with a double slash "//".
+ when /\A([a-z][a-z\d\-+\.]*:|\/\/).*/i
+ options
+ when String
+ request.protocol + request.host_with_port + options
+ when Proc
+ _compute_redirect_to_location request, options.call
+ else
+ url_for(options)
+ end.delete("\0\r\n")
+ end
+ module_function :_compute_redirect_to_location
+ public :_compute_redirect_to_location
+
+ private
+ def _extract_redirect_to_status(options, response_status)
+ if options.is_a?(Hash) && options.key?(:status)
+ Rack::Utils.status_code(options.delete(:status))
+ elsif response_status.key?(:status)
+ Rack::Utils.status_code(response_status[:status])
+ else
+ 302
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/metal/renderers.rb b/actionpack/lib/action_controller/metal/renderers.rb
new file mode 100644
index 0000000000..26752571f8
--- /dev/null
+++ b/actionpack/lib/action_controller/metal/renderers.rb
@@ -0,0 +1,181 @@
+# frozen_string_literal: true
+
+require "set"
+
+module ActionController
+ # See <tt>Renderers.add</tt>
+ def self.add_renderer(key, &block)
+ Renderers.add(key, &block)
+ end
+
+ # See <tt>Renderers.remove</tt>
+ def self.remove_renderer(key)
+ Renderers.remove(key)
+ end
+
+ # See <tt>Responder#api_behavior</tt>
+ class MissingRenderer < LoadError
+ def initialize(format)
+ super "No renderer defined for format: #{format}"
+ end
+ end
+
+ module Renderers
+ extend ActiveSupport::Concern
+
+ # A Set containing renderer names that correspond to available renderer procs.
+ # Default values are <tt>:json</tt>, <tt>:js</tt>, <tt>:xml</tt>.
+ RENDERERS = Set.new
+
+ included do
+ class_attribute :_renderers, default: Set.new.freeze
+ end
+
+ # Used in <tt>ActionController::Base</tt>
+ # and <tt>ActionController::API</tt> to include all
+ # renderers by default.
+ module All
+ extend ActiveSupport::Concern
+ include Renderers
+
+ included do
+ self._renderers = RENDERERS
+ end
+ end
+
+ # Adds a new renderer to call within controller actions.
+ # A renderer is invoked by passing its name as an option to
+ # <tt>AbstractController::Rendering#render</tt>. To create a renderer
+ # pass it a name and a block. The block takes two arguments, the first
+ # is the value paired with its key and the second is the remaining
+ # hash of options passed to +render+.
+ #
+ # Create a csv renderer:
+ #
+ # ActionController::Renderers.add :csv do |obj, options|
+ # filename = options[:filename] || 'data'
+ # str = obj.respond_to?(:to_csv) ? obj.to_csv : obj.to_s
+ # send_data str, type: Mime[:csv],
+ # disposition: "attachment; filename=#{filename}.csv"
+ # end
+ #
+ # Note that we used Mime[:csv] for the csv mime type as it comes with Rails.
+ # For a custom renderer, you'll need to register a mime type with
+ # <tt>Mime::Type.register</tt>.
+ #
+ # To use the csv renderer in a controller action:
+ #
+ # def show
+ # @csvable = Csvable.find(params[:id])
+ # respond_to do |format|
+ # format.html
+ # format.csv { render csv: @csvable, filename: @csvable.name }
+ # end
+ # end
+ def self.add(key, &block)
+ define_method(_render_with_renderer_method_name(key), &block)
+ RENDERERS << key.to_sym
+ end
+
+ # This method is the opposite of add method.
+ #
+ # To remove a csv renderer:
+ #
+ # ActionController::Renderers.remove(:csv)
+ def self.remove(key)
+ RENDERERS.delete(key.to_sym)
+ method_name = _render_with_renderer_method_name(key)
+ remove_method(method_name) if method_defined?(method_name)
+ end
+
+ def self._render_with_renderer_method_name(key)
+ "_render_with_renderer_#{key}"
+ end
+
+ module ClassMethods
+ # Adds, by name, a renderer or renderers to the +_renderers+ available
+ # to call within controller actions.
+ #
+ # It is useful when rendering from an <tt>ActionController::Metal</tt> controller or
+ # otherwise to add an available renderer proc to a specific controller.
+ #
+ # Both <tt>ActionController::Base</tt> and <tt>ActionController::API</tt>
+ # include <tt>ActionController::Renderers::All</tt>, making all renderers
+ # available in the controller. See <tt>Renderers::RENDERERS</tt> and <tt>Renderers.add</tt>.
+ #
+ # Since <tt>ActionController::Metal</tt> controllers cannot render, the controller
+ # must include <tt>AbstractController::Rendering</tt>, <tt>ActionController::Rendering</tt>,
+ # and <tt>ActionController::Renderers</tt>, and have at least one renderer.
+ #
+ # Rather than including <tt>ActionController::Renderers::All</tt> and including all renderers,
+ # you may specify which renderers to include by passing the renderer name or names to
+ # +use_renderers+. For example, a controller that includes only the <tt>:json</tt> renderer
+ # (+_render_with_renderer_json+) might look like:
+ #
+ # class MetalRenderingController < ActionController::Metal
+ # include AbstractController::Rendering
+ # include ActionController::Rendering
+ # include ActionController::Renderers
+ #
+ # use_renderers :json
+ #
+ # def show
+ # render json: record
+ # end
+ # end
+ #
+ # You must specify a +use_renderer+, else the +controller.renderer+ and
+ # +controller._renderers+ will be <tt>nil</tt>, and the action will fail.
+ def use_renderers(*args)
+ renderers = _renderers + args
+ self._renderers = renderers.freeze
+ end
+ alias use_renderer use_renderers
+ end
+
+ # Called by +render+ in <tt>AbstractController::Rendering</tt>
+ # which sets the return value as the +response_body+.
+ #
+ # If no renderer is found, +super+ returns control to
+ # <tt>ActionView::Rendering.render_to_body</tt>, if present.
+ def render_to_body(options)
+ _render_to_body_with_renderer(options) || super
+ end
+
+ def _render_to_body_with_renderer(options)
+ _renderers.each do |name|
+ if options.key?(name)
+ _process_options(options)
+ method_name = Renderers._render_with_renderer_method_name(name)
+ return send(method_name, options.delete(name), options)
+ end
+ end
+ nil
+ end
+
+ add :json do |json, options|
+ json = json.to_json(options) unless json.kind_of?(String)
+
+ if options[:callback].present?
+ if content_type.nil? || content_type == Mime[:json]
+ self.content_type = Mime[:js]
+ end
+
+ "/**/#{options[:callback]}(#{json})"
+ else
+ self.content_type ||= Mime[:json]
+ json
+ end
+ end
+
+ add :js do |js, options|
+ self.content_type ||= Mime[:js]
+ js.respond_to?(:to_js) ? js.to_js(options) : js
+ end
+
+ add :xml do |xml, options|
+ self.content_type ||= Mime[:xml]
+ xml.respond_to?(:to_xml) ? xml.to_xml(options) : xml
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb
new file mode 100644
index 0000000000..d32eabf9ba
--- /dev/null
+++ b/actionpack/lib/action_controller/metal/rendering.rb
@@ -0,0 +1,124 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/string/filters"
+
+module ActionController
+ module Rendering
+ extend ActiveSupport::Concern
+
+ RENDER_FORMATS_IN_PRIORITY = [:body, :plain, :html]
+
+ module ClassMethods
+ # Documentation at ActionController::Renderer#render
+ delegate :render, to: :renderer
+
+ # Returns a renderer instance (inherited from ActionController::Renderer)
+ # for the controller.
+ attr_reader :renderer
+
+ def setup_renderer! # :nodoc:
+ @renderer = Renderer.for(self)
+ end
+
+ def inherited(klass)
+ klass.setup_renderer!
+ super
+ end
+ end
+
+ # Before processing, set the request formats in current controller formats.
+ def process_action(*) #:nodoc:
+ self.formats = request.formats.map(&:ref).compact
+ super
+ end
+
+ # Check for double render errors and set the content_type after rendering.
+ def render(*args) #:nodoc:
+ raise ::AbstractController::DoubleRenderError if response_body
+ super
+ end
+
+ # Overwrite render_to_string because body can now be set to a Rack body.
+ def render_to_string(*)
+ result = super
+ if result.respond_to?(:each)
+ string = ""
+ result.each { |r| string << r }
+ string
+ else
+ result
+ end
+ end
+
+ def render_to_body(options = {})
+ super || _render_in_priorities(options) || " "
+ end
+
+ private
+
+ def _process_variant(options)
+ if defined?(request) && !request.nil? && request.variant.present?
+ options[:variant] = request.variant
+ end
+ end
+
+ def _render_in_priorities(options)
+ RENDER_FORMATS_IN_PRIORITY.each do |format|
+ return options[format] if options.key?(format)
+ end
+
+ nil
+ end
+
+ def _set_html_content_type
+ self.content_type = Mime[:html].to_s
+ end
+
+ def _set_rendered_content_type(format)
+ if format && !response.content_type
+ self.content_type = format.to_s
+ end
+ end
+
+ # Normalize arguments by catching blocks and setting them on :update.
+ def _normalize_args(action = nil, options = {}, &blk)
+ options = super
+ options[:update] = blk if block_given?
+ options
+ end
+
+ # Normalize both text and status options.
+ def _normalize_options(options)
+ _normalize_text(options)
+
+ if options[:html]
+ options[:html] = ERB::Util.html_escape(options[:html])
+ end
+
+ if options[:status]
+ options[:status] = Rack::Utils.status_code(options[:status])
+ end
+
+ super
+ end
+
+ def _normalize_text(options)
+ RENDER_FORMATS_IN_PRIORITY.each do |format|
+ if options.key?(format) && options[format].respond_to?(:to_text)
+ options[format] = options[format].to_text
+ end
+ end
+ end
+
+ # Process controller specific options, as status, content-type and location.
+ def _process_options(options)
+ status, content_type, location = options.values_at(:status, :content_type, :location)
+
+ self.status = status if status
+ self.content_type = content_type if content_type
+ headers["Location"] = url_for(location) if location
+
+ super
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb
new file mode 100644
index 0000000000..d397c62461
--- /dev/null
+++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb
@@ -0,0 +1,433 @@
+# frozen_string_literal: true
+
+require "rack/session/abstract/id"
+require_relative "exceptions"
+require "active_support/security_utils"
+
+module ActionController #:nodoc:
+ class InvalidAuthenticityToken < ActionControllerError #:nodoc:
+ end
+
+ class InvalidCrossOriginRequest < ActionControllerError #:nodoc:
+ end
+
+ # Controller actions are protected from Cross-Site Request Forgery (CSRF) attacks
+ # by including a token in the rendered HTML for your application. This token is
+ # stored as a random string in the session, to which an attacker does not have
+ # access. When a request reaches your application, \Rails verifies the received
+ # token with the token in the session. All requests are checked except GET requests
+ # as these should be idempotent. Keep in mind that all session-oriented requests
+ # should be CSRF protected, including JavaScript and HTML requests.
+ #
+ # Since HTML and JavaScript requests are typically made from the browser, we
+ # need to ensure to verify request authenticity for the web browser. We can
+ # use session-oriented authentication for these types of requests, by using
+ # the `protect_from_forgery` method in our controllers.
+ #
+ # GET requests are not protected since they don't have side effects like writing
+ # to the database and don't leak sensitive information. JavaScript requests are
+ # an exception: a third-party site can use a <script> tag to reference a JavaScript
+ # URL on your site. When your JavaScript response loads on their site, it executes.
+ # With carefully crafted JavaScript on their end, sensitive data in your JavaScript
+ # response may be extracted. To prevent this, only XmlHttpRequest (known as XHR or
+ # Ajax) requests are allowed to make GET requests for JavaScript responses.
+ #
+ # It's important to remember that XML or JSON requests are also affected and if
+ # you're building an API you should change forgery protection method in
+ # <tt>ApplicationController</tt> (by default: <tt>:exception</tt>):
+ #
+ # class ApplicationController < ActionController::Base
+ # protect_from_forgery unless: -> { request.format.json? }
+ # end
+ #
+ # CSRF protection is turned on with the <tt>protect_from_forgery</tt> method.
+ # By default <tt>protect_from_forgery</tt> protects your session with
+ # <tt>:null_session</tt> method, which provides an empty session
+ # during request.
+ #
+ # We may want to disable CSRF protection for APIs since they are typically
+ # designed to be state-less. That is, the request API client will handle
+ # the session for you instead of Rails.
+ #
+ # The token parameter is named <tt>authenticity_token</tt> by default. The name and
+ # value of this token must be added to every layout that renders forms by including
+ # <tt>csrf_meta_tags</tt> in the HTML +head+.
+ #
+ # Learn more about CSRF attacks and securing your application in the
+ # {Ruby on Rails Security Guide}[http://guides.rubyonrails.org/security.html].
+ module RequestForgeryProtection
+ extend ActiveSupport::Concern
+
+ include AbstractController::Helpers
+ include AbstractController::Callbacks
+
+ included do
+ # Sets the token parameter name for RequestForgery. Calling +protect_from_forgery+
+ # sets it to <tt>:authenticity_token</tt> by default.
+ config_accessor :request_forgery_protection_token
+ self.request_forgery_protection_token ||= :authenticity_token
+
+ # Holds the class which implements the request forgery protection.
+ config_accessor :forgery_protection_strategy
+ self.forgery_protection_strategy = nil
+
+ # Controls whether request forgery protection is turned on or not. Turned off by default only in test mode.
+ config_accessor :allow_forgery_protection
+ self.allow_forgery_protection = true if allow_forgery_protection.nil?
+
+ # Controls whether a CSRF failure logs a warning. On by default.
+ config_accessor :log_warning_on_csrf_failure
+ self.log_warning_on_csrf_failure = true
+
+ # Controls whether the Origin header is checked in addition to the CSRF token.
+ config_accessor :forgery_protection_origin_check
+ self.forgery_protection_origin_check = false
+
+ # Controls whether form-action/method specific CSRF tokens are used.
+ config_accessor :per_form_csrf_tokens
+ self.per_form_csrf_tokens = false
+
+ # Controls whether forgery protection is enabled by default.
+ config_accessor :default_protect_from_forgery
+ self.default_protect_from_forgery = false
+
+ helper_method :form_authenticity_token
+ helper_method :protect_against_forgery?
+ end
+
+ module ClassMethods
+ # Turn on request forgery protection. Bear in mind that GET and HEAD requests are not checked.
+ #
+ # class ApplicationController < ActionController::Base
+ # protect_from_forgery
+ # end
+ #
+ # class FooController < ApplicationController
+ # protect_from_forgery except: :index
+ # end
+ #
+ # You can disable forgery protection on controller by skipping the verification before_action:
+ #
+ # skip_before_action :verify_authenticity_token
+ #
+ # Valid Options:
+ #
+ # * <tt>:only/:except</tt> - Only apply forgery protection to a subset of actions. For example <tt>only: [ :create, :create_all ]</tt>.
+ # * <tt>:if/:unless</tt> - Turn off the forgery protection entirely depending on the passed Proc or method reference.
+ # * <tt>:prepend</tt> - By default, the verification of the authentication token will be added at the position of the
+ # protect_from_forgery call in your application. This means any callbacks added before are run first. This is useful
+ # when you want your forgery protection to depend on other callbacks, like authentication methods (Oauth vs Cookie auth).
+ #
+ # If you need to add verification to the beginning of the callback chain, use <tt>prepend: true</tt>.
+ # * <tt>:with</tt> - Set the method to handle unverified request.
+ #
+ # Valid unverified request handling methods are:
+ # * <tt>:exception</tt> - Raises ActionController::InvalidAuthenticityToken exception.
+ # * <tt>:reset_session</tt> - Resets the session.
+ # * <tt>:null_session</tt> - Provides an empty session during request but doesn't reset it completely. Used as default if <tt>:with</tt> option is not specified.
+ def protect_from_forgery(options = {})
+ options = options.reverse_merge(prepend: false)
+
+ self.forgery_protection_strategy = protection_method_class(options[:with] || :null_session)
+ self.request_forgery_protection_token ||= :authenticity_token
+ before_action :verify_authenticity_token, options
+ append_after_action :verify_same_origin_request
+ end
+
+ # Turn off request forgery protection. This is a wrapper for:
+ #
+ # skip_before_action :verify_authenticity_token
+ #
+ # See +skip_before_action+ for allowed options.
+ def skip_forgery_protection(options = {})
+ skip_before_action :verify_authenticity_token, options
+ end
+
+ private
+
+ def protection_method_class(name)
+ ActionController::RequestForgeryProtection::ProtectionMethods.const_get(name.to_s.classify)
+ rescue NameError
+ raise ArgumentError, "Invalid request forgery protection method, use :null_session, :exception, or :reset_session"
+ end
+ end
+
+ module ProtectionMethods
+ class NullSession
+ def initialize(controller)
+ @controller = controller
+ end
+
+ # This is the method that defines the application behavior when a request is found to be unverified.
+ def handle_unverified_request
+ request = @controller.request
+ request.session = NullSessionHash.new(request)
+ request.flash = nil
+ request.session_options = { skip: true }
+ request.cookie_jar = NullCookieJar.build(request, {})
+ end
+
+ private
+
+ class NullSessionHash < Rack::Session::Abstract::SessionHash #:nodoc:
+ def initialize(req)
+ super(nil, req)
+ @data = {}
+ @loaded = true
+ end
+
+ # no-op
+ def destroy; end
+
+ def exists?
+ true
+ end
+ end
+
+ class NullCookieJar < ActionDispatch::Cookies::CookieJar #:nodoc:
+ def write(*)
+ # nothing
+ end
+ end
+ end
+
+ class ResetSession
+ def initialize(controller)
+ @controller = controller
+ end
+
+ def handle_unverified_request
+ @controller.reset_session
+ end
+ end
+
+ class Exception
+ def initialize(controller)
+ @controller = controller
+ end
+
+ def handle_unverified_request
+ raise ActionController::InvalidAuthenticityToken
+ end
+ end
+ end
+
+ private
+ # The actual before_action that is used to verify the CSRF token.
+ # Don't override this directly. Provide your own forgery protection
+ # strategy instead. If you override, you'll disable same-origin
+ # `<script>` verification.
+ #
+ # Lean on the protect_from_forgery declaration to mark which actions are
+ # due for same-origin request verification. If protect_from_forgery is
+ # enabled on an action, this before_action flags its after_action to
+ # verify that JavaScript responses are for XHR requests, ensuring they
+ # follow the browser's same-origin policy.
+ def verify_authenticity_token # :doc:
+ mark_for_same_origin_verification!
+
+ if !verified_request?
+ if logger && log_warning_on_csrf_failure
+ if valid_request_origin?
+ logger.warn "Can't verify CSRF token authenticity."
+ else
+ logger.warn "HTTP Origin header (#{request.origin}) didn't match request.base_url (#{request.base_url})"
+ end
+ end
+ handle_unverified_request
+ end
+ end
+
+ def handle_unverified_request # :doc:
+ forgery_protection_strategy.new(self).handle_unverified_request
+ end
+
+ #:nodoc:
+ CROSS_ORIGIN_JAVASCRIPT_WARNING = "Security warning: an embedded " \
+ "<script> tag on another site requested protected JavaScript. " \
+ "If you know what you're doing, go ahead and disable forgery " \
+ "protection on this action to permit cross-origin JavaScript embedding."
+ private_constant :CROSS_ORIGIN_JAVASCRIPT_WARNING
+
+ # If `verify_authenticity_token` was run (indicating that we have
+ # forgery protection enabled for this request) then also verify that
+ # we aren't serving an unauthorized cross-origin response.
+ def verify_same_origin_request # :doc:
+ if marked_for_same_origin_verification? && non_xhr_javascript_response?
+ if logger && log_warning_on_csrf_failure
+ logger.warn CROSS_ORIGIN_JAVASCRIPT_WARNING
+ end
+ raise ActionController::InvalidCrossOriginRequest, CROSS_ORIGIN_JAVASCRIPT_WARNING
+ end
+ end
+
+ # GET requests are checked for cross-origin JavaScript after rendering.
+ def mark_for_same_origin_verification! # :doc:
+ @marked_for_same_origin_verification = request.get?
+ end
+
+ # If the `verify_authenticity_token` before_action ran, verify that
+ # JavaScript responses are only served to same-origin GET requests.
+ def marked_for_same_origin_verification? # :doc:
+ @marked_for_same_origin_verification ||= false
+ end
+
+ # Check for cross-origin JavaScript responses.
+ def non_xhr_javascript_response? # :doc:
+ content_type =~ %r(\Atext/javascript) && !request.xhr?
+ end
+
+ AUTHENTICITY_TOKEN_LENGTH = 32
+
+ # Returns true or false if a request is verified. Checks:
+ #
+ # * Is it a GET or HEAD request? GETs should be safe and idempotent
+ # * Does the form_authenticity_token match the given token value from the params?
+ # * Does the X-CSRF-Token header match the form_authenticity_token?
+ def verified_request? # :doc:
+ !protect_against_forgery? || request.get? || request.head? ||
+ (valid_request_origin? && any_authenticity_token_valid?)
+ end
+
+ # Checks if any of the authenticity tokens from the request are valid.
+ def any_authenticity_token_valid? # :doc:
+ request_authenticity_tokens.any? do |token|
+ valid_authenticity_token?(session, token)
+ end
+ end
+
+ # Possible authenticity tokens sent in the request.
+ def request_authenticity_tokens # :doc:
+ [form_authenticity_param, request.x_csrf_token]
+ end
+
+ # Sets the token value for the current session.
+ def form_authenticity_token(form_options: {})
+ masked_authenticity_token(session, form_options: form_options)
+ end
+
+ # Creates a masked version of the authenticity token that varies
+ # on each request. The masking is used to mitigate SSL attacks
+ # like BREACH.
+ def masked_authenticity_token(session, form_options: {}) # :doc:
+ action, method = form_options.values_at(:action, :method)
+
+ raw_token = if per_form_csrf_tokens && action && method
+ action_path = normalize_action_path(action)
+ per_form_csrf_token(session, action_path, method)
+ else
+ real_csrf_token(session)
+ end
+
+ one_time_pad = SecureRandom.random_bytes(AUTHENTICITY_TOKEN_LENGTH)
+ encrypted_csrf_token = xor_byte_strings(one_time_pad, raw_token)
+ masked_token = one_time_pad + encrypted_csrf_token
+ Base64.strict_encode64(masked_token)
+ end
+
+ # Checks the client's masked token to see if it matches the
+ # session token. Essentially the inverse of
+ # +masked_authenticity_token+.
+ def valid_authenticity_token?(session, encoded_masked_token) # :doc:
+ if encoded_masked_token.nil? || encoded_masked_token.empty? || !encoded_masked_token.is_a?(String)
+ return false
+ end
+
+ begin
+ masked_token = Base64.strict_decode64(encoded_masked_token)
+ rescue ArgumentError # encoded_masked_token is invalid Base64
+ return false
+ end
+
+ # See if it's actually a masked token or not. In order to
+ # deploy this code, we should be able to handle any unmasked
+ # tokens that we've issued without error.
+
+ if masked_token.length == AUTHENTICITY_TOKEN_LENGTH
+ # This is actually an unmasked token. This is expected if
+ # you have just upgraded to masked tokens, but should stop
+ # happening shortly after installing this gem.
+ compare_with_real_token masked_token, session
+
+ elsif masked_token.length == AUTHENTICITY_TOKEN_LENGTH * 2
+ csrf_token = unmask_token(masked_token)
+
+ compare_with_real_token(csrf_token, session) ||
+ valid_per_form_csrf_token?(csrf_token, session)
+ else
+ false # Token is malformed.
+ end
+ end
+
+ def unmask_token(masked_token) # :doc:
+ # Split the token into the one-time pad and the encrypted
+ # value and decrypt it.
+ one_time_pad = masked_token[0...AUTHENTICITY_TOKEN_LENGTH]
+ encrypted_csrf_token = masked_token[AUTHENTICITY_TOKEN_LENGTH..-1]
+ xor_byte_strings(one_time_pad, encrypted_csrf_token)
+ end
+
+ def compare_with_real_token(token, session) # :doc:
+ ActiveSupport::SecurityUtils.secure_compare(token, real_csrf_token(session))
+ end
+
+ def valid_per_form_csrf_token?(token, session) # :doc:
+ if per_form_csrf_tokens
+ correct_token = per_form_csrf_token(
+ session,
+ normalize_action_path(request.fullpath),
+ request.request_method
+ )
+
+ ActiveSupport::SecurityUtils.secure_compare(token, correct_token)
+ else
+ false
+ end
+ end
+
+ def real_csrf_token(session) # :doc:
+ session[:_csrf_token] ||= SecureRandom.base64(AUTHENTICITY_TOKEN_LENGTH)
+ Base64.strict_decode64(session[:_csrf_token])
+ end
+
+ def per_form_csrf_token(session, action_path, method) # :doc:
+ OpenSSL::HMAC.digest(
+ OpenSSL::Digest::SHA256.new,
+ real_csrf_token(session),
+ [action_path, method.downcase].join("#")
+ )
+ end
+
+ def xor_byte_strings(s1, s2) # :doc:
+ s2_bytes = s2.bytes
+ s1.each_byte.with_index { |c1, i| s2_bytes[i] ^= c1 }
+ s2_bytes.pack("C*")
+ end
+
+ # The form's authenticity parameter. Override to provide your own.
+ def form_authenticity_param # :doc:
+ params[request_forgery_protection_token]
+ end
+
+ # Checks if the controller allows forgery protection.
+ def protect_against_forgery? # :doc:
+ allow_forgery_protection
+ end
+
+ # Checks if the request originated from the same origin by looking at the
+ # Origin header.
+ def valid_request_origin? # :doc:
+ if forgery_protection_origin_check
+ # We accept blank origin headers because some user agents don't send it.
+ request.origin.nil? || request.origin == request.base_url
+ else
+ true
+ end
+ end
+
+ def normalize_action_path(action_path) # :doc:
+ uri = URI.parse(action_path)
+ uri.path.chomp("/")
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/metal/rescue.rb b/actionpack/lib/action_controller/metal/rescue.rb
new file mode 100644
index 0000000000..843c99f57b
--- /dev/null
+++ b/actionpack/lib/action_controller/metal/rescue.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module ActionController #:nodoc:
+ # This module is responsible for providing `rescue_from` helpers
+ # to controllers and configuring when detailed exceptions must be
+ # shown.
+ module Rescue
+ extend ActiveSupport::Concern
+ include ActiveSupport::Rescuable
+
+ # Override this method if you want to customize when detailed
+ # exceptions must be shown. This method is only called when
+ # consider_all_requests_local is false. By default, it returns
+ # false, but someone may set it to `request.local?` so local
+ # requests in production still show the detailed exception pages.
+ def show_detailed_exceptions?
+ false
+ end
+
+ private
+ def process_action(*args)
+ super
+ rescue Exception => exception
+ request.env["action_dispatch.show_detailed_exceptions"] ||= show_detailed_exceptions?
+ rescue_with_handler(exception) || raise
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/metal/streaming.rb b/actionpack/lib/action_controller/metal/streaming.rb
new file mode 100644
index 0000000000..0b1598bf1b
--- /dev/null
+++ b/actionpack/lib/action_controller/metal/streaming.rb
@@ -0,0 +1,223 @@
+# frozen_string_literal: true
+
+require "rack/chunked"
+
+module ActionController #:nodoc:
+ # Allows views to be streamed back to the client as they are rendered.
+ #
+ # By default, Rails renders views by first rendering the template
+ # and then the layout. The response is sent to the client after the whole
+ # template is rendered, all queries are made, and the layout is processed.
+ #
+ # Streaming inverts the rendering flow by rendering the layout first and
+ # streaming each part of the layout as they are processed. This allows the
+ # header of the HTML (which is usually in the layout) to be streamed back
+ # to client very quickly, allowing JavaScripts and stylesheets to be loaded
+ # earlier than usual.
+ #
+ # This approach was introduced in Rails 3.1 and is still improving. Several
+ # Rack middlewares may not work and you need to be careful when streaming.
+ # Those points are going to be addressed soon.
+ #
+ # In order to use streaming, you will need to use a Ruby version that
+ # supports fibers (fibers are supported since version 1.9.2 of the main
+ # Ruby implementation).
+ #
+ # Streaming can be added to a given template easily, all you need to do is
+ # to pass the :stream option.
+ #
+ # class PostsController
+ # def index
+ # @posts = Post.all
+ # render stream: true
+ # end
+ # end
+ #
+ # == When to use streaming
+ #
+ # Streaming may be considered to be overkill for lightweight actions like
+ # +new+ or +edit+. The real benefit of streaming is on expensive actions
+ # that, for example, do a lot of queries on the database.
+ #
+ # In such actions, you want to delay queries execution as much as you can.
+ # For example, imagine the following +dashboard+ action:
+ #
+ # def dashboard
+ # @posts = Post.all
+ # @pages = Page.all
+ # @articles = Article.all
+ # end
+ #
+ # Most of the queries here are happening in the controller. In order to benefit
+ # from streaming you would want to rewrite it as:
+ #
+ # def dashboard
+ # # Allow lazy execution of the queries
+ # @posts = Post.all
+ # @pages = Page.all
+ # @articles = Article.all
+ # render stream: true
+ # end
+ #
+ # Notice that :stream only works with templates. Rendering :json
+ # or :xml with :stream won't work.
+ #
+ # == Communication between layout and template
+ #
+ # When streaming, rendering happens top-down instead of inside-out.
+ # Rails starts with the layout, and the template is rendered later,
+ # when its +yield+ is reached.
+ #
+ # This means that, if your application currently relies on instance
+ # variables set in the template to be used in the layout, they won't
+ # work once you move to streaming. The proper way to communicate
+ # between layout and template, regardless of whether you use streaming
+ # or not, is by using +content_for+, +provide+ and +yield+.
+ #
+ # Take a simple example where the layout expects the template to tell
+ # which title to use:
+ #
+ # <html>
+ # <head><title><%= yield :title %></title></head>
+ # <body><%= yield %></body>
+ # </html>
+ #
+ # You would use +content_for+ in your template to specify the title:
+ #
+ # <%= content_for :title, "Main" %>
+ # Hello
+ #
+ # And the final result would be:
+ #
+ # <html>
+ # <head><title>Main</title></head>
+ # <body>Hello</body>
+ # </html>
+ #
+ # However, if +content_for+ is called several times, the final result
+ # would have all calls concatenated. For instance, if we have the following
+ # template:
+ #
+ # <%= content_for :title, "Main" %>
+ # Hello
+ # <%= content_for :title, " page" %>
+ #
+ # The final result would be:
+ #
+ # <html>
+ # <head><title>Main page</title></head>
+ # <body>Hello</body>
+ # </html>
+ #
+ # This means that, if you have <code>yield :title</code> in your layout
+ # and you want to use streaming, you would have to render the whole template
+ # (and eventually trigger all queries) before streaming the title and all
+ # assets, which kills the purpose of streaming. For this purpose, you can use
+ # a helper called +provide+ that does the same as +content_for+ but tells the
+ # layout to stop searching for other entries and continue rendering.
+ #
+ # For instance, the template above using +provide+ would be:
+ #
+ # <%= provide :title, "Main" %>
+ # Hello
+ # <%= content_for :title, " page" %>
+ #
+ # Giving:
+ #
+ # <html>
+ # <head><title>Main</title></head>
+ # <body>Hello</body>
+ # </html>
+ #
+ # That said, when streaming, you need to properly check your templates
+ # and choose when to use +provide+ and +content_for+.
+ #
+ # == Headers, cookies, session and flash
+ #
+ # When streaming, the HTTP headers are sent to the client right before
+ # it renders the first line. This means that, modifying headers, cookies,
+ # session or flash after the template starts rendering will not propagate
+ # to the client.
+ #
+ # == Middlewares
+ #
+ # Middlewares that need to manipulate the body won't work with streaming.
+ # You should disable those middlewares whenever streaming in development
+ # or production. For instance, <tt>Rack::Bug</tt> won't work when streaming as it
+ # needs to inject contents in the HTML body.
+ #
+ # Also <tt>Rack::Cache</tt> won't work with streaming as it does not support
+ # streaming bodies yet. Whenever streaming Cache-Control is automatically
+ # set to "no-cache".
+ #
+ # == Errors
+ #
+ # When it comes to streaming, exceptions get a bit more complicated. This
+ # happens because part of the template was already rendered and streamed to
+ # the client, making it impossible to render a whole exception page.
+ #
+ # Currently, when an exception happens in development or production, Rails
+ # will automatically stream to the client:
+ #
+ # "><script>window.location = "/500.html"</script></html>
+ #
+ # The first two characters (">) are required in case the exception happens
+ # while rendering attributes for a given tag. You can check the real cause
+ # for the exception in your logger.
+ #
+ # == Web server support
+ #
+ # Not all web servers support streaming out-of-the-box. You need to check
+ # the instructions for each of them.
+ #
+ # ==== Unicorn
+ #
+ # Unicorn supports streaming but it needs to be configured. For this, you
+ # need to create a config file as follow:
+ #
+ # # unicorn.config.rb
+ # listen 3000, tcp_nopush: false
+ #
+ # And use it on initialization:
+ #
+ # unicorn_rails --config-file unicorn.config.rb
+ #
+ # You may also want to configure other parameters like <tt>:tcp_nodelay</tt>.
+ # Please check its documentation for more information: http://unicorn.bogomips.org/Unicorn/Configurator.html#method-i-listen
+ #
+ # If you are using Unicorn with NGINX, you may need to tweak NGINX.
+ # Streaming should work out of the box on Rainbows.
+ #
+ # ==== Passenger
+ #
+ # To be described.
+ #
+ module Streaming
+ extend ActiveSupport::Concern
+
+ private
+
+ # Set proper cache control and transfer encoding when streaming
+ def _process_options(options)
+ super
+ if options[:stream]
+ if request.version == "HTTP/1.0"
+ options.delete(:stream)
+ else
+ headers["Cache-Control"] ||= "no-cache"
+ headers["Transfer-Encoding"] = "chunked"
+ headers.delete("Content-Length")
+ end
+ end
+ end
+
+ # Call render_body if we are streaming instead of usual +render+.
+ def _render_template(options)
+ if options.delete(:stream)
+ Rack::Chunked::Body.new view_renderer.render_body(view_context, options)
+ else
+ super
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb
new file mode 100644
index 0000000000..ef7c4c4c16
--- /dev/null
+++ b/actionpack/lib/action_controller/metal/strong_parameters.rb
@@ -0,0 +1,1085 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/indifferent_access"
+require "active_support/core_ext/hash/transform_values"
+require "active_support/core_ext/array/wrap"
+require "active_support/core_ext/string/filters"
+require "active_support/core_ext/object/to_query"
+require "active_support/rescuable"
+require "action_dispatch/http/upload"
+require "rack/test"
+require "stringio"
+require "set"
+require "yaml"
+
+module ActionController
+ # Raised when a required parameter is missing.
+ #
+ # params = ActionController::Parameters.new(a: {})
+ # params.fetch(:b)
+ # # => ActionController::ParameterMissing: param is missing or the value is empty: b
+ # params.require(:a)
+ # # => ActionController::ParameterMissing: param is missing or the value is empty: a
+ class ParameterMissing < KeyError
+ attr_reader :param # :nodoc:
+
+ def initialize(param) # :nodoc:
+ @param = param
+ super("param is missing or the value is empty: #{param}")
+ end
+ end
+
+ # Raised when a supplied parameter is not expected and
+ # ActionController::Parameters.action_on_unpermitted_parameters
+ # is set to <tt>:raise</tt>.
+ #
+ # params = ActionController::Parameters.new(a: "123", b: "456")
+ # params.permit(:c)
+ # # => ActionController::UnpermittedParameters: found unpermitted parameters: :a, :b
+ class UnpermittedParameters < IndexError
+ attr_reader :params # :nodoc:
+
+ def initialize(params) # :nodoc:
+ @params = params
+ super("found unpermitted parameter#{'s' if params.size > 1 }: #{params.map { |e| ":#{e}" }.join(", ")}")
+ end
+ end
+
+ # Raised when a Parameters instance is not marked as permitted and
+ # an operation to transform it to hash is called.
+ #
+ # params = ActionController::Parameters.new(a: "123", b: "456")
+ # params.to_h
+ # # => ActionController::UnfilteredParameters: unable to convert unpermitted parameters to hash
+ class UnfilteredParameters < ArgumentError
+ def initialize # :nodoc:
+ super("unable to convert unpermitted parameters to hash")
+ end
+ end
+
+ # == Action Controller \Parameters
+ #
+ # Allows you to choose which attributes should be whitelisted for mass updating
+ # and thus prevent accidentally exposing that which shouldn't be exposed.
+ # Provides two methods for this purpose: #require and #permit. The former is
+ # used to mark parameters as required. The latter is used to set the parameter
+ # as permitted and limit which attributes should be allowed for mass updating.
+ #
+ # params = ActionController::Parameters.new({
+ # person: {
+ # name: "Francesco",
+ # age: 22,
+ # role: "admin"
+ # }
+ # })
+ #
+ # permitted = params.require(:person).permit(:name, :age)
+ # permitted # => <ActionController::Parameters {"name"=>"Francesco", "age"=>22} permitted: true>
+ # permitted.permitted? # => true
+ #
+ # Person.first.update!(permitted)
+ # # => #<Person id: 1, name: "Francesco", age: 22, role: "user">
+ #
+ # It provides two options that controls the top-level behavior of new instances:
+ #
+ # * +permit_all_parameters+ - If it's +true+, all the parameters will be
+ # permitted by default. The default is +false+.
+ # * +action_on_unpermitted_parameters+ - Allow to control the behavior when parameters
+ # that are not explicitly permitted are found. The values can be +false+ to just filter them
+ # out, <tt>:log</tt> to additionally write a message on the logger, or <tt>:raise</tt> to raise
+ # ActionController::UnpermittedParameters exception. The default value is <tt>:log</tt>
+ # in test and development environments, +false+ otherwise.
+ #
+ # Examples:
+ #
+ # params = ActionController::Parameters.new
+ # params.permitted? # => false
+ #
+ # ActionController::Parameters.permit_all_parameters = true
+ #
+ # params = ActionController::Parameters.new
+ # params.permitted? # => true
+ #
+ # params = ActionController::Parameters.new(a: "123", b: "456")
+ # params.permit(:c)
+ # # => <ActionController::Parameters {} permitted: true>
+ #
+ # ActionController::Parameters.action_on_unpermitted_parameters = :raise
+ #
+ # params = ActionController::Parameters.new(a: "123", b: "456")
+ # params.permit(:c)
+ # # => ActionController::UnpermittedParameters: found unpermitted keys: a, b
+ #
+ # Please note that these options *are not thread-safe*. In a multi-threaded
+ # environment they should only be set once at boot-time and never mutated at
+ # runtime.
+ #
+ # You can fetch values of <tt>ActionController::Parameters</tt> using either
+ # <tt>:key</tt> or <tt>"key"</tt>.
+ #
+ # params = ActionController::Parameters.new(key: "value")
+ # params[:key] # => "value"
+ # params["key"] # => "value"
+ class Parameters
+ cattr_accessor :permit_all_parameters, instance_accessor: false, default: false
+
+ cattr_accessor :action_on_unpermitted_parameters, instance_accessor: false
+
+ ##
+ # :method: as_json
+ #
+ # :call-seq:
+ # as_json(options=nil)
+ #
+ # Returns a hash that can be used as the JSON representation for the parameters.
+
+ ##
+ # :method: empty?
+ #
+ # :call-seq:
+ # empty?()
+ #
+ # Returns true if the parameters have no key/value pairs.
+
+ ##
+ # :method: has_key?
+ #
+ # :call-seq:
+ # has_key?(key)
+ #
+ # Returns true if the given key is present in the parameters.
+
+ ##
+ # :method: has_value?
+ #
+ # :call-seq:
+ # has_value?(value)
+ #
+ # Returns true if the given value is present for some key in the parameters.
+
+ ##
+ # :method: include?
+ #
+ # :call-seq:
+ # include?(key)
+ #
+ # Returns true if the given key is present in the parameters.
+
+ ##
+ # :method: key?
+ #
+ # :call-seq:
+ # key?(key)
+ #
+ # Returns true if the given key is present in the parameters.
+
+ ##
+ # :method: keys
+ #
+ # :call-seq:
+ # keys()
+ #
+ # Returns a new array of the keys of the parameters.
+
+ ##
+ # :method: to_s
+ #
+ # :call-seq:
+ # to_s()
+ #
+ # Returns the content of the parameters as a string.
+
+ ##
+ # :method: value?
+ #
+ # :call-seq:
+ # value?(value)
+ #
+ # Returns true if the given value is present for some key in the parameters.
+
+ ##
+ # :method: values
+ #
+ # :call-seq:
+ # values()
+ #
+ # Returns a new array of the values of the parameters.
+ delegate :keys, :key?, :has_key?, :values, :has_value?, :value?, :empty?, :include?,
+ :as_json, :to_s, to: :@parameters
+
+ # By default, never raise an UnpermittedParameters exception if these
+ # params are present. The default includes both 'controller' and 'action'
+ # because they are added by Rails and should be of no concern. One way
+ # to change these is to specify `always_permitted_parameters` in your
+ # config. For instance:
+ #
+ # config.always_permitted_parameters = %w( controller action format )
+ cattr_accessor :always_permitted_parameters, default: %w( controller action )
+
+ # Returns a new instance of <tt>ActionController::Parameters</tt>.
+ # Also, sets the +permitted+ attribute to the default value of
+ # <tt>ActionController::Parameters.permit_all_parameters</tt>.
+ #
+ # class Person < ActiveRecord::Base
+ # end
+ #
+ # params = ActionController::Parameters.new(name: "Francesco")
+ # params.permitted? # => false
+ # Person.new(params) # => ActiveModel::ForbiddenAttributesError
+ #
+ # ActionController::Parameters.permit_all_parameters = true
+ #
+ # params = ActionController::Parameters.new(name: "Francesco")
+ # params.permitted? # => true
+ # Person.new(params) # => #<Person id: nil, name: "Francesco">
+ def initialize(parameters = {})
+ @parameters = parameters.with_indifferent_access
+ @permitted = self.class.permit_all_parameters
+ end
+
+ # Returns true if another +Parameters+ object contains the same content and
+ # permitted flag.
+ def ==(other)
+ if other.respond_to?(:permitted?)
+ permitted? == other.permitted? && parameters == other.parameters
+ else
+ @parameters == other
+ end
+ end
+
+ # Returns a safe <tt>ActiveSupport::HashWithIndifferentAccess</tt>
+ # representation of the parameters with all unpermitted keys removed.
+ #
+ # params = ActionController::Parameters.new({
+ # name: "Senjougahara Hitagi",
+ # oddity: "Heavy stone crab"
+ # })
+ # params.to_h
+ # # => ActionController::UnfilteredParameters: unable to convert unpermitted parameters to hash
+ #
+ # safe_params = params.permit(:name)
+ # safe_params.to_h # => {"name"=>"Senjougahara Hitagi"}
+ def to_h
+ if permitted?
+ convert_parameters_to_hashes(@parameters, :to_h)
+ else
+ raise UnfilteredParameters
+ end
+ end
+
+ # Returns a safe <tt>Hash</tt> representation of the parameters
+ # with all unpermitted keys removed.
+ #
+ # params = ActionController::Parameters.new({
+ # name: "Senjougahara Hitagi",
+ # oddity: "Heavy stone crab"
+ # })
+ # params.to_hash
+ # # => ActionController::UnfilteredParameters: unable to convert unpermitted parameters to hash
+ #
+ # safe_params = params.permit(:name)
+ # safe_params.to_hash # => {"name"=>"Senjougahara Hitagi"}
+ def to_hash
+ to_h.to_hash
+ end
+
+ # Returns a string representation of the receiver suitable for use as a URL
+ # query string:
+ #
+ # params = ActionController::Parameters.new({
+ # name: "David",
+ # nationality: "Danish"
+ # })
+ # params.to_query
+ # # => ActionController::UnfilteredParameters: unable to convert unpermitted parameters to hash
+ #
+ # safe_params = params.permit(:name, :nationality)
+ # safe_params.to_query
+ # # => "name=David&nationality=Danish"
+ #
+ # An optional namespace can be passed to enclose key names:
+ #
+ # params = ActionController::Parameters.new({
+ # name: "David",
+ # nationality: "Danish"
+ # })
+ # safe_params = params.permit(:name, :nationality)
+ # safe_params.to_query("user")
+ # # => "user%5Bname%5D=David&user%5Bnationality%5D=Danish"
+ #
+ # The string pairs "key=value" that conform the query string
+ # are sorted lexicographically in ascending order.
+ #
+ # This method is also aliased as +to_param+.
+ def to_query(*args)
+ to_h.to_query(*args)
+ end
+ alias_method :to_param, :to_query
+
+ # Returns an unsafe, unfiltered
+ # <tt>ActiveSupport::HashWithIndifferentAccess</tt> representation of the
+ # parameters.
+ #
+ # 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
+ alias_method :to_unsafe_hash, :to_unsafe_h
+
+ # Convert all hashes in values into parameters, then yield each pair in
+ # the same way as <tt>Hash#each_pair</tt>.
+ def each_pair(&block)
+ @parameters.each_pair do |key, value|
+ yield key, convert_hashes_to_parameters(key, value)
+ end
+ end
+ alias_method :each, :each_pair
+
+ # Attribute that keeps track of converted arrays, if any, to avoid double
+ # looping in the common use case permit + mass-assignment. Defined in a
+ # method to instantiate it only if needed.
+ #
+ # Testing membership still loops, but it's going to be faster than our own
+ # loop that converts values. Also, we are not going to build a new array
+ # object per fetch.
+ def converted_arrays
+ @converted_arrays ||= Set.new
+ end
+
+ # Returns +true+ if the parameter is permitted, +false+ otherwise.
+ #
+ # params = ActionController::Parameters.new
+ # params.permitted? # => false
+ # params.permit!
+ # params.permitted? # => true
+ def permitted?
+ @permitted
+ end
+
+ # Sets the +permitted+ attribute to +true+. This can be used to pass
+ # mass assignment. Returns +self+.
+ #
+ # class Person < ActiveRecord::Base
+ # end
+ #
+ # params = ActionController::Parameters.new(name: "Francesco")
+ # params.permitted? # => false
+ # Person.new(params) # => ActiveModel::ForbiddenAttributesError
+ # params.permit!
+ # params.permitted? # => true
+ # Person.new(params) # => #<Person id: nil, name: "Francesco">
+ def permit!
+ each_pair do |key, value|
+ Array.wrap(value).each do |v|
+ v.permit! if v.respond_to? :permit!
+ end
+ end
+
+ @permitted = true
+ self
+ end
+
+ # This method accepts both a single key and an array of keys.
+ #
+ # When passed a single key, if it exists and its associated value is
+ # either present or the singleton +false+, returns said value:
+ #
+ # ActionController::Parameters.new(person: { name: "Francesco" }).require(:person)
+ # # => <ActionController::Parameters {"name"=>"Francesco"} permitted: false>
+ #
+ # Otherwise raises <tt>ActionController::ParameterMissing</tt>:
+ #
+ # ActionController::Parameters.new.require(:person)
+ # # ActionController::ParameterMissing: param is missing or the value is empty: person
+ #
+ # ActionController::Parameters.new(person: nil).require(:person)
+ # # ActionController::ParameterMissing: param is missing or the value is empty: person
+ #
+ # ActionController::Parameters.new(person: "\t").require(:person)
+ # # ActionController::ParameterMissing: param is missing or the value is empty: person
+ #
+ # ActionController::Parameters.new(person: {}).require(:person)
+ # # ActionController::ParameterMissing: param is missing or the value is empty: person
+ #
+ # When given an array of keys, the method tries to require each one of them
+ # in order. If it succeeds, an array with the respective return values is
+ # returned:
+ #
+ # params = ActionController::Parameters.new(user: { ... }, profile: { ... })
+ # user_params, profile_params = params.require([:user, :profile])
+ #
+ # Otherwise, the method re-raises the first exception found:
+ #
+ # params = ActionController::Parameters.new(user: {}, profile: {})
+ # user_params, profile_params = params.require([:user, :profile])
+ # # ActionController::ParameterMissing: param is missing or the value is empty: user
+ #
+ # Technically this method can be used to fetch terminal values:
+ #
+ # # CAREFUL
+ # params = ActionController::Parameters.new(person: { name: "Finn" })
+ # name = params.require(:person).require(:name) # CAREFUL
+ #
+ # but take into account that at some point those ones have to be permitted:
+ #
+ # def person_params
+ # params.require(:person).permit(:name).tap do |person_params|
+ # person_params.require(:name) # SAFER
+ # end
+ # end
+ #
+ # for example.
+ def require(key)
+ return key.map { |k| require(k) } if key.is_a?(Array)
+ value = self[key]
+ if value.present? || value == false
+ value
+ else
+ raise ParameterMissing.new(key)
+ end
+ end
+
+ # Alias of #require.
+ alias :required :require
+
+ # Returns a new <tt>ActionController::Parameters</tt> instance that
+ # includes only the given +filters+ and sets the +permitted+ attribute
+ # for the object to +true+. This is useful for limiting which attributes
+ # should be allowed for mass updating.
+ #
+ # params = ActionController::Parameters.new(user: { name: "Francesco", age: 22, role: "admin" })
+ # permitted = params.require(:user).permit(:name, :age)
+ # permitted.permitted? # => true
+ # permitted.has_key?(:name) # => true
+ # permitted.has_key?(:age) # => true
+ # permitted.has_key?(:role) # => false
+ #
+ # Only permitted scalars pass the filter. For example, given
+ #
+ # params.permit(:name)
+ #
+ # +:name+ passes if it is a key of +params+ whose associated value is of type
+ # +String+, +Symbol+, +NilClass+, +Numeric+, +TrueClass+, +FalseClass+,
+ # +Date+, +Time+, +DateTime+, +StringIO+, +IO+,
+ # +ActionDispatch::Http::UploadedFile+ or +Rack::Test::UploadedFile+.
+ # Otherwise, the key +:name+ is filtered out.
+ #
+ # You may declare that the parameter should be an array of permitted scalars
+ # by mapping it to an empty array:
+ #
+ # params = ActionController::Parameters.new(tags: ["rails", "parameters"])
+ # params.permit(tags: [])
+ #
+ # Sometimes it is not possible or convenient to declare the valid keys of
+ # a hash parameter or its internal structure. Just map to an empty hash:
+ #
+ # params.permit(preferences: {})
+ #
+ # Be careful because this opens the door to arbitrary input. In this
+ # case, +permit+ ensures values in the returned structure are permitted
+ # scalars and filters out anything else.
+ #
+ # You can also use +permit+ on nested parameters, like:
+ #
+ # params = ActionController::Parameters.new({
+ # person: {
+ # name: "Francesco",
+ # age: 22,
+ # pets: [{
+ # name: "Purplish",
+ # category: "dogs"
+ # }]
+ # }
+ # })
+ #
+ # permitted = params.permit(person: [ :name, { pets: :name } ])
+ # permitted.permitted? # => true
+ # permitted[:person][:name] # => "Francesco"
+ # permitted[:person][:age] # => nil
+ # permitted[:person][:pets][0][:name] # => "Purplish"
+ # permitted[:person][:pets][0][:category] # => nil
+ #
+ # Note that if you use +permit+ in a key that points to a hash,
+ # it won't allow all the hash. You also need to specify which
+ # attributes inside the hash should be whitelisted.
+ #
+ # params = ActionController::Parameters.new({
+ # person: {
+ # contact: {
+ # email: "none@test.com",
+ # phone: "555-1234"
+ # }
+ # }
+ # })
+ #
+ # params.require(:person).permit(:contact)
+ # # => <ActionController::Parameters {} permitted: true>
+ #
+ # params.require(:person).permit(contact: :phone)
+ # # => <ActionController::Parameters {"contact"=><ActionController::Parameters {"phone"=>"555-1234"} permitted: true>} permitted: true>
+ #
+ # params.require(:person).permit(contact: [ :email, :phone ])
+ # # => <ActionController::Parameters {"contact"=><ActionController::Parameters {"email"=>"none@test.com", "phone"=>"555-1234"} permitted: true>} permitted: true>
+ def permit(*filters)
+ params = self.class.new
+
+ filters.flatten.each do |filter|
+ case filter
+ when Symbol, String
+ permitted_scalar_filter(params, filter)
+ when Hash
+ hash_filter(params, filter)
+ end
+ end
+
+ unpermitted_parameters!(params) if self.class.action_on_unpermitted_parameters
+
+ params.permit!
+ end
+
+ # Returns a parameter for the given +key+. If not found,
+ # returns +nil+.
+ #
+ # params = ActionController::Parameters.new(person: { name: "Francesco" })
+ # params[:person] # => <ActionController::Parameters {"name"=>"Francesco"} permitted: false>
+ # params[:none] # => nil
+ def [](key)
+ convert_hashes_to_parameters(key, @parameters[key])
+ end
+
+ # Assigns a value to a given +key+. The given key may still get filtered out
+ # when +permit+ is called.
+ def []=(key, value)
+ @parameters[key] = value
+ end
+
+ # Returns a parameter for the given +key+. If the +key+
+ # can't be found, there are several options: With no other arguments,
+ # it will raise an <tt>ActionController::ParameterMissing</tt> error;
+ # if more arguments are given, then that will be returned; if a block
+ # is given, then that will be run and its result returned.
+ #
+ # params = ActionController::Parameters.new(person: { name: "Francesco" })
+ # params.fetch(:person) # => <ActionController::Parameters {"name"=>"Francesco"} permitted: false>
+ # params.fetch(:none) # => ActionController::ParameterMissing: param is missing or the value is empty: none
+ # params.fetch(:none, "Francesco") # => "Francesco"
+ # params.fetch(:none) { "Francesco" } # => "Francesco"
+ def fetch(key, *args)
+ convert_value_to_parameters(
+ @parameters.fetch(key) {
+ if block_given?
+ yield
+ else
+ args.fetch(0) { raise ActionController::ParameterMissing.new(key) }
+ end
+ }
+ )
+ 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.
+ #
+ # params = ActionController::Parameters.new(a: 1, b: 2, c: 3)
+ # params.slice(:a, :b) # => <ActionController::Parameters {"a"=>1, "b"=>2} permitted: false>
+ # params.slice(:d) # => <ActionController::Parameters {} permitted: false>
+ def slice(*keys)
+ new_instance_with_inherited_permitted_status(@parameters.slice(*keys))
+ end
+
+ # Returns current <tt>ActionController::Parameters</tt> instance which
+ # contains only the given +keys+.
+ def slice!(*keys)
+ @parameters.slice!(*keys)
+ self
+ end
+
+ # Returns a new <tt>ActionController::Parameters</tt> instance that
+ # filters out the given +keys+.
+ #
+ # params = ActionController::Parameters.new(a: 1, b: 2, c: 3)
+ # params.except(:a, :b) # => <ActionController::Parameters {"c"=>3} permitted: false>
+ # params.except(:d) # => <ActionController::Parameters {"a"=>1, "b"=>2, "c"=>3} permitted: false>
+ def except(*keys)
+ new_instance_with_inherited_permitted_status(@parameters.except(*keys))
+ end
+
+ # Removes and returns the key/value pairs matching the given keys.
+ #
+ # params = ActionController::Parameters.new(a: 1, b: 2, c: 3)
+ # params.extract!(:a, :b) # => <ActionController::Parameters {"a"=>1, "b"=>2} permitted: false>
+ # params # => <ActionController::Parameters {"c"=>3} permitted: false>
+ def extract!(*keys)
+ new_instance_with_inherited_permitted_status(@parameters.extract!(*keys))
+ end
+
+ # Returns a new <tt>ActionController::Parameters</tt> with the results of
+ # running +block+ once for every value. The keys are unchanged.
+ #
+ # params = ActionController::Parameters.new(a: 1, b: 2, c: 3)
+ # params.transform_values { |x| x * 2 }
+ # # => <ActionController::Parameters {"a"=>2, "b"=>4, "c"=>6} permitted: false>
+ def transform_values(&block)
+ if block
+ new_instance_with_inherited_permitted_status(
+ @parameters.transform_values(&block)
+ )
+ else
+ @parameters.transform_values
+ end
+ end
+
+ # Performs values transformation and returns the altered
+ # <tt>ActionController::Parameters</tt> instance.
+ def transform_values!(&block)
+ @parameters.transform_values!(&block)
+ self
+ end
+
+ # Returns a new <tt>ActionController::Parameters</tt> instance with the
+ # results of running +block+ once for every key. The values are unchanged.
+ def transform_keys(&block)
+ if block
+ new_instance_with_inherited_permitted_status(
+ @parameters.transform_keys(&block)
+ )
+ else
+ @parameters.transform_keys
+ end
+ end
+
+ # Performs keys transformation and returns the altered
+ # <tt>ActionController::Parameters</tt> instance.
+ def transform_keys!(&block)
+ @parameters.transform_keys!(&block)
+ self
+ end
+
+ # Deletes a key-value pair from +Parameters+ and returns the value. If
+ # +key+ is not found, returns +nil+ (or, with optional code block, yields
+ # +key+ and returns the result). Cf. +#extract!+, which returns the
+ # corresponding +ActionController::Parameters+ object.
+ def delete(key, &block)
+ convert_value_to_parameters(@parameters.delete(key, &block))
+ end
+
+ # Returns a new instance of <tt>ActionController::Parameters</tt> with only
+ # items that the block evaluates to true.
+ def select(&block)
+ new_instance_with_inherited_permitted_status(@parameters.select(&block))
+ end
+
+ # Equivalent to Hash#keep_if, but returns +nil+ if no changes were made.
+ def select!(&block)
+ @parameters.select!(&block)
+ self
+ end
+ alias_method :keep_if, :select!
+
+ # Returns a new instance of <tt>ActionController::Parameters</tt> with items
+ # that the block evaluates to true removed.
+ def reject(&block)
+ new_instance_with_inherited_permitted_status(@parameters.reject(&block))
+ end
+
+ # Removes items that the block evaluates to true and returns self.
+ def reject!(&block)
+ @parameters.reject!(&block)
+ self
+ end
+ alias_method :delete_if, :reject!
+
+ # Returns values that were assigned to the given +keys+. Note that all the
+ # +Hash+ objects will be converted to <tt>ActionController::Parameters</tt>.
+ def values_at(*keys)
+ convert_value_to_parameters(@parameters.values_at(*keys))
+ end
+
+ # Returns a new <tt>ActionController::Parameters</tt> with all keys from
+ # +other_hash+ merged into current hash.
+ def merge(other_hash)
+ new_instance_with_inherited_permitted_status(
+ @parameters.merge(other_hash.to_h)
+ )
+ end
+
+ # Returns current <tt>ActionController::Parameters</tt> instance with
+ # +other_hash+ merged into current hash.
+ def merge!(other_hash)
+ @parameters.merge!(other_hash.to_h)
+ self
+ end
+
+ # Returns a new <tt>ActionController::Parameters</tt> with all keys from
+ # current hash merged into +other_hash+.
+ def reverse_merge(other_hash)
+ new_instance_with_inherited_permitted_status(
+ other_hash.to_h.merge(@parameters)
+ )
+ end
+ alias_method :with_defaults, :reverse_merge
+
+ # Returns current <tt>ActionController::Parameters</tt> instance with
+ # current hash merged into +other_hash+.
+ def reverse_merge!(other_hash)
+ @parameters.merge!(other_hash.to_h) { |key, left, right| left }
+ self
+ end
+ alias_method :with_defaults!, :reverse_merge!
+
+ # This is required by ActiveModel attribute assignment, so that user can
+ # pass +Parameters+ to a mass assignment methods in a model. It should not
+ # matter as we are using +HashWithIndifferentAccess+ internally.
+ def stringify_keys # :nodoc:
+ dup
+ end
+
+ def inspect
+ "<#{self.class} #{@parameters} permitted: #{@permitted}>"
+ end
+
+ def self.hook_into_yaml_loading # :nodoc:
+ # Wire up YAML format compatibility with Rails 4.2 and Psych 2.0.8 and 2.0.9+.
+ # Makes the YAML parser call `init_with` when it encounters the keys below
+ # instead of trying its own parsing routines.
+ YAML.load_tags["!ruby/hash-with-ivars:ActionController::Parameters"] = name
+ YAML.load_tags["!ruby/hash:ActionController::Parameters"] = name
+ end
+ hook_into_yaml_loading
+
+ def init_with(coder) # :nodoc:
+ case coder.tag
+ when "!ruby/hash:ActionController::Parameters"
+ # YAML 2.0.8's format where hash instance variables weren't stored.
+ @parameters = coder.map.with_indifferent_access
+ @permitted = false
+ when "!ruby/hash-with-ivars:ActionController::Parameters"
+ # YAML 2.0.9's Hash subclass format where keys and values
+ # were stored under an elements hash and `permitted` within an ivars hash.
+ @parameters = coder.map["elements"].with_indifferent_access
+ @permitted = coder.map["ivars"][:@permitted]
+ when "!ruby/object:ActionController::Parameters"
+ # YAML's Object format. Only needed because of the format
+ # backwardscompability above, otherwise equivalent to YAML's initialization.
+ @parameters, @permitted = coder.map["parameters"], coder.map["permitted"]
+ end
+ end
+
+ # Returns duplicate of object including all parameters.
+ def deep_dup
+ self.class.new(@parameters.deep_dup).tap do |duplicate|
+ duplicate.permitted = @permitted
+ end
+ end
+
+ protected
+ attr_reader :parameters
+
+ def permitted=(new_permitted)
+ @permitted = new_permitted
+ end
+
+ def fields_for_style?
+ @parameters.all? { |k, v| k =~ /\A-?\d+\z/ && (v.is_a?(Hash) || v.is_a?(Parameters)) }
+ end
+
+ private
+ def new_instance_with_inherited_permitted_status(hash)
+ self.class.new(hash).tap do |new_instance|
+ new_instance.permitted = @permitted
+ end
+ end
+
+ def convert_parameters_to_hashes(value, using)
+ case value
+ when Array
+ value.map { |v| convert_parameters_to_hashes(v, using) }
+ when Hash
+ value.transform_values do |v|
+ convert_parameters_to_hashes(v, using)
+ end.with_indifferent_access
+ when Parameters
+ value.send(using)
+ else
+ value
+ end
+ end
+
+ def convert_hashes_to_parameters(key, value)
+ converted = convert_value_to_parameters(value)
+ @parameters[key] = converted unless converted.equal?(value)
+ converted
+ end
+
+ def convert_value_to_parameters(value)
+ case value
+ when Array
+ return value if converted_arrays.member?(value)
+ converted = value.map { |_| convert_value_to_parameters(_) }
+ converted_arrays << converted
+ converted
+ when Hash
+ self.class.new(value)
+ else
+ value
+ end
+ end
+
+ def each_element(object)
+ case object
+ when Array
+ object.grep(Parameters).map { |el| yield el }.compact
+ when Parameters
+ if object.fields_for_style?
+ hash = object.class.new
+ object.each { |k, v| hash[k] = yield v }
+ hash
+ else
+ yield object
+ end
+ end
+ end
+
+ def unpermitted_parameters!(params)
+ unpermitted_keys = unpermitted_keys(params)
+ if unpermitted_keys.any?
+ case self.class.action_on_unpermitted_parameters
+ when :log
+ name = "unpermitted_parameters.action_controller"
+ ActiveSupport::Notifications.instrument(name, keys: unpermitted_keys)
+ when :raise
+ raise ActionController::UnpermittedParameters.new(unpermitted_keys)
+ end
+ end
+ end
+
+ def unpermitted_keys(params)
+ keys - params.keys - always_permitted_parameters
+ end
+
+ #
+ # --- Filtering ----------------------------------------------------------
+ #
+
+ # This is a white list of permitted scalar types that includes the ones
+ # supported in XML and JSON requests.
+ #
+ # This list is in particular used to filter ordinary requests, String goes
+ # as first element to quickly short-circuit the common case.
+ #
+ # If you modify this collection please update the API of +permit+ above.
+ PERMITTED_SCALAR_TYPES = [
+ String,
+ Symbol,
+ NilClass,
+ Numeric,
+ TrueClass,
+ FalseClass,
+ Date,
+ Time,
+ # DateTimes are Dates, we document the type but avoid the redundant check.
+ StringIO,
+ IO,
+ ActionDispatch::Http::UploadedFile,
+ Rack::Test::UploadedFile,
+ ]
+
+ def permitted_scalar?(value)
+ PERMITTED_SCALAR_TYPES.any? { |type| value.is_a?(type) }
+ end
+
+ def permitted_scalar_filter(params, key)
+ if has_key?(key) && permitted_scalar?(self[key])
+ params[key] = self[key]
+ end
+
+ keys.grep(/\A#{Regexp.escape(key)}\(\d+[if]?\)\z/) do |k|
+ if permitted_scalar?(self[k])
+ params[k] = self[k]
+ end
+ end
+ end
+
+ def array_of_permitted_scalars?(value)
+ if value.is_a?(Array) && value.all? { |element| permitted_scalar?(element) }
+ yield value
+ end
+ end
+
+ def non_scalar?(value)
+ value.is_a?(Array) || value.is_a?(Parameters)
+ end
+
+ EMPTY_ARRAY = []
+ EMPTY_HASH = {}
+ def hash_filter(params, filter)
+ filter = filter.with_indifferent_access
+
+ # Slicing filters out non-declared keys.
+ slice(*filter.keys).each do |key, value|
+ next unless value
+ next unless has_key? key
+
+ if filter[key] == EMPTY_ARRAY
+ # Declaration { comment_ids: [] }.
+ array_of_permitted_scalars?(self[key]) do |val|
+ params[key] = val
+ end
+ elsif filter[key] == EMPTY_HASH
+ # Declaration { preferences: {} }.
+ if value.is_a?(Parameters)
+ params[key] = permit_any_in_parameters(value)
+ end
+ 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]))
+ end
+ end
+ end
+ end
+
+ def permit_any_in_parameters(params)
+ self.class.new.tap do |sanitized|
+ params.each do |key, value|
+ case value
+ when ->(v) { permitted_scalar?(v) }
+ sanitized[key] = value
+ when Array
+ sanitized[key] = permit_any_in_array(value)
+ when Parameters
+ sanitized[key] = permit_any_in_parameters(value)
+ else
+ # Filter this one out.
+ end
+ end
+ end
+ end
+
+ def permit_any_in_array(array)
+ [].tap do |sanitized|
+ array.each do |element|
+ case element
+ when ->(e) { permitted_scalar?(e) }
+ sanitized << element
+ when Parameters
+ sanitized << permit_any_in_parameters(element)
+ else
+ # Filter this one out.
+ end
+ end
+ end
+ end
+
+ def initialize_copy(source)
+ super
+ @parameters = @parameters.dup
+ end
+ end
+
+ # == Strong \Parameters
+ #
+ # It provides an interface for protecting attributes from end-user
+ # assignment. This makes Action Controller parameters forbidden
+ # to be used in Active Model mass assignment until they have been
+ # whitelisted.
+ #
+ # In addition, parameters can be marked as required and flow through a
+ # predefined raise/rescue flow to end up as a <tt>400 Bad Request</tt> with no
+ # effort.
+ #
+ # class PeopleController < ActionController::Base
+ # # Using "Person.create(params[:person])" would raise an
+ # # ActiveModel::ForbiddenAttributesError exception because it'd
+ # # be using mass assignment without an explicit permit step.
+ # # This is the recommended form:
+ # def create
+ # Person.create(person_params)
+ # end
+ #
+ # # This will pass with flying colors as long as there's a person key in the
+ # # parameters, otherwise it'll raise an ActionController::ParameterMissing
+ # # exception, which will get caught by ActionController::Base and turned
+ # # into a 400 Bad Request reply.
+ # def update
+ # redirect_to current_account.people.find(params[:id]).tap { |person|
+ # person.update!(person_params)
+ # }
+ # end
+ #
+ # private
+ # # Using a private method to encapsulate the permissible parameters is
+ # # a good pattern since you'll be able to reuse the same permit
+ # # list between create and update. Also, you can specialize this method
+ # # with per-user checking of permissible attributes.
+ # def person_params
+ # params.require(:person).permit(:name, :age)
+ # end
+ # 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. You might want
+ # to allow +:id+ and +:_destroy+, see ActiveRecord::NestedAttributes for more information.
+ #
+ # class Person
+ # has_many :pets
+ # accepts_nested_attributes_for :pets
+ # end
+ #
+ # class PeopleController < ActionController::Base
+ # def create
+ # Person.create(person_params)
+ # end
+ #
+ # ...
+ #
+ # private
+ #
+ # def person_params
+ # # 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: [ :id, :name, :category ])
+ # end
+ # end
+ #
+ # See ActionController::Parameters.require and ActionController::Parameters.permit
+ # for more information.
+ module StrongParameters
+ extend ActiveSupport::Concern
+ include ActiveSupport::Rescuable
+
+ # Returns a new ActionController::Parameters object that
+ # has been instantiated with the <tt>request.parameters</tt>.
+ def params
+ @_params ||= Parameters.new(request.parameters)
+ end
+
+ # Assigns the given +value+ to the +params+ hash. If +value+
+ # is a Hash, this will create an ActionController::Parameters
+ # object that has been instantiated with the given +value+ hash.
+ def params=(value)
+ @_params = value.is_a?(Hash) ? Parameters.new(value) : value
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/metal/testing.rb b/actionpack/lib/action_controller/metal/testing.rb
new file mode 100644
index 0000000000..b07f1f3d8c
--- /dev/null
+++ b/actionpack/lib/action_controller/metal/testing.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module ActionController
+ module Testing
+ extend ActiveSupport::Concern
+
+ # Behavior specific to functional tests
+ module Functional # :nodoc:
+ def recycle!
+ @_url_options = nil
+ self.formats = nil
+ self.params = nil
+ end
+ end
+
+ module ClassMethods
+ def before_filters
+ _process_action_callbacks.find_all { |x| x.kind == :before }.map(&:name)
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/metal/url_for.rb b/actionpack/lib/action_controller/metal/url_for.rb
new file mode 100644
index 0000000000..84dbb59a63
--- /dev/null
+++ b/actionpack/lib/action_controller/metal/url_for.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+module ActionController
+ # Includes +url_for+ into the host class. The class has to provide a +RouteSet+ by implementing
+ # the <tt>_routes</tt> method. Otherwise, an exception will be raised.
+ #
+ # In addition to <tt>AbstractController::UrlFor</tt>, this module accesses the HTTP layer to define
+ # URL options like the +host+. In order to do so, this module requires the host class
+ # to implement +env+ which needs to be Rack-compatible and +request+
+ # which is either an instance of +ActionDispatch::Request+ or an object
+ # that responds to the +host+, +optional_port+, +protocol+ and
+ # +symbolized_path_parameter+ methods.
+ #
+ # class RootUrl
+ # include ActionController::UrlFor
+ # include Rails.application.routes.url_helpers
+ #
+ # delegate :env, :request, to: :controller
+ #
+ # def initialize(controller)
+ # @controller = controller
+ # @url = root_path # named route from the application.
+ # end
+ # end
+ module UrlFor
+ extend ActiveSupport::Concern
+
+ include AbstractController::UrlFor
+
+ def url_options
+ @_url_options ||= {
+ host: request.host,
+ port: request.optional_port,
+ protocol: request.protocol,
+ _recall: request.path_parameters
+ }.merge!(super).freeze
+
+ if (same_origin = _routes.equal?(request.routes)) ||
+ (script_name = request.engine_script_name(_routes)) ||
+ (original_script_name = request.original_script_name)
+
+ options = @_url_options.dup
+ if original_script_name
+ options[:original_script_name] = original_script_name
+ else
+ if same_origin
+ options[:script_name] = request.script_name.empty? ? "".freeze : request.script_name.dup
+ else
+ options[:script_name] = script_name
+ end
+ end
+ options.freeze
+ else
+ @_url_options
+ end
+ end
+ end
+end