Transform actionpack documentation to Markdown

This commit is contained in:
Rafael Mendonça França 2024-01-25 19:51:51 +00:00
parent 3079e8b0f8
commit d6bf4de7dc
No known key found for this signature in database
GPG Key ID: FC23B6D0F1EEE948
151 changed files with 5573 additions and 5084 deletions

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
require "action_pack" require "action_pack"
require "active_support" require "active_support"
require "active_support/rails" require "active_support/rails"

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
module AbstractController module AbstractController
module AssetPaths # :nodoc: module AssetPaths # :nodoc:
extend ActiveSupport::Concern extend ActiveSupport::Concern

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
require "abstract_controller/error" require "abstract_controller/error"
require "active_support/configurable" require "active_support/configurable"
require "active_support/descendants_tracker" require "active_support/descendants_tracker"
@ -26,12 +28,12 @@ def corrections # :nodoc:
end end
end end
# = Abstract Controller \Base # # Abstract Controller Base
# #
# AbstractController::Base is a low-level API. Nobody should be # AbstractController::Base is a low-level API. Nobody should be using it
# using it directly, and subclasses (like ActionController::Base) are # directly, and subclasses (like ActionController::Base) are expected to provide
# expected to provide their own +render+ method, since rendering means # their own `render` method, since rendering means different things depending on
# different things depending on the context. # the context.
class Base class Base
## ##
# Returns the body of the HTTP response sent by the controller. # Returns the body of the HTTP response sent by the controller.
@ -52,27 +54,26 @@ class << self
attr_reader :abstract attr_reader :abstract
alias_method :abstract?, :abstract alias_method :abstract?, :abstract
# Define a controller as abstract. See internal_methods for more # Define a controller as abstract. See internal_methods for more details.
# details.
def abstract! def abstract!
@abstract = true @abstract = true
end end
def inherited(klass) # :nodoc: def inherited(klass) # :nodoc:
# Define the abstract ivar on subclasses so that we don't get # Define the abstract ivar on subclasses so that we don't get uninitialized ivar
# uninitialized ivar warnings # warnings
unless klass.instance_variable_defined?(:@abstract) unless klass.instance_variable_defined?(:@abstract)
klass.instance_variable_set(:@abstract, false) klass.instance_variable_set(:@abstract, false)
end end
super super
end end
# A list of all internal methods for a controller. This finds the first # A list of all internal methods for a controller. This finds the first abstract
# abstract superclass of a controller, and gets a list of all public # superclass of a controller, and gets a list of all public instance methods on
# instance methods on that abstract class. Public instance methods of # that abstract class. Public instance methods of a controller would normally be
# a controller would normally be considered action methods, so methods # considered action methods, so methods declared on abstract classes are being
# declared on abstract classes are being removed. # removed. (ActionController::Metal and ActionController::Base are defined as
# (ActionController::Metal and ActionController::Base are defined as abstract) # abstract)
def internal_methods def internal_methods
controller = self controller = self
methods = [] methods = []
@ -85,18 +86,18 @@ def internal_methods
controller.public_instance_methods(true) - methods controller.public_instance_methods(true) - methods
end end
# A list of method names that should be considered actions. This # A list of method names that should be considered actions. This includes all
# includes all public instance methods on a controller, less # public instance methods on a controller, less any internal methods (see
# any internal methods (see internal_methods), adding back in # internal_methods), adding back in any methods that are internal, but still
# any methods that are internal, but still exist on the class # exist on the class itself.
# itself. #
# #### Returns
# * `Set` - A set of all methods that should be considered actions.
# #
# ==== Returns
# * <tt>Set</tt> - A set of all methods that should be considered actions.
def action_methods def action_methods
@action_methods ||= begin @action_methods ||= begin
# All public instance methods of this class, including ancestors # All public instance methods of this class, including ancestors except for
# except for public instance methods of Base and its ancestors. # public instance methods of Base and its ancestors.
methods = public_instance_methods(true) - internal_methods methods = public_instance_methods(true) - internal_methods
# Be sure to include shadowed public instance methods of this class. # Be sure to include shadowed public instance methods of this class.
methods.concat(public_instance_methods(false)) methods.concat(public_instance_methods(false))
@ -105,23 +106,24 @@ def action_methods
end end
end end
# action_methods are cached and there is sometimes a need to refresh # action_methods are cached and there is sometimes a need to refresh them.
# them. ::clear_action_methods! allows you to do that, so next time # ::clear_action_methods! allows you to do that, so next time you run
# you run action_methods, they will be recalculated. # action_methods, they will be recalculated.
def clear_action_methods! def clear_action_methods!
@action_methods = nil @action_methods = nil
end end
# Returns the full controller name, underscored, without the ending Controller. # Returns the full controller name, underscored, without the ending Controller.
# #
# class MyApp::MyPostsController < AbstractController::Base # class MyApp::MyPostsController < AbstractController::Base
# #
# end # end
# #
# MyApp::MyPostsController.controller_path # => "my_app/my_posts" # MyApp::MyPostsController.controller_path # => "my_app/my_posts"
#
# #### Returns
# * `String`
# #
# ==== Returns
# * <tt>String</tt>
def controller_path def controller_path
@controller_path ||= name.delete_suffix("Controller").underscore unless anonymous? @controller_path ||= name.delete_suffix("Controller").underscore unless anonymous?
end end
@ -142,12 +144,13 @@ def eager_load! # :nodoc:
# Calls the action going through the entire Action Dispatch stack. # Calls the action going through the entire Action Dispatch stack.
# #
# The actual method that is called is determined by calling # The actual method that is called is determined by calling #method_for_action.
# #method_for_action. If no method can handle the action, then an # If no method can handle the action, then an AbstractController::ActionNotFound
# AbstractController::ActionNotFound error is raised. # error is raised.
#
# #### Returns
# * `self`
# #
# ==== Returns
# * <tt>self</tt>
def process(action, ...) def process(action, ...)
@_action_name = action.to_s @_action_name = action.to_s
@ -170,31 +173,30 @@ def action_methods
self.class.action_methods self.class.action_methods
end end
# Returns true if a method for the action is available and # Returns true if a method for the action is available and can be dispatched,
# can be dispatched, false otherwise. # false otherwise.
# #
# Notice that <tt>action_methods.include?("foo")</tt> may return # Notice that `action_methods.include?("foo")` may return false and
# false and <tt>available_action?("foo")</tt> returns true because # `available_action?("foo")` returns true because this method considers actions
# this method considers actions that are also available # that are also available through other means, for example, implicit render
# through other means, for example, implicit render ones. # ones.
#
# #### Parameters
# * `action_name` - The name of an action to be tested
# #
# ==== Parameters
# * <tt>action_name</tt> - The name of an action to be tested
def available_action?(action_name) def available_action?(action_name)
_find_action_name(action_name) _find_action_name(action_name)
end end
# Tests if a response body is set. Used to determine if the # Tests if a response body is set. Used to determine if the `process_action`
# +process_action+ callback needs to be terminated in # callback needs to be terminated in AbstractController::Callbacks.
# AbstractController::Callbacks.
def performed? def performed?
response_body response_body
end end
# Returns true if the given controller is capable of rendering # Returns true if the given controller is capable of rendering a path. A
# a path. A subclass of +AbstractController::Base+ # subclass of `AbstractController::Base` may return false. An Email controller
# may return false. An Email controller for example does not # for example does not support paths, only full URLs.
# support paths, only full URLs.
def self.supports_path? def self.supports_path?
true true
end end
@ -204,80 +206,83 @@ def inspect # :nodoc:
end end
private private
# Returns true if the name can be considered an action because # Returns true if the name can be considered an action because it has a method
# it has a method defined in the controller. # defined in the controller.
#
# #### Parameters
# * `name` - The name of an action to be tested
# #
# ==== Parameters
# * <tt>name</tt> - The name of an action to be tested
def action_method?(name) def action_method?(name)
self.class.action_methods.include?(name) self.class.action_methods.include?(name)
end end
# Call the action. Override this in a subclass to modify the # Call the action. Override this in a subclass to modify the behavior around
# behavior around processing an action. This, and not #process, # processing an action. This, and not #process, is the intended way to override
# is the intended way to override action dispatching. # action dispatching.
# #
# Notice that the first argument is the method to be dispatched # Notice that the first argument is the method to be dispatched which is **not**
# which is *not* necessarily the same as the action name. # necessarily the same as the action name.
def process_action(...) def process_action(...)
send_action(...) send_action(...)
end end
# Actually call the method associated with the action. Override # Actually call the method associated with the action. Override this method if
# this method if you wish to change how action methods are called, # you wish to change how action methods are called, not to add additional
# not to add additional behavior around it. For example, you would # behavior around it. For example, you would override #send_action if you want
# override #send_action if you want to inject arguments into the # to inject arguments into the method.
# method.
alias send_action send alias send_action send
# If the action name was not found, but a method called "action_missing" # If the action name was not found, but a method called "action_missing" was
# was found, #method_for_action will return "_handle_action_missing". # found, #method_for_action will return "_handle_action_missing". This method
# This method calls #action_missing with the current action name. # calls #action_missing with the current action name.
def _handle_action_missing(*args) def _handle_action_missing(*args)
action_missing(@_action_name, *args) action_missing(@_action_name, *args)
end end
# Takes an action name and returns the name of the method that will # Takes an action name and returns the name of the method that will handle the
# handle the action. # action.
# #
# It checks if the action name is valid and returns false otherwise. # It checks if the action name is valid and returns false otherwise.
# #
# See method_for_action for more information. # See method_for_action for more information.
# #
# ==== Parameters # #### Parameters
# * <tt>action_name</tt> - An action name to find a method name for # * `action_name` - An action name to find a method name for
# #
# ==== Returns #
# * <tt>string</tt> - The name of the method that handles the action # #### Returns
# * false - No valid method name could be found. # * `string` - The name of the method that handles the action
# Raise +AbstractController::ActionNotFound+. # * false - No valid method name could be found.
#
# Raise `AbstractController::ActionNotFound`.
def _find_action_name(action_name) def _find_action_name(action_name)
_valid_action_name?(action_name) && method_for_action(action_name) _valid_action_name?(action_name) && method_for_action(action_name)
end end
# Takes an action name and returns the name of the method that will # Takes an action name and returns the name of the method that will handle the
# handle the action. In normal cases, this method returns the same # action. In normal cases, this method returns the same name as it receives. By
# name as it receives. By default, if #method_for_action receives # default, if #method_for_action receives a name that is not an action, it will
# a name that is not an action, it will look for an #action_missing # look for an #action_missing method and return "_handle_action_missing" if one
# method and return "_handle_action_missing" if one is found. # is found.
# #
# Subclasses may override this method to add additional conditions # Subclasses may override this method to add additional conditions that should
# that should be considered an action. For instance, an HTTP controller # be considered an action. For instance, an HTTP controller with a template
# with a template matching the action name is considered to exist. # matching the action name is considered to exist.
# #
# If you override this method to handle additional cases, you may # If you override this method to handle additional cases, you may also provide a
# also provide a method (like +_handle_method_missing+) to handle # method (like `_handle_method_missing`) to handle the case.
# the case.
# #
# If none of these conditions are true, and +method_for_action+ # If none of these conditions are true, and `method_for_action` returns `nil`,
# returns +nil+, an +AbstractController::ActionNotFound+ exception will be raised. # an `AbstractController::ActionNotFound` exception will be raised.
# #
# ==== Parameters # #### Parameters
# * <tt>action_name</tt> - An action name to find a method name for # * `action_name` - An action name to find a method name for
#
#
# #### Returns
# * `string` - The name of the method that handles the action
# * `nil` - No method name could be found.
# #
# ==== Returns
# * <tt>string</tt> - The name of the method that handles the action
# * <tt>nil</tt> - No method name could be found.
def method_for_action(action_name) def method_for_action(action_name)
if action_method?(action_name) if action_method?(action_name)
action_name action_name

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
module AbstractController module AbstractController
module Caching module Caching
extend ActiveSupport::Concern extend ActiveSupport::Concern

@ -1,22 +1,23 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
module AbstractController module AbstractController
module Caching module Caching
# = Abstract Controller Caching \Fragments # # Abstract Controller Caching Fragments
# #
# Fragment caching is used for caching various blocks within # Fragment caching is used for caching various blocks within views without
# views without caching the entire action as a whole. This is # caching the entire action as a whole. This is useful when certain elements of
# useful when certain elements of an action change frequently or # an action change frequently or depend on complicated state while other parts
# depend on complicated state while other parts rarely change or # rarely change or can be shared amongst multiple parties. The caching is done
# can be shared amongst multiple parties. The caching is done using # using the `cache` helper available in the Action View. See
# the +cache+ helper available in the Action View. See
# ActionView::Helpers::CacheHelper for more information. # ActionView::Helpers::CacheHelper for more information.
# #
# While it's strongly recommended that you use key-based cache # While it's strongly recommended that you use key-based cache expiration (see
# expiration (see links in CacheHelper for more information), # links in CacheHelper for more information), it is also possible to manually
# it is also possible to manually expire caches. For example: # expire caches. For example:
# #
# expire_fragment('name_of_cache') # expire_fragment('name_of_cache')
module Fragments module Fragments
extend ActiveSupport::Concern extend ActiveSupport::Concern
@ -35,38 +36,35 @@ module Fragments
end end
module ClassMethods module ClassMethods
# Allows you to specify controller-wide key prefixes for # Allows you to specify controller-wide key prefixes for cache fragments. Pass
# cache fragments. Pass either a constant +value+, or a block # either a constant `value`, or a block which computes a value each time a cache
# which computes a value each time a cache key is generated. # key is generated.
# #
# For example, you may want to prefix all fragment cache keys # For example, you may want to prefix all fragment cache keys with a global
# with a global version identifier, so you can easily # version identifier, so you can easily invalidate all caches.
# invalidate all caches.
# #
# class ApplicationController # class ApplicationController
# fragment_cache_key "v1" # fragment_cache_key "v1"
# end # end
# #
# When it's time to invalidate all fragments, simply change # When it's time to invalidate all fragments, simply change the string constant.
# the string constant. Or, progressively roll out the cache # Or, progressively roll out the cache invalidation using a computed value:
# invalidation using a computed value: #
# # class ApplicationController
# class ApplicationController # fragment_cache_key do
# fragment_cache_key do # @account.id.odd? ? "v1" : "v2"
# @account.id.odd? ? "v1" : "v2" # end
# end # end
# end
def fragment_cache_key(value = nil, &key) def fragment_cache_key(value = nil, &key)
self.fragment_cache_keys += [key || -> { value }] self.fragment_cache_keys += [key || -> { value }]
end end
end end
# Given a key (as described in +expire_fragment+), returns # Given a key (as described in `expire_fragment`), returns a key array suitable
# a key array suitable for use in reading, writing, or expiring a # for use in reading, writing, or expiring a cached fragment. All keys begin
# cached fragment. All keys begin with <tt>:views</tt>, # with `:views`, followed by `ENV["RAILS_CACHE_ID"]` or
# followed by <tt>ENV["RAILS_CACHE_ID"]</tt> or <tt>ENV["RAILS_APP_VERSION"]</tt> if set, # `ENV["RAILS_APP_VERSION"]` if set, followed by any controller-wide key prefix
# followed by any controller-wide key prefix values, ending # values, ending with the specified `key` value.
# with the specified +key+ value.
def combined_fragment_cache_key(key) def combined_fragment_cache_key(key)
head = self.class.fragment_cache_keys.map { |k| instance_exec(&k) } head = self.class.fragment_cache_keys.map { |k| instance_exec(&k) }
tail = key.is_a?(Hash) ? url_for(key).split("://").last : key tail = key.is_a?(Hash) ? url_for(key).split("://").last : key
@ -77,8 +75,8 @@ def combined_fragment_cache_key(key)
cache_key cache_key
end end
# Writes +content+ to the location signified by # Writes `content` to the location signified by `key` (see `expire_fragment` for
# +key+ (see +expire_fragment+ for acceptable formats). # acceptable formats).
def write_fragment(key, content, options = nil) def write_fragment(key, content, options = nil)
return content unless cache_configured? return content unless cache_configured?
@ -90,8 +88,8 @@ def write_fragment(key, content, options = nil)
content content
end end
# Reads a cached fragment from the location signified by +key+ # Reads a cached fragment from the location signified by `key` (see
# (see +expire_fragment+ for acceptable formats). # `expire_fragment` for acceptable formats).
def read_fragment(key, options = nil) def read_fragment(key, options = nil)
return unless cache_configured? return unless cache_configured?
@ -102,8 +100,8 @@ def read_fragment(key, options = nil)
end end
end end
# Check if a cached fragment from the location signified by # Check if a cached fragment from the location signified by `key` exists (see
# +key+ exists (see +expire_fragment+ for acceptable formats). # `expire_fragment` for acceptable formats).
def fragment_exist?(key, options = nil) def fragment_exist?(key, options = nil)
return unless cache_configured? return unless cache_configured?
key = combined_fragment_cache_key(key) key = combined_fragment_cache_key(key)
@ -115,22 +113,21 @@ def fragment_exist?(key, options = nil)
# Removes fragments from the cache. # Removes fragments from the cache.
# #
# +key+ can take one of three forms: # `key` can take one of three forms:
# #
# * String - This would normally take the form of a path, like # * String - This would normally take the form of a path, like
# <tt>pages/45/notes</tt>. # `pages/45/notes`.
# * Hash - Treated as an implicit call to +url_for+, like # * Hash - Treated as an implicit call to `url_for`, like `{ controller:
# <tt>{ controller: 'pages', action: 'notes', id: 45}</tt> # 'pages', action: 'notes', id: 45}`
# * Regexp - Will remove any fragment that matches, so # * Regexp - Will remove any fragment that matches, so `%r{pages/\d*/notes}`
# <tt>%r{pages/\d*/notes}</tt> might remove all notes. Make sure you # might remove all notes. Make sure you don't use anchors in the regex (`^`
# don't use anchors in the regex (<tt>^</tt> or <tt>$</tt>) because # or `$`) because the actual filename matched looks like
# the actual filename matched looks like # `./cache/filename/path.cache`. Note: Regexp expiration is only supported
# <tt>./cache/filename/path.cache</tt>. Note: Regexp expiration is # on caches that can iterate over all keys (unlike memcached).
# only supported on caches that can iterate over all keys (unlike
# memcached).
# #
# +options+ is passed through to the cache store's +delete+ #
# method (or <tt>delete_matched</tt>, for Regexp keys). # `options` is passed through to the cache store's `delete` method (or
# `delete_matched`, for Regexp keys).
def expire_fragment(key, options = nil) def expire_fragment(key, options = nil)
return unless cache_configured? return unless cache_configured?
key = combined_fragment_cache_key(key) unless key.is_a?(Regexp) key = combined_fragment_cache_key(key) unless key.is_a?(Regexp)

@ -1,29 +1,32 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
module AbstractController module AbstractController
# = Abstract Controller \Callbacks # # Abstract Controller Callbacks
# #
# Abstract Controller provides hooks during the life cycle of a controller action. # Abstract Controller provides hooks during the life cycle of a controller
# Callbacks allow you to trigger logic during this cycle. Available callbacks are: # action. Callbacks allow you to trigger logic during this cycle. Available
# callbacks are:
# #
# * <tt>after_action</tt> # * `after_action`
# * <tt>append_after_action</tt> # * `append_after_action`
# * <tt>append_around_action</tt> # * `append_around_action`
# * <tt>append_before_action</tt> # * `append_before_action`
# * <tt>around_action</tt> # * `around_action`
# * <tt>before_action</tt> # * `before_action`
# * <tt>prepend_after_action</tt> # * `prepend_after_action`
# * <tt>prepend_around_action</tt> # * `prepend_around_action`
# * <tt>prepend_before_action</tt> # * `prepend_before_action`
# * <tt>skip_after_action</tt> # * `skip_after_action`
# * <tt>skip_around_action</tt> # * `skip_around_action`
# * <tt>skip_before_action</tt> # * `skip_before_action`
module Callbacks module Callbacks
extend ActiveSupport::Concern extend ActiveSupport::Concern
# Uses ActiveSupport::Callbacks as the base functionality. For # Uses ActiveSupport::Callbacks as the base functionality. For more details on
# more details on the whole callback system, read the documentation # the whole callback system, read the documentation for
# for ActiveSupport::Callbacks. # ActiveSupport::Callbacks.
include ActiveSupport::Callbacks include ActiveSupport::Callbacks
included do included do
@ -69,25 +72,24 @@ def match?(controller)
end end
module ClassMethods module ClassMethods
# If +:only+ or +:except+ are used, convert the options into the # If `:only` or `:except` are used, convert the options into the `:if` and
# +:if+ and +:unless+ options of ActiveSupport::Callbacks. # `:unless` options of ActiveSupport::Callbacks.
# #
# The basic idea is that <tt>:only => :index</tt> gets converted to # The basic idea is that `:only => :index` gets converted to `:if => proc {|c|
# <tt>:if => proc {|c| c.action_name == "index" }</tt>. # c.action_name == "index" }`.
# #
# Note that <tt>:only</tt> has priority over <tt>:if</tt> in case they # Note that `:only` has priority over `:if` in case they are used together.
# are used together.
# #
# only: :index, if: -> { true } # the :if option will be ignored. # only: :index, if: -> { true } # the :if option will be ignored.
# #
# Note that <tt>:if</tt> has priority over <tt>:except</tt> in case they # Note that `:if` has priority over `:except` in case they are used together.
# are used together.
# #
# except: :index, if: -> { true } # the :except option will be ignored. # except: :index, if: -> { true } # the :except option will be ignored.
#
# #### Options
# * `only` - The callback should be run only for this action.
# * `except` - The callback should be run for all actions except this action.
# #
# ==== Options
# * <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.
def _normalize_callback_options(options) def _normalize_callback_options(options)
_normalize_callback_option(options, :only, :if) _normalize_callback_option(options, :only, :if)
_normalize_callback_option(options, :except, :unless) _normalize_callback_option(options, :except, :unless)
@ -101,18 +103,20 @@ def _normalize_callback_option(options, from, to) # :nodoc:
end end
end end
# Take callback names and an optional callback proc, normalize them, # Take callback names and an optional callback proc, normalize them, then call
# then call the block with each callback. This allows us to abstract # the block with each callback. This allows us to abstract the normalization
# the normalization across several methods that use it. # across several methods that use it.
# #
# ==== Parameters # #### Parameters
# * <tt>callbacks</tt> - An array of callbacks, with an optional # * `callbacks` - An array of callbacks, with an optional options hash as the
# options hash as the last parameter. # last parameter.
# * <tt>block</tt> - A proc that should be added to the callbacks. # * `block` - A proc that should be added to the callbacks.
#
#
# #### Block Parameters
# * `name` - The callback to be added.
# * `options` - A hash of options to be used when adding the callback.
# #
# ==== Block Parameters
# * <tt>name</tt> - The callback to be added.
# * <tt>options</tt> - A hash of options to be used when adding the callback.
def _insert_callbacks(callbacks, block = nil) def _insert_callbacks(callbacks, block = nil)
options = callbacks.extract_options! options = callbacks.extract_options!
callbacks.push(block) if block callbacks.push(block) if block
@ -131,20 +135,21 @@ def _insert_callbacks(callbacks, block = nil)
# #
# Append a callback before actions. See _insert_callbacks for parameter details. # Append a callback before actions. See _insert_callbacks for parameter details.
# #
# If the callback renders or redirects, the action will not run. If there # If the callback renders or redirects, the action will not run. If there are
# are additional callbacks scheduled to run after that callback, they are # additional callbacks scheduled to run after that callback, they are also
# also cancelled. # cancelled.
## ##
# :method: prepend_before_action # :method: prepend_before_action
# #
# :call-seq: prepend_before_action(names, block) # :call-seq: prepend_before_action(names, block)
# #
# Prepend a callback before actions. See _insert_callbacks for parameter details. # Prepend a callback before actions. See _insert_callbacks for parameter
# details.
# #
# If the callback renders or redirects, the action will not run. If there # If the callback renders or redirects, the action will not run. If there are
# are additional callbacks scheduled to run after that callback, they are # additional callbacks scheduled to run after that callback, they are also
# also cancelled. # cancelled.
## ##
# :method: skip_before_action # :method: skip_before_action
@ -160,9 +165,9 @@ def _insert_callbacks(callbacks, block = nil)
# #
# Append a callback before actions. See _insert_callbacks for parameter details. # Append a callback before actions. See _insert_callbacks for parameter details.
# #
# If the callback renders or redirects, the action will not run. If there # If the callback renders or redirects, the action will not run. If there are
# are additional callbacks scheduled to run after that callback, they are # additional callbacks scheduled to run after that callback, they are also
# also cancelled. # cancelled.
## ##
# :method: after_action # :method: after_action
@ -204,7 +209,8 @@ def _insert_callbacks(callbacks, block = nil)
# #
# :call-seq: prepend_around_action(names, block) # :call-seq: prepend_around_action(names, block)
# #
# Prepend a callback around actions. See _insert_callbacks for parameter details. # Prepend a callback around actions. See _insert_callbacks for parameter
# details.
## ##
# :method: skip_around_action # :method: skip_around_action
@ -219,9 +225,8 @@ def _insert_callbacks(callbacks, block = nil)
# :call-seq: append_around_action(names, block) # :call-seq: append_around_action(names, block)
# #
# Append a callback around actions. See _insert_callbacks for parameter details. # Append a callback around actions. See _insert_callbacks for parameter details.
# set up before_action, prepend_before_action, skip_before_action, etc. for each
# set up before_action, prepend_before_action, skip_before_action, etc. # of before, after, and around.
# for each of before, after, and around.
[:before, :after, :around].each do |callback| [:before, :after, :around].each do |callback|
define_method "#{callback}_action" do |*names, &blk| define_method "#{callback}_action" do |*names, &blk|
_insert_callbacks(names, blk) do |name, options| _insert_callbacks(names, blk) do |name, options|
@ -235,8 +240,8 @@ def _insert_callbacks(callbacks, block = nil)
end end
end end
# Skip a before, after or around callback. See _insert_callbacks # Skip a before, after or around callback. See _insert_callbacks for details on
# for details on the allowed parameters. # the allowed parameters.
define_method "skip_#{callback}_action" do |*names| define_method "skip_#{callback}_action" do |*names|
_insert_callbacks(names) do |name, options| _insert_callbacks(names) do |name, options|
skip_callback(:process_action, callback, name, options) skip_callback(:process_action, callback, name, options)
@ -249,8 +254,8 @@ def _insert_callbacks(callbacks, block = nil)
end end
private private
# Override <tt>AbstractController::Base#process_action</tt> to run the # Override `AbstractController::Base#process_action` to run the `process_action`
# <tt>process_action</tt> callbacks around the normal behavior. # callbacks around the normal behavior.
def process_action(...) def process_action(...)
run_callbacks(:process_action) do run_callbacks(:process_action) do
super super

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
require "action_dispatch/http/mime_type" require "action_dispatch/http/mime_type"
module AbstractController module AbstractController

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
module AbstractController module AbstractController
def self.deprecator # :nodoc: def self.deprecator # :nodoc:
@deprecator ||= ActiveSupport::Deprecation.new @deprecator ||= ActiveSupport::Deprecation.new

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
module AbstractController module AbstractController
class Error < StandardError # :nodoc: class Error < StandardError # :nodoc:
end end

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
require "active_support/dependencies" require "active_support/dependencies"
require "active_support/core_ext/name_error" require "active_support/core_ext/name_error"
@ -10,8 +12,8 @@ module Helpers
included do included do
class_attribute :_helper_methods, default: Array.new class_attribute :_helper_methods, default: Array.new
# This is here so that it is always higher in the inheritance chain than # This is here so that it is always higher in the inheritance chain than the
# the definition in lib/action_view/rendering.rb # definition in lib/action_view/rendering.rb
redefine_singleton_method(:_helpers) do redefine_singleton_method(:_helpers) do
if @_helpers ||= nil if @_helpers ||= nil
@_helpers @_helpers
@ -60,9 +62,9 @@ def helper_modules_from_paths(paths)
extend Resolution extend Resolution
module ClassMethods module ClassMethods
# When a class is inherited, wrap its helper module in a new module. # When a class is inherited, wrap its helper module in a new module. This
# This ensures that the parent class's module can be changed # ensures that the parent class's module can be changed independently of the
# independently of the child class's. # child class's.
def inherited(klass) def inherited(klass)
# Inherited from parent by default # Inherited from parent by default
klass._helpers = nil klass._helpers = nil
@ -79,49 +81,48 @@ def inherited(klass)
# :method: modules_for_helpers # :method: modules_for_helpers
# :call-seq: modules_for_helpers(modules_or_helper_prefixes) # :call-seq: modules_for_helpers(modules_or_helper_prefixes)
# #
# Given an array of values like the ones accepted by +helper+, this method # Given an array of values like the ones accepted by `helper`, this method
# returns an array with the corresponding modules, in the same order. # returns an array with the corresponding modules, in the same order.
# #
# ActionController::Base.modules_for_helpers(["application", "chart", "rubygems"]) # ActionController::Base.modules_for_helpers(["application", "chart", "rubygems"])
# # => [ApplicationHelper, ChartHelper, RubygemsHelper] # # => [ApplicationHelper, ChartHelper, RubygemsHelper]
# #
#-- #--
# Implemented by Resolution#modules_for_helpers. # Implemented by Resolution#modules_for_helpers.
## # :method: # all_helpers_from_path
# :method: all_helpers_from_path
# :call-seq: all_helpers_from_path(path) # :call-seq: all_helpers_from_path(path)
# #
# Returns a list of helper names in a given path. # Returns a list of helper names in a given path.
# #
# ActionController::Base.all_helpers_from_path 'app/helpers' # ActionController::Base.all_helpers_from_path 'app/helpers'
# # => ["application", "chart", "rubygems"] # # => ["application", "chart", "rubygems"]
# #
#-- #--
# Implemented by Resolution#all_helpers_from_path. # Implemented by Resolution#all_helpers_from_path.
# Declare a controller method as a helper. For example, the following # Declare a controller method as a helper. For example, the following
# makes the +current_user+ and +logged_in?+ controller methods available # makes the `current_user` and `logged_in?` controller methods available
# to the view: # to the view:
# class ApplicationController < ActionController::Base # class ApplicationController < ActionController::Base
# helper_method :current_user, :logged_in? # helper_method :current_user, :logged_in?
# #
# private # private
# def current_user # def current_user
# @current_user ||= User.find_by(id: session[:user]) # @current_user ||= User.find_by(id: session[:user])
# end # end
# #
# def logged_in? # def logged_in?
# current_user != nil # current_user != nil
# end # end
# end # end
# #
# In a view: # In a view:
# <% if logged_in? -%>Welcome, <%= current_user.name %><% end -%> # <% if logged_in? -%>Welcome, <%= current_user.name %><% end -%>
# #
# ==== Parameters # #### Parameters
# * <tt>method[, method]</tt> - A name or names of a method on the controller # * `method[, method]` - A name or names of a method on the controller to be
# to be made available on the view. # made available on the view.
def helper_method(*methods) def helper_method(*methods)
methods.flatten! methods.flatten!
self._helper_methods += methods self._helper_methods += methods
@ -131,7 +132,7 @@ def helper_method(*methods)
methods.each do |method| methods.each do |method|
# def current_user(*args, &block) # def current_user(*args, &block)
# controller.send(:'current_user', *args, &block) # controller.send(:'current_user', *args, &block)
# end # end
_helpers_for_modification.class_eval <<~ruby_eval.lines.map(&:strip).join(";"), file, line _helpers_for_modification.class_eval <<~ruby_eval.lines.map(&:strip).join(";"), file, line
def #{method}(...) def #{method}(...)
@ -143,54 +144,54 @@ def #{method}(...)
# Includes the given modules in the template class. # Includes the given modules in the template class.
# #
# Modules can be specified in different ways. All of the following calls # Modules can be specified in different ways. All of the following calls include
# include +FooHelper+: # `FooHelper`:
# #
# # Module, recommended. # # Module, recommended.
# helper FooHelper # helper FooHelper
# #
# # String/symbol without the "helper" suffix, camel or snake case. # # String/symbol without the "helper" suffix, camel or snake case.
# helper "Foo" # helper "Foo"
# helper :Foo # helper :Foo
# helper "foo" # helper "foo"
# helper :foo # helper :foo
# #
# The last two assume that <tt>"foo".camelize</tt> returns "Foo". # The last two assume that `"foo".camelize` returns "Foo".
# #
# When strings or symbols are passed, the method finds the actual module # When strings or symbols are passed, the method finds the actual module object
# object using String#constantize. Therefore, if the module has not been # using String#constantize. Therefore, if the module has not been yet loaded, it
# yet loaded, it has to be autoloadable, which is normally the case. # has to be autoloadable, which is normally the case.
# #
# Namespaces are supported. The following calls include +Foo::BarHelper+: # Namespaces are supported. The following calls include `Foo::BarHelper`:
# #
# # Module, recommended. # # Module, recommended.
# helper Foo::BarHelper # helper Foo::BarHelper
# #
# # String/symbol without the "helper" suffix, camel or snake case. # # String/symbol without the "helper" suffix, camel or snake case.
# helper "Foo::Bar" # helper "Foo::Bar"
# helper :"Foo::Bar" # helper :"Foo::Bar"
# helper "foo/bar" # helper "foo/bar"
# helper :"foo/bar" # helper :"foo/bar"
# #
# The last two assume that <tt>"foo/bar".camelize</tt> returns "Foo::Bar". # The last two assume that `"foo/bar".camelize` returns "Foo::Bar".
# #
# The method accepts a block too. If present, the block is evaluated in # The method accepts a block too. If present, the block is evaluated in the
# the context of the controller helper module. This simple call makes the # context of the controller helper module. This simple call makes the `wadus`
# +wadus+ method available in templates of the enclosing controller: # method available in templates of the enclosing controller:
# #
# helper do # helper do
# def wadus # def wadus
# "wadus" # "wadus"
# end
# end # end
# end
# #
# Furthermore, all the above styles can be mixed together: # Furthermore, all the above styles can be mixed together:
# #
# helper FooHelper, "woo", "bar/baz" do # helper FooHelper, "woo", "bar/baz" do
# def wadus # def wadus
# "wadus" # "wadus"
# end
# end # end
# end
# #
def helper(*args, &block) def helper(*args, &block)
modules_for_helpers(args).each do |mod| modules_for_helpers(args).each do |mod|
@ -201,8 +202,8 @@ def helper(*args, &block)
_helpers_for_modification.module_eval(&block) if block_given? _helpers_for_modification.module_eval(&block) if block_given?
end end
# Clears up all existing helpers in this class, only keeping the helper # Clears up all existing helpers in this class, only keeping the helper with the
# with the same name as this class. # same name as this class.
def clear_helpers def clear_helpers
inherited_helper_methods = _helper_methods inherited_helper_methods = _helper_methods
self._helpers = Module.new self._helpers = Module.new
@ -221,8 +222,8 @@ def _helpers_for_modification
private private
def define_helpers_module(klass, helpers = nil) def define_helpers_module(klass, helpers = nil)
# In some tests inherited is called explicitly. In that case, just # In some tests inherited is called explicitly. In that case, just return the
# return the module from the first time it was defined # module from the first time it was defined
return klass.const_get(:HelperMethods) if klass.const_defined?(:HelperMethods, false) return klass.const_get(:HelperMethods) if klass.const_defined?(:HelperMethods, false)
mod = Module.new mod = Module.new

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
require "active_support/benchmarkable" require "active_support/benchmarkable"
module AbstractController module AbstractController

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
require "active_support/core_ext/module/introspection" require "active_support/core_ext/module/introspection"
module AbstractController module AbstractController

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
require "abstract_controller/error" require "abstract_controller/error"
require "action_view" require "action_view"
require "action_view/view_paths" require "action_view/view_paths"
@ -19,9 +21,9 @@ module Rendering
include ActionView::ViewPaths include ActionView::ViewPaths
# Normalizes arguments and options, and then delegates to render_to_body and # Normalizes arguments and options, and then delegates to render_to_body and
# sticks the result in <tt>self.response_body</tt>. # sticks the result in `self.response_body`.
# #
# Supported options depend on the underlying +render_to_body+ implementation. # Supported options depend on the underlying `render_to_body` implementation.
def render(*args, &block) def render(*args, &block)
options = _normalize_render(*args, &block) options = _normalize_render(*args, &block)
rendered_body = render_to_body(options) rendered_body = render_to_body(options)
@ -35,11 +37,11 @@ def render(*args, &block)
end end
# Similar to #render, but only returns the rendered template as a string, # Similar to #render, but only returns the rendered template as a string,
# instead of setting +self.response_body+. # instead of setting `self.response_body`.
# #
# If a component extends the semantics of +response_body+ (as ActionController # If a component extends the semantics of `response_body` (as ActionController
# extends it to be anything that responds to the method each), this method # extends it to be anything that responds to the method each), this method needs
# needs to be overridden in order to still return a string. # to be overridden in order to still return a string.
def render_to_string(*args, &block) def render_to_string(*args, &block)
options = _normalize_render(*args, &block) options = _normalize_render(*args, &block)
render_to_body(options) render_to_body(options)
@ -49,15 +51,15 @@ def render_to_string(*args, &block)
def render_to_body(options = {}) def render_to_body(options = {})
end end
# Returns +Content-Type+ of rendered content. # Returns `Content-Type` of rendered content.
def rendered_format def rendered_format
Mime[:text] Mime[:text]
end end
DEFAULT_PROTECTED_INSTANCE_VARIABLES = %i(@_action_name @_response_body @_formats @_prefixes) DEFAULT_PROTECTED_INSTANCE_VARIABLES = %i(@_action_name @_response_body @_formats @_prefixes)
# This method should return a hash with assigns. # This method should return a hash with assigns. You can overwrite this
# You can overwrite this configuration per controller. # configuration per controller.
def view_assigns def view_assigns
variables = instance_variables - _protected_ivars variables = instance_variables - _protected_ivars
@ -67,9 +69,8 @@ def view_assigns
end end
private private
# Normalize args by converting <tt>render "foo"</tt> to # Normalize args by converting `render "foo"` to `render action: "foo"` and
# <tt>render action: "foo"</tt> and <tt>render "foo/bar"</tt> to # `render "foo/bar"` to `render file: "foo/bar"`.
# <tt>render file: "foo/bar"</tt>.
def _normalize_args(action = nil, options = {}) # :doc: def _normalize_args(action = nil, options = {}) # :doc:
if action.respond_to?(:permitted?) if action.respond_to?(:permitted?)
if action.permitted? if action.permitted?

@ -1,17 +1,19 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
require "active_support/html_safe_translation" require "active_support/html_safe_translation"
module AbstractController module AbstractController
module Translation module Translation
# Delegates to <tt>I18n.translate</tt>. # Delegates to `I18n.translate`.
# #
# When the given key starts with a period, it will be scoped by the current # When the given key starts with a period, it will be scoped by the current
# controller and action. So if you call <tt>translate(".foo")</tt> from # controller and action. So if you call `translate(".foo")` from
# <tt>PeopleController#index</tt>, it will convert the call to # `PeopleController#index`, it will convert the call to
# <tt>I18n.translate("people.index.foo")</tt>. This makes it less repetitive # `I18n.translate("people.index.foo")`. This makes it less repetitive to
# to translate many keys within the same controller / action and gives you a # translate many keys within the same controller / action and gives you a simple
# simple framework for scoping them consistently. # framework for scoping them consistently.
def translate(key, **options) def translate(key, **options)
if key&.start_with?(".") if key&.start_with?(".")
path = controller_path.tr("/", ".") path = controller_path.tr("/", ".")
@ -25,7 +27,7 @@ def translate(key, **options)
end end
alias :t :translate alias :t :translate
# Delegates to <tt>I18n.localize</tt>. # Delegates to `I18n.localize`.
def localize(object, **options) def localize(object, **options)
I18n.localize(object, **options) I18n.localize(object, **options)
end end

@ -1,14 +1,16 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
module AbstractController module AbstractController
# = URL For # # URL For
# #
# Includes +url_for+ into the host class (e.g. an abstract controller or mailer). The class # Includes `url_for` into the host class (e.g. an abstract controller or
# has to provide a +RouteSet+ by implementing the <tt>_routes</tt> methods. Otherwise, an # mailer). The class has to provide a `RouteSet` by implementing the `_routes`
# exception will be raised. # methods. Otherwise, an exception will be raised.
# #
# Note that this module is completely decoupled from HTTP - the only requirement is a valid # Note that this module is completely decoupled from HTTP - the only requirement
# <tt>_routes</tt> implementation. # is a valid `_routes` implementation.
module UrlFor module UrlFor
extend ActiveSupport::Concern extend ActiveSupport::Concern
include ActionDispatch::Routing::UrlFor include ActionDispatch::Routing::UrlFor

@ -1,12 +1,14 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
require "abstract_controller" require "abstract_controller"
require "action_dispatch" require "action_dispatch"
require "action_controller/deprecator" require "action_controller/deprecator"
require "action_controller/metal/strong_parameters" require "action_controller/metal/strong_parameters"
require "action_controller/metal/exceptions" require "action_controller/metal/exceptions"
# = Action Controller # # Action Controller
# #
# Action Controller is a module of Action Pack. # Action Controller is a module of Action Pack.
# #

@ -1,107 +1,108 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
require "action_view" require "action_view"
require "action_controller" require "action_controller"
require "action_controller/log_subscriber" require "action_controller/log_subscriber"
module ActionController module ActionController
# = Action Controller \API # # Action Controller API
# #
# API Controller is a lightweight version of ActionController::Base, # API Controller is a lightweight version of ActionController::Base, created for
# created for applications that don't require all functionalities that a complete # applications that don't require all functionalities that a complete Rails
# \Rails controller provides, allowing you to create controllers with just the # controller provides, allowing you to create controllers with just the features
# features that you need for API only applications. # that you need for API only applications.
# #
# An API Controller is different from a normal controller in the sense that # An API Controller is different from a normal controller in the sense that by
# by default it doesn't include a number of features that are usually required # default it doesn't include a number of features that are usually required by
# by browser access only: layouts and templates rendering, # browser access only: layouts and templates rendering, flash, assets, and so
# flash, assets, and so on. This makes the entire controller stack thinner, # on. This makes the entire controller stack thinner, suitable for API
# suitable for API applications. It doesn't mean you won't have such # applications. It doesn't mean you won't have such features if you need them:
# features if you need them: they're all available for you to include in # they're all available for you to include in your application, they're just not
# your application, they're just not part of the default API controller stack. # part of the default API controller stack.
# #
# Normally, +ApplicationController+ is the only controller that inherits from # Normally, `ApplicationController` is the only controller that inherits from
# +ActionController::API+. All other controllers in turn inherit from # `ActionController::API`. All other controllers in turn inherit from
# +ApplicationController+. # `ApplicationController`.
# #
# A sample controller could look like this: # A sample controller could look like this:
# #
# class PostsController < ApplicationController # class PostsController < ApplicationController
# def index # def index
# posts = Post.all # posts = Post.all
# render json: posts # render json: posts
# end
# end # end
# end
# #
# Request, response, and parameters objects all work the exact same way as # Request, response, and parameters objects all work the exact same way as
# ActionController::Base. # ActionController::Base.
# #
# == Renders # ## Renders
# #
# The default API Controller stack includes all renderers, which means you # The default API Controller stack includes all renderers, which means you can
# can use <tt>render :json</tt> and siblings freely in your controllers. Keep # use `render :json` and siblings freely in your controllers. Keep in mind that
# in mind that templates are not going to be rendered, so you need to ensure # templates are not going to be rendered, so you need to ensure your controller
# your controller is calling either <tt>render</tt> or <tt>redirect_to</tt> in # is calling either `render` or `redirect_to` in all actions, otherwise it will
# all actions, otherwise it will return <tt>204 No Content</tt>. # return `204 No Content`.
# #
# def show # def show
# post = Post.find(params[:id]) # post = Post.find(params[:id])
# render json: post # render json: post
# end # end
# #
# == Redirects # ## Redirects
# #
# Redirects are used to move from one action to another. You can use the # Redirects are used to move from one action to another. You can use the
# <tt>redirect_to</tt> method in your controllers in the same way as in # `redirect_to` method in your controllers in the same way as in
# ActionController::Base. For example: # ActionController::Base. For example:
# #
# def create # def create
# redirect_to root_url and return if not_authorized? # redirect_to root_url and return if not_authorized?
# # do stuff here # # do stuff here
# end # end
# #
# == Adding New Behavior # ## Adding New Behavior
# #
# In some scenarios you may want to add back some functionality provided by # In some scenarios you may want to add back some functionality provided by
# ActionController::Base that is not present by default in # ActionController::Base that is not present by default in
# +ActionController::API+, for instance <tt>MimeResponds</tt>. This # `ActionController::API`, for instance `MimeResponds`. This module gives you
# module gives you the <tt>respond_to</tt> method. Adding it is quite simple, # the `respond_to` method. Adding it is quite simple, you just need to include
# you just need to include the module in a specific controller or in # the module in a specific controller or in `ApplicationController` in case you
# +ApplicationController+ in case you want it available in your entire # want it available in your entire application:
# application:
# #
# class ApplicationController < ActionController::API # class ApplicationController < ActionController::API
# include ActionController::MimeResponds # include ActionController::MimeResponds
# end # end
# #
# class PostsController < ApplicationController # class PostsController < ApplicationController
# def index # def index
# posts = Post.all # posts = Post.all
# #
# respond_to do |format| # respond_to do |format|
# format.json { render json: posts } # format.json { render json: posts }
# format.xml { render xml: posts } # format.xml { render xml: posts }
# end
# end # end
# end # end
# end
# #
# Make sure to check the modules included in ActionController::Base # Make sure to check the modules included in ActionController::Base if you want
# if you want to use any other functionality that is not provided # to use any other functionality that is not provided by `ActionController::API`
# by +ActionController::API+ out of the box. # out of the box.
class API < Metal class API < Metal
abstract! abstract!
# Shortcut helper that returns all the ActionController::API modules except # Shortcut helper that returns all the ActionController::API modules except the
# the ones passed as arguments: # ones passed as arguments:
# #
# class MyAPIBaseController < ActionController::Metal # class MyAPIBaseController < ActionController::Metal
# ActionController::API.without_modules(:UrlFor).each do |left| # ActionController::API.without_modules(:UrlFor).each do |left|
# include left # include left
# end
# end # end
# end
# #
# This gives better control over what you want to exclude and makes it easier # This gives better control over what you want to exclude and makes it easier to
# to create an API controller class, instead of listing the modules required # create an API controller class, instead of listing the modules required
# manually. # manually.
def self.without_modules(*modules) def self.without_modules(*modules)
modules = modules.map do |m| modules = modules.map do |m|
@ -127,19 +128,19 @@ def self.without_modules(*modules)
DefaultHeaders, DefaultHeaders,
Logging, Logging,
# Before callbacks should also be executed as early as possible, so # Before callbacks should also be executed as early as possible, so also include
# also include them at the bottom. # them at the bottom.
AbstractController::Callbacks, AbstractController::Callbacks,
# Append rescue at the bottom to wrap as much as possible. # Append rescue at the bottom to wrap as much as possible.
Rescue, Rescue,
# Add instrumentations hooks at the bottom, to ensure they instrument # Add instrumentations hooks at the bottom, to ensure they instrument all the
# all the methods properly. # methods properly.
Instrumentation, Instrumentation,
# Params wrapper should come before instrumentation so they are # Params wrapper should come before instrumentation so they are properly showed
# properly showed in logs # in logs
ParamsWrapper ParamsWrapper
] ]

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
module ActionController module ActionController
module ApiRendering module ApiRendering
extend ActiveSupport::Concern extend ActiveSupport::Concern

@ -1,170 +1,205 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
require "action_view" require "action_view"
require "action_controller/log_subscriber" require "action_controller/log_subscriber"
require "action_controller/metal/params_wrapper" require "action_controller/metal/params_wrapper"
module ActionController module ActionController
# = Action Controller \Base # # Action Controller Base
# #
# Action Controllers are the core of a web request in \Rails. They are made up of one or more actions that are executed # Action Controllers are the core of a web request in Rails. They are made up of
# on request and then either it renders a template or redirects to another action. An action is defined as a public method # one or more actions that are executed on request and then either it renders a
# on the controller, which will automatically be made accessible to the web-server through \Rails Routes. # template or redirects to another action. An action is defined as a public
# method on the controller, which will automatically be made accessible to the
# web-server through Rails Routes.
# #
# By default, only the ApplicationController in a \Rails application inherits from +ActionController::Base+. All other # By default, only the ApplicationController in a Rails application inherits
# controllers inherit from ApplicationController. This gives you one class to configure things such as # from `ActionController::Base`. All other controllers inherit from
# ApplicationController. This gives you one class to configure things such as
# request forgery protection and filtering of sensitive request parameters. # request forgery protection and filtering of sensitive request parameters.
# #
# A sample controller could look like this: # A sample controller could look like this:
# #
# class PostsController < ApplicationController # class PostsController < ApplicationController
# def index # def index
# @posts = Post.all # @posts = Post.all
# end
#
# def create
# @post = Post.create params[:post]
# redirect_to posts_path
# end
# end # end
# #
# def create # Actions, by default, render a template in the `app/views` directory
# @post = Post.create params[:post] # corresponding to the name of the controller and action after executing code in
# redirect_to posts_path # the action. For example, the `index` action of the PostsController would
# render the template `app/views/posts/index.html.erb` by default after
# populating the `@posts` instance variable.
#
# Unlike index, the create action will not render a template. After performing
# its main purpose (creating a new post), it initiates a redirect instead. This
# redirect works by returning an external `302 Moved` HTTP response that takes
# the user to the index action.
#
# These two methods represent the two basic action archetypes used in Action
# Controllers: Get-and-show and do-and-redirect. Most actions are variations on
# these themes.
#
# ## Requests
#
# For every request, the router determines the value of the `controller` and
# `action` keys. These determine which controller and action are called. The
# remaining request parameters, the session (if one is available), and the full
# request with all the HTTP headers are made available to the action through
# accessor methods. Then the action is performed.
#
# The full request object is available via the request accessor and is primarily
# used to query for HTTP headers:
#
# def server_ip
# location = request.env["REMOTE_ADDR"]
# render plain: "This server hosted at #{location}"
# end # end
# end
# #
# Actions, by default, render a template in the <tt>app/views</tt> directory corresponding to the name of the controller and action # ## Parameters
# after executing code in the action. For example, the +index+ action of the PostsController would render the
# template <tt>app/views/posts/index.html.erb</tt> by default after populating the <tt>@posts</tt> instance variable.
# #
# Unlike index, the create action will not render a template. After performing its main purpose (creating a # All request parameters, whether they come from a query string in the URL or
# new post), it initiates a redirect instead. This redirect works by returning an external # form data submitted through a POST request are available through the `params`
# <tt>302 Moved</tt> HTTP response that takes the user to the index action. # method which returns a hash. For example, an action that was performed through
# `/posts?category=All&limit=5` will include `{ "category" => "All", "limit" =>
# "5" }` in `params`.
# #
# These two methods represent the two basic action archetypes used in Action Controllers: Get-and-show and do-and-redirect. # It's also possible to construct multi-dimensional parameter hashes by
# Most actions are variations on these themes. # specifying keys using brackets, such as:
# #
# == Requests # <input type="text" name="post[name]" value="david">
# <input type="text" name="post[address]" value="hyacintvej">
# #
# For every request, the router determines the value of the +controller+ and +action+ keys. These determine which controller # A request coming from a form holding these inputs will include `{ "post" => {
# and action are called. The remaining request parameters, the session (if one is available), and the full request with # "name" => "david", "address" => "hyacintvej" } }`. If the address input had
# all the HTTP headers are made available to the action through accessor methods. Then the action is performed. # been named `post[address][street]`, the `params` would have included `{ "post"
# => { "address" => { "street" => "hyacintvej" } } }`. There's no limit to the
# depth of the nesting.
# #
# The full request object is available via the request accessor and is primarily used to query for HTTP headers: # ## Sessions
# #
# def server_ip # Sessions allow you to store objects in between requests. This is useful for
# location = request.env["REMOTE_ADDR"] # objects that are not yet ready to be persisted, such as a Signup object
# render plain: "This server hosted at #{location}" # constructed in a multi-paged process, or objects that don't change much and
# end # are needed all the time, such as a User object for a system that requires
# login. The session should not be used, however, as a cache for objects where
# it's likely they could be changed unknowingly. It's usually too much work to
# keep it all synchronized -- something databases already excel at.
# #
# == Parameters # You can place objects in the session by using the `session` method, which
# accesses a hash:
# #
# All request parameters, whether they come from a query string in the URL or form data submitted through a POST request are # session[:person] = Person.authenticate(user_name, password)
# available through the <tt>params</tt> method which returns a hash. For example, an action that was performed through
# <tt>/posts?category=All&limit=5</tt> will include <tt>{ "category" => "All", "limit" => "5" }</tt> in <tt>params</tt>.
#
# It's also possible to construct multi-dimensional parameter hashes by specifying keys using brackets, such as:
#
# <input type="text" name="post[name]" value="david">
# <input type="text" name="post[address]" value="hyacintvej">
#
# A request coming from a form holding these inputs will include <tt>{ "post" => { "name" => "david", "address" => "hyacintvej" } }</tt>.
# If the address input had been named <tt>post[address][street]</tt>, the <tt>params</tt> would have included
# <tt>{ "post" => { "address" => { "street" => "hyacintvej" } } }</tt>. There's no limit to the depth of the nesting.
#
# == Sessions
#
# Sessions allow you to store objects in between requests. This is useful for objects that are not yet ready to be persisted,
# such as a Signup object constructed in a multi-paged process, or objects that don't change much and are needed all the time, such
# as a User object for a system that requires login. The session should not be used, however, as a cache for objects where it's likely
# they could be changed unknowingly. It's usually too much work to keep it all synchronized -- something databases already excel at.
#
# You can place objects in the session by using the <tt>session</tt> method, which accesses a hash:
#
# session[:person] = Person.authenticate(user_name, password)
# #
# You can retrieve it again through the same hash: # You can retrieve it again through the same hash:
# #
# "Hello #{session[:person]}" # "Hello #{session[:person]}"
# #
# For removing objects from the session, you can either assign a single key to +nil+: # For removing objects from the session, you can either assign a single key to
# `nil`:
# #
# # removes :person from session # # removes :person from session
# session[:person] = nil # session[:person] = nil
# #
# or you can remove the entire session with +reset_session+. # or you can remove the entire session with `reset_session`.
# #
# By default, sessions are stored in an encrypted browser cookie (see # By default, sessions are stored in an encrypted browser cookie (see
# ActionDispatch::Session::CookieStore). Thus the user will not be able to # ActionDispatch::Session::CookieStore). Thus the user will not be able to read
# read or edit the session data. However, the user can keep a copy of the # or edit the session data. However, the user can keep a copy of the cookie even
# cookie even after it has expired, so you should avoid storing sensitive # after it has expired, so you should avoid storing sensitive information in
# information in cookie-based sessions. # cookie-based sessions.
# #
# == Responses # ## Responses
# #
# Each action results in a response, which holds the headers and document to be sent to the user's browser. The actual response # Each action results in a response, which holds the headers and document to be
# object is generated automatically through the use of renders and redirects and requires no user intervention. # sent to the user's browser. The actual response object is generated
# automatically through the use of renders and redirects and requires no user
# intervention.
# #
# == Renders # ## Renders
# #
# Action Controller sends content to the user by using one of five rendering methods. The most versatile and common is the rendering # Action Controller sends content to the user by using one of five rendering
# of a template. Included in the Action Pack is the Action View, which enables rendering of ERB templates. It's automatically configured. # methods. The most versatile and common is the rendering of a template.
# The controller passes objects to the view by assigning instance variables: # Included in the Action Pack is the Action View, which enables rendering of ERB
# templates. It's automatically configured. The controller passes objects to the
# view by assigning instance variables:
# #
# def show # def show
# @post = Post.find(params[:id]) # @post = Post.find(params[:id])
# end # end
# #
# Which are then automatically available to the view: # Which are then automatically available to the view:
# #
# Title: <%= @post.title %> # Title: <%= @post.title %>
# #
# You don't have to rely on the automated rendering. For example, actions that could result in the rendering of different templates # You don't have to rely on the automated rendering. For example, actions that
# will use the manual rendering methods: # could result in the rendering of different templates will use the manual
# rendering methods:
# #
# def search # def search
# @results = Search.find(params[:query]) # @results = Search.find(params[:query])
# case @results.count # case @results.count
# when 0 then render action: "no_results" # when 0 then render action: "no_results"
# when 1 then render action: "show" # when 1 then render action: "show"
# when 2..10 then render action: "show_many" # when 2..10 then render action: "show_many"
# end
# end # end
# end
# #
# Read more about writing ERB and Builder templates in ActionView::Base. # Read more about writing ERB and Builder templates in ActionView::Base.
# #
# == Redirects # ## Redirects
# #
# Redirects are used to move from one action to another. For example, after a <tt>create</tt> action, which stores a blog entry to the # Redirects are used to move from one action to another. For example, after a
# database, we might like to show the user the new entry. Because we're following good DRY principles (Don't Repeat Yourself), we're # `create` action, which stores a blog entry to the database, we might like to
# going to reuse (and redirect to) a <tt>show</tt> action that we'll assume has already been created. The code might look like this: # show the user the new entry. Because we're following good DRY principles
# (Don't Repeat Yourself), we're going to reuse (and redirect to) a `show`
# action that we'll assume has already been created. The code might look like
# this:
# #
# def create # def create
# @entry = Entry.new(params[:entry]) # @entry = Entry.new(params[:entry])
# if @entry.save # if @entry.save
# # The entry was saved correctly, redirect to show # # The entry was saved correctly, redirect to show
# redirect_to action: 'show', id: @entry.id # redirect_to action: 'show', id: @entry.id
# else # else
# # things didn't go so well, do something else # # things didn't go so well, do something else
# end
# end # end
# end
# #
# In this case, after saving our new entry to the database, the user is redirected to the <tt>show</tt> method, which is then executed. # In this case, after saving our new entry to the database, the user is
# Note that this is an external HTTP-level redirection which will cause the browser to make a second request (a GET to the show action), # redirected to the `show` method, which is then executed. Note that this is an
# and not some internal re-routing which calls both "create" and then "show" within one request. # external HTTP-level redirection which will cause the browser to make a second
# request (a GET to the show action), and not some internal re-routing which
# calls both "create" and then "show" within one request.
# #
# Learn more about <tt>redirect_to</tt> and what options you have in ActionController::Redirecting. # Learn more about `redirect_to` and what options you have in
# ActionController::Redirecting.
# #
# == Calling multiple redirects or renders # ## Calling multiple redirects or renders
# #
# An action may contain only a single render or a single redirect. Attempting to try to do either again will result in a DoubleRenderError: # An action may contain only a single render or a single redirect. Attempting to
# try to do either again will result in a DoubleRenderError:
# #
# def do_something # def do_something
# redirect_to action: "elsewhere" # redirect_to action: "elsewhere"
# render action: "overthere" # raises DoubleRenderError # render action: "overthere" # raises DoubleRenderError
# end # end
# #
# If you need to redirect on the condition of something, then be sure to add "and return" to halt execution. # If you need to redirect on the condition of something, then be sure to add
# "and return" to halt execution.
# #
# def do_something # def do_something
# redirect_to(action: "elsewhere") and return if monkeys.nil? # redirect_to(action: "elsewhere") and return if monkeys.nil?
# render action: "overthere" # won't be called if monkeys is nil # render action: "overthere" # won't be called if monkeys is nil
# end # end
# #
class Base < Metal class Base < Metal
abstract! abstract!
@ -172,15 +207,15 @@ class Base < Metal
# Shortcut helper that returns all the modules included in # Shortcut helper that returns all the modules included in
# ActionController::Base except the ones passed as arguments: # ActionController::Base except the ones passed as arguments:
# #
# class MyBaseController < ActionController::Metal # class MyBaseController < ActionController::Metal
# ActionController::Base.without_modules(:ParamsWrapper, :Streaming).each do |left| # ActionController::Base.without_modules(:ParamsWrapper, :Streaming).each do |left|
# include left # include left
# end
# end # end
# end
# #
# This gives better control over what you want to exclude and makes it # This gives better control over what you want to exclude and makes it easier to
# easier to create a bare controller class, instead of listing the modules # create a bare controller class, instead of listing the modules required
# required manually. # manually.
def self.without_modules(*modules) def self.without_modules(*modules)
modules = modules.map do |m| modules = modules.map do |m|
m.is_a?(Symbol) ? ActionController.const_get(m) : m m.is_a?(Symbol) ? ActionController.const_get(m) : m
@ -224,19 +259,19 @@ def self.without_modules(*modules)
DefaultHeaders, DefaultHeaders,
Logging, Logging,
# Before callbacks should also be executed as early as possible, so # Before callbacks should also be executed as early as possible, so also include
# also include them at the bottom. # them at the bottom.
AbstractController::Callbacks, AbstractController::Callbacks,
# Append rescue at the bottom to wrap as much as possible. # Append rescue at the bottom to wrap as much as possible.
Rescue, Rescue,
# Add instrumentations hooks at the bottom, to ensure they instrument # Add instrumentations hooks at the bottom, to ensure they instrument all the
# all the methods properly. # methods properly.
Instrumentation, Instrumentation,
# Params wrapper should come before instrumentation so they are # Params wrapper should come before instrumentation so they are properly showed
# properly showed in logs # in logs
ParamsWrapper ParamsWrapper
] ]

@ -1,28 +1,31 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
module ActionController module ActionController
# = Action Controller \Caching # # Action Controller Caching
# #
# \Caching is a cheap way of speeding up slow applications by keeping the result of # Caching is a cheap way of speeding up slow applications by keeping the result
# calculations, renderings, and database calls around for subsequent requests. # of calculations, renderings, and database calls around for subsequent
# requests.
# #
# You can read more about each approach by clicking the modules below. # You can read more about each approach by clicking the modules below.
# #
# Note: To turn off all caching provided by Action Controller, set # Note: To turn off all caching provided by Action Controller, set
# config.action_controller.perform_caching = false # config.action_controller.perform_caching = false
# #
# == \Caching stores # ## Caching stores
# #
# All the caching stores from ActiveSupport::Cache are available to be used as backends # All the caching stores from ActiveSupport::Cache are available to be used as
# for Action Controller caching. # backends for Action Controller caching.
# #
# Configuration examples (FileStore is the default): # Configuration examples (FileStore is the default):
# #
# config.action_controller.cache_store = :memory_store # config.action_controller.cache_store = :memory_store
# config.action_controller.cache_store = :file_store, '/path/to/cache/directory' # config.action_controller.cache_store = :file_store, '/path/to/cache/directory'
# config.action_controller.cache_store = :mem_cache_store, 'localhost' # config.action_controller.cache_store = :mem_cache_store, 'localhost'
# config.action_controller.cache_store = :mem_cache_store, Memcached::Rails.new('localhost:11211') # config.action_controller.cache_store = :mem_cache_store, Memcached::Rails.new('localhost:11211')
# config.action_controller.cache_store = MyOwnStore.new('parameter') # config.action_controller.cache_store = MyOwnStore.new('parameter')
module Caching module Caching
extend ActiveSupport::Concern extend ActiveSupport::Concern

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
module ActionController module ActionController
def self.deprecator # :nodoc: def self.deprecator # :nodoc:
AbstractController.deprecator AbstractController.deprecator

@ -1,31 +1,33 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
module ActionController module ActionController
# = Action Controller Form Builder # # Action Controller Form Builder
# #
# Override the default form builder for all views rendered by this # Override the default form builder for all views rendered by this controller
# controller and any of its descendants. Accepts a subclass of # and any of its descendants. Accepts a subclass of
# ActionView::Helpers::FormBuilder. # ActionView::Helpers::FormBuilder.
# #
# For example, given a form builder: # For example, given a form builder:
# #
# class AdminFormBuilder < ActionView::Helpers::FormBuilder # class AdminFormBuilder < ActionView::Helpers::FormBuilder
# def special_field(name) # def special_field(name)
# end
# end # end
# end
# #
# The controller specifies a form builder as its default: # The controller specifies a form builder as its default:
# #
# class AdminAreaController < ApplicationController # class AdminAreaController < ApplicationController
# default_form_builder AdminFormBuilder # default_form_builder AdminFormBuilder
# end # end
# #
# Then in the view any form using +form_for+ will be an instance of the # Then in the view any form using `form_for` will be an instance of the
# specified form builder: # specified form builder:
# #
# <%= form_for(@instance) do |builder| %> # <%= form_for(@instance) do |builder| %>
# <%= builder.special_field(:name) %> # <%= builder.special_field(:name) %>
# <% end %> # <% end %>
module FormBuilder module FormBuilder
extend ActiveSupport::Concern extend ActiveSupport::Concern
@ -34,11 +36,12 @@ module FormBuilder
end end
module ClassMethods module ClassMethods
# Set the form builder to be used as the default for all forms # Set the form builder to be used as the default for all forms in the views
# in the views rendered by this controller and its subclasses. # rendered by this controller and its subclasses.
# #
# ==== Parameters # #### Parameters
# * <tt>builder</tt> - Default form builder, an instance of ActionView::Helpers::FormBuilder # * `builder` - Default form builder, an instance of
# ActionView::Helpers::FormBuilder
def default_form_builder(builder) def default_form_builder(builder)
self._default_form_builder = builder self._default_form_builder = builder
end end

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
module ActionController module ActionController
class LogSubscriber < ActiveSupport::LogSubscriber class LogSubscriber < ActiveSupport::LogSubscriber
INTERNAL_PARAMS = %w(controller action format _method only_path) INTERNAL_PARAMS = %w(controller action format _method only_path)

@ -1,17 +1,19 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
require "active_support/core_ext/array/extract_options" require "active_support/core_ext/array/extract_options"
require "action_dispatch/middleware/stack" require "action_dispatch/middleware/stack"
module ActionController module ActionController
# = Action Controller \MiddlewareStack # # Action Controller MiddlewareStack
# #
# Extend ActionDispatch middleware stack to make it aware of options # Extend ActionDispatch middleware stack to make it aware of options allowing
# allowing the following syntax in controllers: # the following syntax in controllers:
# #
# class PostsController < ApplicationController # class PostsController < ApplicationController
# use AuthenticationMiddleware, except: [:index, :show] # use AuthenticationMiddleware, except: [:index, :show]
# end # end
# #
class MiddlewareStack < ActionDispatch::MiddlewareStack # :nodoc: class MiddlewareStack < ActionDispatch::MiddlewareStack # :nodoc:
class Middleware < ActionDispatch::MiddlewareStack::Middleware # :nodoc: class Middleware < ActionDispatch::MiddlewareStack::Middleware # :nodoc:
@ -60,73 +62,71 @@ def build_middleware(klass, args, block)
end end
end end
# = Action Controller \Metal # # Action Controller Metal
# #
# +ActionController::Metal+ is the simplest possible controller, providing a # `ActionController::Metal` is the simplest possible controller, providing a
# valid Rack interface without the additional niceties provided by # valid Rack interface without the additional niceties provided by
# ActionController::Base. # ActionController::Base.
# #
# A sample metal controller might look like this: # A sample metal controller might look like this:
# #
# class HelloController < ActionController::Metal # class HelloController < ActionController::Metal
# def index # def index
# self.response_body = "Hello World!" # self.response_body = "Hello World!"
# end
# end # end
# end
# #
# And then to route requests to your metal controller, you would add # And then to route requests to your metal controller, you would add something
# something like this to <tt>config/routes.rb</tt>: # like this to `config/routes.rb`:
# #
# get 'hello', to: HelloController.action(:index) # get 'hello', to: HelloController.action(:index)
# #
# The +action+ method returns a valid Rack application for the \Rails # The `action` method returns a valid Rack application for the Rails router to
# router to dispatch to. # dispatch to.
# #
# == \Rendering \Helpers # ## Rendering Helpers
# #
# +ActionController::Metal+ by default provides no utilities for rendering # `ActionController::Metal` by default provides no utilities for rendering
# views, partials, or other responses aside from explicitly calling of # views, partials, or other responses aside from explicitly calling of
# <tt>response_body=</tt>, <tt>content_type=</tt>, and <tt>status=</tt>. To # `response_body=`, `content_type=`, and `status=`. To add the render helpers
# add the render helpers you're used to having in a normal controller, you # you're used to having in a normal controller, you can do the following:
# can do the following:
# #
# class HelloController < ActionController::Metal # class HelloController < ActionController::Metal
# include AbstractController::Rendering # include AbstractController::Rendering
# include ActionView::Layouts # include ActionView::Layouts
# append_view_path "#{Rails.root}/app/views" # append_view_path "#{Rails.root}/app/views"
# #
# def index # def index
# render "hello/index" # render "hello/index"
# end
# end # end
# end
# #
# == Redirection \Helpers # ## Redirection Helpers
# #
# To add redirection helpers to your metal controller, do the following: # To add redirection helpers to your metal controller, do the following:
# #
# class HelloController < ActionController::Metal # class HelloController < ActionController::Metal
# include ActionController::Redirecting # include ActionController::Redirecting
# include Rails.application.routes.url_helpers # include Rails.application.routes.url_helpers
# #
# def index # def index
# redirect_to root_url # redirect_to root_url
# end
# end # end
# end
# #
# == Other \Helpers # ## Other Helpers
#
# You can refer to the modules included in ActionController::Base to see
# other features you can bring into your metal controller.
# #
# You can refer to the modules included in ActionController::Base to see other
# features you can bring into your metal controller.
class Metal < AbstractController::Base class Metal < AbstractController::Base
abstract! abstract!
# Returns the last part of the controller's name, underscored, without the ending # Returns the last part of the controller's name, underscored, without the
# <tt>Controller</tt>. For instance, +PostsController+ returns <tt>posts</tt>. # ending `Controller`. For instance, `PostsController` returns `posts`.
# Namespaces are left out, so +Admin::PostsController+ returns <tt>posts</tt> as well. # Namespaces are left out, so `Admin::PostsController` returns `posts` as well.
# #
# ==== Returns # #### Returns
# * <tt>string</tt> # * `string`
def self.controller_name def self.controller_name
@controller_name ||= (name.demodulize.delete_suffix("Controller").underscore unless anonymous?) @controller_name ||= (name.demodulize.delete_suffix("Controller").underscore unless anonymous?)
end end
@ -172,7 +172,7 @@ def controller_name
## ##
# The ActionDispatch::Request::Session instance for the current request. # The ActionDispatch::Request::Session instance for the current request.
# See further details in the # See further details in the
# {Active Controller Session guide}[https://guides.rubyonrails.org/action_controller_overview.html#session]. # [Active Controller Session guide](https://guides.rubyonrails.org/action_controller_overview.html#session).
delegate :session, to: "@_request" delegate :session, to: "@_request"
## ##
@ -201,7 +201,7 @@ def params=(val)
alias :response_code :status # :nodoc: alias :response_code :status # :nodoc:
# Basic \url_for that can be overridden for more robust functionality. # Basic url_for that can be overridden for more robust functionality.
def url_for(string) def url_for(string)
string string
end end
@ -238,7 +238,8 @@ def set_response!(response) # :nodoc:
@_response = response @_response = response
end end
# Assign the response and mark it as committed. No further processing will occur. # Assign the response and mark it as committed. No further processing will
# occur.
def response=(response) def response=(response)
set_response!(response) set_response!(response)
@ -271,15 +272,15 @@ def use(...)
# The middleware stack used by this controller. # The middleware stack used by this controller.
# #
# By default uses a variation of ActionDispatch::MiddlewareStack which # By default uses a variation of ActionDispatch::MiddlewareStack which allows
# allows for the following syntax: # for the following syntax:
# #
# class PostsController < ApplicationController # class PostsController < ApplicationController
# use AuthenticationMiddleware, except: [:index, :show] # use AuthenticationMiddleware, except: [:index, :show]
# end # end
# #
# Read more about {Rails middleware # Read more about [Rails middleware stack]
# stack}[https://guides.rubyonrails.org/rails_on_rack.html#action-dispatcher-middleware-stack] # (https://guides.rubyonrails.org/rails_on_rack.html#action-dispatcher-middleware-stack)
# in the guides. # in the guides.
def self.middleware def self.middleware
middleware_stack middleware_stack
@ -300,8 +301,8 @@ def self.action(name)
end end
end end
# Direct dispatch to the controller. Instantiates the controller, then # Direct dispatch to the controller. Instantiates the controller, then executes
# executes the action named +name+. # the action named `name`.
def self.dispatch(name, req, res) def self.dispatch(name, req, res)
if middleware_stack.any? if middleware_stack.any?
middleware_stack.build(name) { |env| new.dispatch(name, req, res) }.call req.env middleware_stack.build(name) { |env| new.dispatch(name, req, res) }.call req.env

@ -1,41 +1,48 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
module ActionController # :nodoc: module ActionController # :nodoc:
module AllowBrowser module AllowBrowser
extend ActiveSupport::Concern extend ActiveSupport::Concern
module ClassMethods module ClassMethods
# Specify the browser versions that will be allowed to access all actions (or some, as limited by <tt>only:</tt> or <tt>except:</tt>). # Specify the browser versions that will be allowed to access all actions (or
# Only browsers matched in the hash or named set passed to <tt>versions:</tt> will be blocked if they're below the versions specified. # some, as limited by `only:` or `except:`). Only browsers matched in the hash
# This means that all other browsers, as well as agents that aren't reporting a user-agent header, will be allowed access. # or named set passed to `versions:` will be blocked if they're below the
# versions specified. This means that all other browsers, as well as agents that
# aren't reporting a user-agent header, will be allowed access.
# #
# A browser that's blocked will by default be served the file in public/426.html with a HTTP status code of "426 Upgrade Required". # A browser that's blocked will by default be served the file in public/426.html
# with a HTTP status code of "426 Upgrade Required".
# #
# In addition to specifically named browser versions, you can also pass <tt>:modern</tt> as the set to restrict support to browsers # In addition to specifically named browser versions, you can also pass
# natively supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has. # `:modern` as the set to restrict support to browsers natively supporting webp
# This includes Safari 17.2+, Chrome 119+, Firefox 121+, Opera 104+. # images, web push, badges, import maps, CSS nesting, and CSS :has. This
# includes Safari 17.2+, Chrome 119+, Firefox 121+, Opera 104+.
# #
# You can use https://caniuse.com to check for browser versions supporting the features you use. # You can use https://caniuse.com to check for browser versions supporting the
# features you use.
# #
# You can use +ActiveSupport::Notifications+ to subscribe to events of browsers being blocked using the +browser_block.action_controller+ # You can use `ActiveSupport::Notifications` to subscribe to events of browsers
# event name. # being blocked using the `browser_block.action_controller` event name.
# #
# Examples: # Examples:
# #
# class ApplicationController < ActionController::Base # class ApplicationController < ActionController::Base
# # Allow only browsers natively supporting webp images, web push, badges, import maps, CSS nesting + :has # # Allow only browsers natively supporting webp images, web push, badges, import maps, CSS nesting + :has
# allow_browser versions: :modern # allow_browser versions: :modern
# end # end
# #
# class ApplicationController < ActionController::Base # class ApplicationController < ActionController::Base
# # All versions of Chrome and Opera will be allowed, but no versions of "internet explorer" (ie). Safari needs to be 16.4+ and Firefox 121+. # # All versions of Chrome and Opera will be allowed, but no versions of "internet explorer" (ie). Safari needs to be 16.4+ and Firefox 121+.
# allow_browser versions: { safari: 16.4, firefox: 121, ie: false } # allow_browser versions: { safari: 16.4, firefox: 121, ie: false }
# end # end
# #
# class MessagesController < ApplicationController # class MessagesController < ApplicationController
# # In addition to the browsers blocked by ApplicationController, also block Opera below 104 and Chrome below 119 for the show action. # # In addition to the browsers blocked by ApplicationController, also block Opera below 104 and Chrome below 119 for the show action.
# allow_browser versions: { opera: 104, chrome: 119 }, only: :show # allow_browser versions: { opera: 104, chrome: 119 }, only: :show
# end # end
def allow_browser(versions:, block: -> { render file: Rails.root.join("public/426.html"), layout: false, status: :upgrade_required }, **options) def allow_browser(versions:, block: -> { render file: Rails.root.join("public/426.html"), layout: false, status: :upgrade_required }, **options)
before_action -> { allow_browser(versions: versions, block: block) }, **options before_action -> { allow_browser(versions: versions, block: block) }, **options
end end

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
module ActionController module ActionController
module BasicImplicitRender # :nodoc: module BasicImplicitRender # :nodoc:
def send_action(method, *args) def send_action(method, *args)

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
require "active_support/core_ext/object/try" require "active_support/core_ext/object/try"
require "active_support/core_ext/integer/time" require "active_support/core_ext/integer/time"
@ -14,116 +16,118 @@ module ConditionalGet
end end
module ClassMethods module ClassMethods
# Allows you to consider additional controller-wide information when generating an ETag. # Allows you to consider additional controller-wide information when generating
# For example, if you serve pages tailored depending on who's logged in at the moment, you # an ETag. For example, if you serve pages tailored depending on who's logged in
# may want to add the current user id to be part of the ETag to prevent unauthorized displaying # at the moment, you may want to add the current user id to be part of the ETag
# of cached pages. # to prevent unauthorized displaying of cached pages.
# #
# class InvoicesController < ApplicationController # class InvoicesController < ApplicationController
# etag { current_user&.id } # etag { current_user&.id }
# #
# def show # def show
# # Etag will differ even for the same invoice when it's viewed by a different current_user # # Etag will differ even for the same invoice when it's viewed by a different current_user
# @invoice = Invoice.find(params[:id]) # @invoice = Invoice.find(params[:id])
# fresh_when etag: @invoice # fresh_when etag: @invoice
# end
# end # end
# end
def etag(&etagger) def etag(&etagger)
self.etaggers += [etagger] self.etaggers += [etagger]
end end
end end
# Sets the +etag+, +last_modified+, or both on the response, and renders a # Sets the `etag`, `last_modified`, or both on the response, and renders a `304
# <tt>304 Not Modified</tt> response if the request is already fresh. # Not Modified` response if the request is already fresh.
# #
# ==== Options # #### Options
# #
# [+:etag+] # `:etag`
# Sets a "weak" ETag validator on the response. See the +:weak_etag+ option. # : Sets a "weak" ETag validator on the response. See the `:weak_etag` option.
# [+:weak_etag+] # `:weak_etag`
# Sets a "weak" ETag validator on the response. Requests that specify an # : Sets a "weak" ETag validator on the response. Requests that specify an
# +If-None-Match+ header may receive a <tt>304 Not Modified</tt> response # `If-None-Match` header may receive a `304 Not Modified` response if the
# if the ETag matches exactly. # ETag matches exactly.
# #
# A weak ETag indicates semantic equivalence, not byte-for-byte equality, # A weak ETag indicates semantic equivalence, not byte-for-byte equality, so
# so they're good for caching HTML pages in browser caches. They can't be # they're good for caching HTML pages in browser caches. They can't be used
# used for responses that must be byte-identical, like serving +Range+ # for responses that must be byte-identical, like serving `Range` requests
# requests within a PDF file. # within a PDF file.
# [+:strong_etag+] # `:strong_etag`
# Sets a "strong" ETag validator on the response. Requests that specify an # : Sets a "strong" ETag validator on the response. Requests that specify an
# +If-None-Match+ header may receive a <tt>304 Not Modified</tt> response # `If-None-Match` header may receive a `304 Not Modified` response if the
# if the ETag matches exactly. # ETag matches exactly.
# #
# A strong ETag implies exact equality -- the response must match byte for # A strong ETag implies exact equality -- the response must match byte for
# byte. This is necessary for serving +Range+ requests within a large # byte. This is necessary for serving `Range` requests within a large video
# video or PDF file, for example, or for compatibility with some CDNs that # or PDF file, for example, or for compatibility with some CDNs that don't
# don't support weak ETags. # support weak ETags.
# [+:last_modified+] # `:last_modified`
# Sets a "weak" last-update validator on the response. Subsequent requests # : Sets a "weak" last-update validator on the response. Subsequent requests
# that specify an +If-Modified-Since+ header may receive a <tt>304 Not Modified</tt> # that specify an `If-Modified-Since` header may receive a `304 Not
# response if +last_modified+ <= +If-Modified-Since+. # Modified` response if `last_modified` <= `If-Modified-Since`.
# [+:public+] # `:public`
# By default the +Cache-Control+ header is private. Set this option to # : By default the `Cache-Control` header is private. Set this option to
# +true+ if you want your application to be cacheable by other devices, # `true` if you want your application to be cacheable by other devices, such
# such as proxy caches. # as proxy caches.
# [+:cache_control+] # `:cache_control`
# When given, will overwrite an existing +Cache-Control+ header. For a # : When given, will overwrite an existing `Cache-Control` header. For a list
# list of +Cache-Control+ directives, see the {article on # of `Cache-Control` directives, see the [article on
# MDN}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control]. # MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Contr
# [+:template+] # ol).
# By default, the template digest for the current controller/action is # `:template`
# included in ETags. If the action renders a different template, you can # : By default, the template digest for the current controller/action is
# include its digest instead. If the action doesn't render a template at # included in ETags. If the action renders a different template, you can
# all, you can pass <tt>template: false</tt> to skip any attempt to check # include its digest instead. If the action doesn't render a template at
# for a template digest. # all, you can pass `template: false` to skip any attempt to check for a
# template digest.
# #
# ==== Examples
# #
# def show # #### Examples
# @article = Article.find(params[:id])
# fresh_when(etag: @article, last_modified: @article.updated_at, public: true)
# end
# #
# This will send a <tt>304 Not Modified</tt> response if the request # def show
# specifies a matching ETag and +If-Modified-Since+ header. Otherwise, it # @article = Article.find(params[:id])
# will render the +show+ template. # fresh_when(etag: @article, last_modified: @article.updated_at, public: true)
# end
#
# This will send a `304 Not Modified` response if the request specifies a
# matching ETag and `If-Modified-Since` header. Otherwise, it will render the
# `show` template.
# #
# You can also just pass a record: # You can also just pass a record:
# #
# def show # def show
# @article = Article.find(params[:id]) # @article = Article.find(params[:id])
# fresh_when(@article) # fresh_when(@article)
# end # end
# #
# +etag+ will be set to the record, and +last_modified+ will be set to the # `etag` will be set to the record, and `last_modified` will be set to the
# record's +updated_at+. # record's `updated_at`.
# #
# You can also pass an object that responds to +maximum+, such as a # You can also pass an object that responds to `maximum`, such as a collection
# collection of records: # of records:
# #
# def index # def index
# @articles = Article.all # @articles = Article.all
# fresh_when(@articles) # fresh_when(@articles)
# end # end
# #
# In this case, +etag+ will be set to the collection, and +last_modified+ # In this case, `etag` will be set to the collection, and `last_modified` will
# will be set to <tt>maximum(:updated_at)</tt> (the timestamp of the most # be set to `maximum(:updated_at)` (the timestamp of the most recently updated
# recently updated record). # record).
# #
# When passing a record or a collection, you can still specify other # When passing a record or a collection, you can still specify other options,
# options, such as +:public+ and +:cache_control+: # such as `:public` and `:cache_control`:
# #
# def show # def show
# @article = Article.find(params[:id]) # @article = Article.find(params[:id])
# fresh_when(@article, public: true, cache_control: { no_cache: true }) # fresh_when(@article, public: true, cache_control: { no_cache: true })
# end # end
# #
# The above will set <tt>Cache-Control: public, no-cache</tt> in the response. # The above will set `Cache-Control: public, no-cache` in the response.
# #
# When rendering a different template than the controller/action's default # When rendering a different template than the controller/action's default
# template, you can indicate which digest to include in the ETag: # template, you can indicate which digest to include in the ETag:
# #
# before_action { fresh_when @article, template: "widgets/show" } # 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, cache_control: {}, template: nil) def fresh_when(object = nil, etag: nil, weak_etag: nil, strong_etag: nil, last_modified: nil, public: false, cache_control: {}, template: nil)
response.cache_control.delete(:no_store) response.cache_control.delete(:no_store)
@ -145,131 +149,132 @@ def fresh_when(object = nil, etag: nil, weak_etag: nil, strong_etag: nil, last_m
head :not_modified if request.fresh?(response) head :not_modified if request.fresh?(response)
end end
# Sets the +etag+ and/or +last_modified+ on the response and checks them # Sets the `etag` and/or `last_modified` on the response and checks them against
# against the request. If the request doesn't match the provided options, it # the request. If the request doesn't match the provided options, it is
# is considered stale, and the response should be rendered from scratch. # considered stale, and the response should be rendered from scratch. Otherwise,
# Otherwise, it is fresh, and a <tt>304 Not Modified</tt> is sent. # it is fresh, and a `304 Not Modified` is sent.
# #
# ==== Options # #### Options
# #
# See #fresh_when for supported options. # See #fresh_when for supported options.
# #
# ==== Examples # #### Examples
# #
# def show # def show
# @article = Article.find(params[:id]) # @article = Article.find(params[:id])
# #
# if stale?(etag: @article, last_modified: @article.updated_at) # if stale?(etag: @article, last_modified: @article.updated_at)
# @statistics = @article.really_expensive_call # @statistics = @article.really_expensive_call
# respond_to do |format| # respond_to do |format|
# # all the supported formats # # all the supported formats
# end
# end # end
# end # end
# end
# #
# You can also just pass a record: # You can also just pass a record:
# #
# def show # def show
# @article = Article.find(params[:id]) # @article = Article.find(params[:id])
# #
# if stale?(@article) # if stale?(@article)
# @statistics = @article.really_expensive_call # @statistics = @article.really_expensive_call
# respond_to do |format| # respond_to do |format|
# # all the supported formats # # all the supported formats
# end
# end # end
# end # end
# end
# #
# +etag+ will be set to the record, and +last_modified+ will be set to the # `etag` will be set to the record, and `last_modified` will be set to the
# record's +updated_at+. # record's `updated_at`.
# #
# You can also pass an object that responds to +maximum+, such as a # You can also pass an object that responds to `maximum`, such as a collection
# collection of records: # of records:
# #
# def index # def index
# @articles = Article.all # @articles = Article.all
# #
# if stale?(@articles) # if stale?(@articles)
# @statistics = @articles.really_expensive_call # @statistics = @articles.really_expensive_call
# respond_to do |format| # respond_to do |format|
# # all the supported formats # # all the supported formats
# end
# end # end
# end # end
# end
# #
# In this case, +etag+ will be set to the collection, and +last_modified+ # In this case, `etag` will be set to the collection, and `last_modified` will
# will be set to <tt>maximum(:updated_at)</tt> (the timestamp of the most # be set to `maximum(:updated_at)` (the timestamp of the most recently updated
# recently updated record). # record).
# #
# When passing a record or a collection, you can still specify other # When passing a record or a collection, you can still specify other options,
# options, such as +:public+ and +:cache_control+: # such as `:public` and `:cache_control`:
# #
# def show # def show
# @article = Article.find(params[:id]) # @article = Article.find(params[:id])
# #
# if stale?(@article, public: true, cache_control: { no_cache: true }) # if stale?(@article, public: true, cache_control: { no_cache: true })
# @statistics = @articles.really_expensive_call # @statistics = @articles.really_expensive_call
# respond_to do |format| # respond_to do |format|
# # all the supported formats # # all the supported formats
# end
# end # end
# end # end
# end
# #
# The above will set <tt>Cache-Control: public, no-cache</tt> in the response. # The above will set `Cache-Control: public, no-cache` in the response.
# #
# When rendering a different template than the controller/action's default # When rendering a different template than the controller/action's default
# template, you can indicate which digest to include in the ETag: # template, you can indicate which digest to include in the ETag:
# #
# def show # def show
# super if stale?(@article, template: "widgets/show") # super if stale?(@article, template: "widgets/show")
# end # end
# #
def stale?(object = nil, **freshness_kwargs) def stale?(object = nil, **freshness_kwargs)
fresh_when(object, **freshness_kwargs) fresh_when(object, **freshness_kwargs)
!request.fresh?(response) !request.fresh?(response)
end end
# Sets the +Cache-Control+ header, overwriting existing directives. This # Sets the `Cache-Control` header, overwriting existing directives. This method
# method will also ensure an HTTP +Date+ header for client compatibility. # will also ensure an HTTP `Date` header for client compatibility.
# #
# Defaults to issuing the +private+ directive, so that intermediate caches # Defaults to issuing the `private` directive, so that intermediate caches must
# must not cache the response. # not cache the response.
# #
# ==== Options # #### Options
# #
# [+:public+] # `:public`
# If true, replaces the default +private+ directive with the +public+ # : If true, replaces the default `private` directive with the `public`
# directive. # directive.
# [+:must_revalidate+] # `:must_revalidate`
# If true, adds the +must-revalidate+ directive. # : If true, adds the `must-revalidate` directive.
# [+:stale_while_revalidate+] # `:stale_while_revalidate`
# Sets the value of the +stale-while-revalidate+ directive. # : Sets the value of the `stale-while-revalidate` directive.
# [+:stale_if_error+] # `:stale_if_error`
# Sets the value of the +stale-if-error+ directive. # : Sets the value of the `stale-if-error` directive.
# #
# Any additional key-value pairs are concatenated as directives. For a list
# of supported +Cache-Control+ directives, see the {article on
# MDN}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control].
# #
# ==== Examples # Any additional key-value pairs are concatenated as directives. For a list of
# supported `Cache-Control` directives, see the [article on
# MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control).
# #
# expires_in 10.minutes # #### Examples
# # => Cache-Control: max-age=600, private
# #
# expires_in 10.minutes, public: true # expires_in 10.minutes
# # => Cache-Control: max-age=600, public # # => Cache-Control: max-age=600, private
# #
# expires_in 10.minutes, public: true, must_revalidate: true # expires_in 10.minutes, public: true
# # => Cache-Control: max-age=600, public, must-revalidate # # => Cache-Control: max-age=600, public
# #
# expires_in 1.hour, stale_while_revalidate: 60.seconds # expires_in 10.minutes, public: true, must_revalidate: true
# # => Cache-Control: max-age=3600, private, stale-while-revalidate=60 # # => Cache-Control: max-age=600, public, must-revalidate
# #
# expires_in 1.hour, stale_if_error: 5.minutes # expires_in 1.hour, stale_while_revalidate: 60.seconds
# # => Cache-Control: max-age=3600, private, stale-if-error=300 # # => Cache-Control: max-age=3600, private, stale-while-revalidate=60
# #
# expires_in 1.hour, public: true, "s-maxage": 3.hours, "no-transform": true # expires_in 1.hour, stale_if_error: 5.minutes
# # => Cache-Control: max-age=3600, public, s-maxage=10800, no-transform=true # # => Cache-Control: max-age=3600, private, stale-if-error=300
#
# expires_in 1.hour, public: true, "s-maxage": 3.hours, "no-transform": true
# # => Cache-Control: max-age=3600, public, s-maxage=10800, no-transform=true
# #
def expires_in(seconds, options = {}) def expires_in(seconds, options = {})
response.cache_control.delete(:no_store) response.cache_control.delete(:no_store)
@ -286,8 +291,8 @@ def expires_in(seconds, options = {})
response.date = Time.now unless response.date? response.date = Time.now unless response.date?
end end
# Sets an HTTP 1.1 +Cache-Control+ header of <tt>no-cache</tt>. This means the # Sets an HTTP 1.1 `Cache-Control` header of `no-cache`. This means the resource
# resource will be marked as stale, so clients must always revalidate. # will be marked as stale, so clients must always revalidate.
# Intermediate/browser caches may still store the asset. # Intermediate/browser caches may still store the asset.
def expires_now def expires_now
response.cache_control.replace(no_cache: true) response.cache_control.replace(no_cache: true)
@ -295,12 +300,13 @@ def expires_now
# Cache or yield the block. The cache is supposed to never expire. # 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, # You can use this method when you have an HTTP response that never changes, and
# and the browser and proxies should cache it indefinitely. # 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.
# #
# * +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) def http_cache_forever(public: false)
expires_in 100.years, public: public expires_in 100.years, public: public
@ -309,8 +315,8 @@ def http_cache_forever(public: false)
public: public) public: public)
end end
# Sets an HTTP 1.1 +Cache-Control+ header of <tt>no-store</tt>. This means the # Sets an HTTP 1.1 `Cache-Control` header of `no-store`. This means the resource
# resource may not be stored in any cache. # may not be stored in any cache.
def no_store def no_store
response.cache_control.replace(no_store: true) response.cache_control.replace(no_store: true)
end end

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
module ActionController # :nodoc: module ActionController # :nodoc:
module ContentSecurityPolicy module ContentSecurityPolicy
extend ActiveSupport::Concern extend ActiveSupport::Concern
@ -13,29 +15,28 @@ module ContentSecurityPolicy
end end
module ClassMethods module ClassMethods
# Overrides parts of the globally configured +Content-Security-Policy+ # Overrides parts of the globally configured `Content-Security-Policy` header:
# header:
# #
# class PostsController < ApplicationController # class PostsController < ApplicationController
# content_security_policy do |policy| # content_security_policy do |policy|
# policy.base_uri "https://www.example.com" # policy.base_uri "https://www.example.com"
# end
# end # end
# end
# #
# Options can be passed similar to +before_action+. For example, pass # Options can be passed similar to `before_action`. For example, pass `only:
# <tt>only: :index</tt> to override the header on the index action only: # :index` to override the header on the index action only:
# #
# class PostsController < ApplicationController # class PostsController < ApplicationController
# content_security_policy(only: :index) do |policy| # content_security_policy(only: :index) do |policy|
# policy.default_src :self, :https # policy.default_src :self, :https
# end
# end # end
# end
# #
# Pass +false+ to remove the +Content-Security-Policy+ header: # Pass `false` to remove the `Content-Security-Policy` header:
# #
# class PostsController < ApplicationController # class PostsController < ApplicationController
# content_security_policy false, only: :index # content_security_policy false, only: :index
# end # end
def content_security_policy(enabled = true, **options, &block) def content_security_policy(enabled = true, **options, &block)
before_action(options) do before_action(options) do
if block_given? if block_given?
@ -50,18 +51,18 @@ def content_security_policy(enabled = true, **options, &block)
end end
end end
# Overrides the globally configured +Content-Security-Policy-Report-Only+ # Overrides the globally configured `Content-Security-Policy-Report-Only`
# header: # header:
# #
# class PostsController < ApplicationController # class PostsController < ApplicationController
# content_security_policy_report_only only: :index # content_security_policy_report_only only: :index
# end # end
# #
# Pass +false+ to remove the +Content-Security-Policy-Report-Only+ header: # Pass `false` to remove the `Content-Security-Policy-Report-Only` header:
# #
# class PostsController < ApplicationController # class PostsController < ApplicationController
# content_security_policy_report_only false, only: :index # content_security_policy_report_only false, only: :index
# end # end
def content_security_policy_report_only(report_only = true, **options) def content_security_policy_report_only(report_only = true, **options)
before_action(options) do before_action(options) do
request.content_security_policy_report_only = report_only request.content_security_policy_report_only = report_only

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
module ActionController # :nodoc: module ActionController # :nodoc:
module Cookies module Cookies
extend ActiveSupport::Concern extend ActiveSupport::Concern
@ -9,8 +11,8 @@ module Cookies
end end
private private
# The cookies for the current request. See ActionDispatch::Cookies for # The cookies for the current request. See ActionDispatch::Cookies for more
# more information. # information.
def cookies # :doc: def cookies # :doc:
request.cookie_jar request.cookie_jar
end end

@ -1,10 +1,12 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
require "action_controller/metal/exceptions" require "action_controller/metal/exceptions"
require "action_dispatch/http/content_disposition" require "action_dispatch/http/content_disposition"
module ActionController # :nodoc: module ActionController # :nodoc:
# = Action Controller Data \Streaming # # Action Controller Data Streaming
# #
# Methods for sending arbitrary data and for streaming files to the browser, # Methods for sending arbitrary data and for streaming files to the browser,
# instead of rendering. # instead of rendering.
@ -17,57 +19,60 @@ module DataStreaming
DEFAULT_SEND_FILE_DISPOSITION = "attachment" # :nodoc: DEFAULT_SEND_FILE_DISPOSITION = "attachment" # :nodoc:
private private
# Sends the file. This uses a server-appropriate method (such as +X-Sendfile+) # 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 # via the `Rack::Sendfile` middleware. The header to use is set via
# +config.action_dispatch.x_sendfile_header+. # `config.action_dispatch.x_sendfile_header`. Your server can also configure
# Your server can also configure this for you by setting the +X-Sendfile-Type+ header. # this for you by setting the `X-Sendfile-Type` header.
# #
# Be careful to sanitize the path parameter if it is coming from a web # Be careful to sanitize the path parameter if it is coming from a web page.
# page. <tt>send_file(params[:path])</tt> allows a malicious user to # `send_file(params[:path])` allows a malicious user to download any file on
# download any file on your server. # your server.
# #
# Options: # Options:
# * <tt>:filename</tt> - suggests a filename for the browser to use. # * `:filename` - suggests a filename for the browser to use. Defaults to
# Defaults to <tt>File.basename(path)</tt>. # `File.basename(path)`.
# * <tt>:type</tt> - specifies an HTTP content type. # * `:type` - specifies an HTTP content type. You can specify either a string
# You can specify either a string or a symbol for a registered type with <tt>Mime::Type.register</tt>, for example +:json+. # or a symbol for a registered type with `Mime::Type.register`, for example
# If omitted, the type will be inferred from the file extension specified in <tt>:filename</tt>. # `:json`. If omitted, the type will be inferred from the file extension
# If no content type is registered for the extension, the default type +application/octet-stream+ will be used. # specified in `:filename`. If no content type is registered for the
# * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded. # extension, the default type `application/octet-stream` will be used.
# Valid values are <tt>"inline"</tt> and <tt>"attachment"</tt> (default). # * `:disposition` - specifies whether the file will be shown inline or
# * <tt>:status</tt> - specifies the status code to send with the response. Defaults to 200. # downloaded. Valid values are `"inline"` and `"attachment"` (default).
# * <tt>:url_based_filename</tt> - set to +true+ if you want the browser to guess the filename from # * `:status` - specifies the status code to send with the response. Defaults
# the URL, which is necessary for i18n filenames on certain browsers # to 200.
# (setting <tt>:filename</tt> overrides this option). # * `:url_based_filename` - 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 `:filename` overrides this option).
# #
# The default +Content-Type+ and +Content-Disposition+ headers are #
# set to download arbitrary binary files in as many browsers as # The default `Content-Type` and `Content-Disposition` headers are set to
# possible. IE versions 4, 5, 5.5, and 6 are all known to have # download arbitrary binary files in as many browsers as possible. IE versions
# a variety of quirks (especially when downloading over SSL). # 4, 5, 5.5, and 6 are all known to have a variety of quirks (especially when
# downloading over SSL).
# #
# Simple download: # Simple download:
# #
# send_file '/path/to.zip' # send_file '/path/to.zip'
# #
# Show a JPEG in the browser: # Show a JPEG in the browser:
# #
# send_file '/path/to.jpeg', type: 'image/jpeg', disposition: 'inline' # send_file '/path/to.jpeg', type: 'image/jpeg', disposition: 'inline'
# #
# Show a 404 page in the browser: # Show a 404 page in the browser:
# #
# send_file '/path/to/404.html', type: 'text/html; charset=utf-8', disposition: 'inline', status: 404 # send_file '/path/to/404.html', type: 'text/html; charset=utf-8', disposition: 'inline', status: 404
# #
# You can use other <tt>Content-*</tt> HTTP headers to provide additional # You can use other `Content-*` HTTP headers to provide additional information
# information to the client. See MDN for a # to the client. See MDN for a [list of HTTP
# {list of HTTP headers}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers]. # headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers).
# #
# Also be aware that the document may be cached by proxies and browsers. # Also be aware that the document may be cached by proxies and browsers. The
# The +Pragma+ and +Cache-Control+ headers declare how the file may be cached # `Pragma` and `Cache-Control` headers declare how the file may be cached by
# by intermediaries. They default to require clients to validate with # intermediaries. They default to require clients to validate with the server
# the server before releasing cached responses. See # before releasing cached responses. See https://www.mnot.net/cache_docs/ for an
# https://www.mnot.net/cache_docs/ for an overview of web caching and # overview of web caching and [RFC
# {RFC 9111}[https://www.rfc-editor.org/rfc/rfc9111.html#name-cache-control] # 9111](https://www.rfc-editor.org/rfc/rfc9111.html#name-cache-control) for the
# for the +Cache-Control+ header spec. # `Cache-Control` header spec.
def send_file(path, options = {}) # :doc: def send_file(path, options = {}) # :doc:
raise MissingFile, "Cannot read file #{path}" unless File.file?(path) && File.readable?(path) raise MissingFile, "Cannot read file #{path}" unless File.file?(path) && File.readable?(path)
@ -79,35 +84,39 @@ def send_file(path, options = {}) # :doc:
response.send_file path response.send_file path
end end
# Sends the given binary data to the browser. This method is similar to # Sends the given binary data to the browser. This method is similar to `render
# <tt>render plain: data</tt>, but also allows you to specify whether # plain: data`, but also allows you to specify whether the browser should
# the browser should display the response as a file attachment (i.e. in a # display the response as a file attachment (i.e. in a download dialog) or as
# download dialog) or as inline data. You may also set the content type, # inline data. You may also set the content type, the file name, and other
# the file name, and other things. # things.
# #
# Options: # Options:
# * <tt>:filename</tt> - suggests a filename for the browser to use. # * `:filename` - suggests a filename for the browser to use.
# * <tt>:type</tt> - specifies an HTTP content type. Defaults to +application/octet-stream+. # * `:type` - specifies an HTTP content type. Defaults to
# You can specify either a string or a symbol for a registered type with <tt>Mime::Type.register</tt>, for example +:json+. # `application/octet-stream`. You can specify either a string or a symbol
# If omitted, type will be inferred from the file extension specified in <tt>:filename</tt>. # for a registered type with `Mime::Type.register`, for example `:json`. If
# If no content type is registered for the extension, the default type +application/octet-stream+ will be used. # omitted, type will be inferred from the file extension specified in
# * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded. # `:filename`. If no content type is registered for the extension, the
# Valid values are <tt>"inline"</tt> and <tt>"attachment"</tt> (default). # default type `application/octet-stream` will be used.
# * <tt>:status</tt> - specifies the status code to send with the response. Defaults to 200. # * `:disposition` - specifies whether the file will be shown inline or
# downloaded. Valid values are `"inline"` and `"attachment"` (default).
# * `:status` - specifies the status code to send with the response. Defaults
# to 200.
#
# #
# Generic data download: # Generic data download:
# #
# send_data buffer # send_data buffer
# #
# Download a dynamically-generated tarball: # Download a dynamically-generated tarball:
# #
# send_data generate_tgz('dir'), filename: 'dir.tgz' # send_data generate_tgz('dir'), filename: 'dir.tgz'
# #
# Display an image Active Record in the browser: # Display an image Active Record in the browser:
# #
# send_data image.data, type: image.content_type, disposition: 'inline' # send_data image.data, type: image.content_type, disposition: 'inline'
# #
# See +send_file+ for more information on HTTP <tt>Content-*</tt> headers and caching. # See `send_file` for more information on HTTP `Content-*` headers and caching.
def send_data(data, options = {}) # :doc: def send_data(data, options = {}) # :doc:
send_file_headers! options send_file_headers! options
render options.slice(:status, :content_type).merge(body: data) render options.slice(:status, :content_type).merge(body: data)

@ -1,10 +1,12 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
module ActionController module ActionController
# = Action Controller Default Headers # # Action Controller Default Headers
# #
# Allows configuring default headers that will be automatically merged into # Allows configuring default headers that will be automatically merged into each
# each response. # response.
module DefaultHeaders module DefaultHeaders
extend ActiveSupport::Concern extend ActiveSupport::Concern

@ -1,7 +1,9 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
module ActionController module ActionController
# = Action Controller Etag With \Flash # # Action Controller Etag With Flash
# #
# When you're using the flash, it's generally used as a conditional on the view. # 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 # This means the content of the view depends on the flash. Which in turn means

@ -1,24 +1,26 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
module ActionController module ActionController
# = Action Controller Etag With Template \Digest # # Action Controller Etag With Template Digest
# #
# When our views change, they should bubble up into HTTP cache freshness # When our views change, they should bubble up into HTTP cache freshness and
# and bust browser caches. So the template digest for the current action # bust browser caches. So the template digest for the current action is
# is automatically included in the ETag. # automatically included in the ETag.
# #
# Enabled by default for apps that use Action View. Disable by setting # Enabled by default for apps that use Action View. Disable by setting
# #
# config.action_controller.etag_with_template_digest = false # config.action_controller.etag_with_template_digest = false
# #
# Override the template to digest by passing +:template+ to +fresh_when+ # Override the template to digest by passing `:template` to `fresh_when` and
# and +stale?+ calls. For example: # `stale?` calls. For example:
# #
# # We're going to render widgets/show, not posts/show # # We're going to render widgets/show, not posts/show
# fresh_when @post, template: 'widgets/show' # fresh_when @post, template: 'widgets/show'
# #
# # We're not going to render a template, so omit it from the ETag. # # We're not going to render a template, so omit it from the ETag.
# fresh_when @post, template: false # fresh_when @post, template: false
# #
module EtagWithTemplateDigest module EtagWithTemplateDigest
extend ActiveSupport::Concern extend ActiveSupport::Concern
@ -40,10 +42,10 @@ def determine_template_etag(options)
end end
end end
# Pick the template digest to include in the ETag. If the +:template+ option # Pick the template digest to include in the ETag. If the `:template` option is
# is present, use the named template. If +:template+ is +nil+ or absent, use # present, use the named template. If `:template` is `nil` or absent, use the
# the default controller/action template. If +:template+ is false, omit the # default controller/action template. If `:template` is false, omit the template
# template digest from the ETag. # digest from the ETag.
def pick_template_for_etag(options) def pick_template_for_etag(options)
unless options[:template] == false unless options[:template] == false
options[:template] || lookup_context.find_all(action_name, _prefixes).first&.virtual_path options[:template] || lookup_context.find_all(action_name, _prefixes).first&.virtual_path

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
module ActionController module ActionController
class ActionControllerError < StandardError # :nodoc: class ActionControllerError < StandardError # :nodoc:
end end
@ -73,16 +75,16 @@ class UnknownHttpMethod < ActionControllerError # :nodoc:
class UnknownFormat < ActionControllerError # :nodoc: class UnknownFormat < ActionControllerError # :nodoc:
end end
# Raised when a nested respond_to is triggered and the content types of each # Raised when a nested respond_to is triggered and the content types of each are
# are incompatible. For example: # incompatible. For example:
# #
# respond_to do |outer_type| # respond_to do |outer_type|
# outer_type.js do # outer_type.js do
# respond_to do |inner_type| # respond_to do |inner_type|
# inner_type.html { render body: "HTML" } # inner_type.html { render body: "HTML" }
# end # end
# end # end
# end # end
class RespondToMismatchError < ActionControllerError class RespondToMismatchError < ActionControllerError
DEFAULT_MESSAGE = "respond_to was called multiple times and matched with conflicting formats in this action. Please note that you may only call respond_to and match on a single format per action." DEFAULT_MESSAGE = "respond_to was called multiple times and matched with conflicting formats in this action. Please note that you may only call respond_to and match on a single format per action."

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
module ActionController # :nodoc: module ActionController # :nodoc:
module Flash module Flash
extend ActiveSupport::Concern extend ActiveSupport::Concern
@ -13,19 +15,19 @@ module Flash
module ClassMethods module ClassMethods
# Creates new flash types. You can pass as many types as you want to create # 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 # flash types other than the default `alert` and `notice` in your controllers
# your controllers and views. For instance: # and views. For instance:
# #
# # in application_controller.rb # # in application_controller.rb
# class ApplicationController < ActionController::Base # class ApplicationController < ActionController::Base
# add_flash_types :warning # add_flash_types :warning
# end # end
# #
# # in your controller # # in your controller
# redirect_to user_path(@user), warning: "Incomplete profile" # redirect_to user_path(@user), warning: "Incomplete profile"
# #
# # in your view # # in your view
# <%= warning %> # <%= warning %>
# #
# This method will automatically define a new method for each of the given # This method will automatically define a new method for each of the given
# names, and it will be available in your views. # names, and it will be available in your views.

@ -1,23 +1,25 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
module ActionController module ActionController
module Head module Head
# Returns a response that has no content (merely headers). The options # Returns a response that has no content (merely headers). The options argument
# argument is interpreted to be a hash of header names and values. # is interpreted to be a hash of header names and values. This allows you to
# This allows you to easily return a response that consists only of # easily return a response that consists only of significant headers:
# significant headers:
# #
# head :created, location: person_path(@person) # head :created, location: person_path(@person)
# #
# head :created, location: @person # head :created, location: @person
# #
# It can also be used to return exceptional conditions: # It can also be used to return exceptional conditions:
# #
# return head(:method_not_allowed) unless request.post? # return head(:method_not_allowed) unless request.post?
# return head(:bad_request) unless valid_request? # return head(:bad_request) unless valid_request?
# render # render
# #
# See +Rack::Utils::SYMBOL_TO_STATUS_CODE+ for a full list of valid +status+ symbols. # See `Rack::Utils::SYMBOL_TO_STATUS_CODE` for a full list of valid `status`
# symbols.
def head(status, options = nil) def head(status, options = nil)
if status.is_a?(Hash) if status.is_a?(Hash)
raise ArgumentError, "#{status.inspect} is not a valid value for `status`." raise ArgumentError, "#{status.inspect} is not a valid value for `status`."

@ -1,59 +1,64 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
module ActionController module ActionController
# = Action Controller \Helpers # # Action Controller Helpers
# #
# The \Rails framework provides a large number of helpers for working with assets, dates, forms, # The Rails framework provides a large number of helpers for working with
# numbers and model objects, to name a few. These helpers are available to all templates # assets, dates, forms, numbers and model objects, to name a few. These helpers
# by default. # are available to all templates by default.
# #
# In addition to using the standard template helpers provided, creating custom helpers to # In addition to using the standard template helpers provided, creating custom
# extract complicated logic or reusable functionality is strongly encouraged. By default, each controller # helpers to extract complicated logic or reusable functionality is strongly
# will include all helpers. These helpers are only accessible on the controller through <tt>#helpers</tt> # encouraged. By default, each controller will include all helpers. These
# helpers are only accessible on the controller through `#helpers`
# #
# In previous versions of \Rails the controller will include a helper which # 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 # matches the name of the controller, e.g., `MyController` will automatically
# include <tt>MyHelper</tt>. You can revert to the old behavior with the following: # include `MyHelper`. You can revert to the old behavior with the following:
# #
# # config/application.rb # # config/application.rb
# class Application < Rails::Application # class Application < Rails::Application
# config.action_controller.include_all_helpers = false # config.action_controller.include_all_helpers = false
# end
#
# 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_fs(format)
# end # end
# end
# #
# FormattedTimeHelper can now be included in a controller, using the +helper+ class method: # Additional helpers can be specified using the `helper` class method in
# ActionController::Base or any controller which inherits from it.
# #
# class EventsController < ActionController::Base # The `to_s` method from the Time class can be wrapped in a helper method to
# helper FormattedTimeHelper # display a custom message if a Time object is blank:
# def index #
# @events = Event.all # module FormattedTimeHelper
# def format_time(time, format=:long, blank_message="&nbsp;")
# time.blank? ? blank_message : time.to_fs(format)
# end
# end # end
# end
# #
# Then, in any view rendered by <tt>EventsController</tt>, the <tt>format_time</tt> method can be called: # FormattedTimeHelper can now be included in a controller, using the `helper`
# class method:
# #
# <% @events.each do |event| -%> # class EventsController < ActionController::Base
# <p> # helper FormattedTimeHelper
# <%= format_time(event.time, :short, "N/A") %> | <%= event.name %> # def index
# </p> # @events = Event.all
# <% end -%> # end
# end
# #
# Finally, assuming we have two event instances, one which has a time and one which does not, # Then, in any view rendered by `EventsController`, the `format_time` method can
# the output might look like this: # be called:
# #
# 23 Aug 11:30 | Carolina Railhawks Soccer Match # <% @events.each do |event| -%>
# N/A | Carolina Railhawks Training Workshop # <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 module Helpers
extend ActiveSupport::Concern extend ActiveSupport::Concern
@ -68,23 +73,24 @@ class << self; attr_accessor :helpers_path; end
module ClassMethods module ClassMethods
# Declares helper accessors for controller attributes. For example, the # Declares helper accessors for controller attributes. For example, the
# following adds new +name+ and <tt>name=</tt> instance methods to a # following adds new `name` and `name=` instance methods to a controller and
# controller and makes them available to the view: # makes them available to the view:
# attr_accessor :name # attr_accessor :name
# helper_attr :name # helper_attr :name
#
# #### Parameters
# * `attrs` - Names of attributes to be converted into helpers.
# #
# ==== Parameters
# * <tt>attrs</tt> - Names of attributes to be converted into helpers.
def helper_attr(*attrs) def helper_attr(*attrs)
attrs.flatten.each { |attr| helper_method(attr, "#{attr}=") } attrs.flatten.each { |attr| helper_method(attr, "#{attr}=") }
end end
# Provides a proxy to access helper methods from outside the view. # Provides a proxy to access helper methods from outside the view.
# #
# Note that the proxy is rendered under a different view context. # Note that the proxy is rendered under a different view context. This may cause
# This may cause incorrect behavior with capture methods. Consider # incorrect behavior with capture methods. Consider using
# using {helper}[rdoc-ref:AbstractController::Helpers::ClassMethods#helper] # [helper](rdoc-ref:AbstractController::Helpers::ClassMethods#helper) instead
# instead when using +capture+. # when using `capture`.
def helpers def helpers
@helper_proxy ||= begin @helper_proxy ||= begin
proxy = ActionView::Base.empty proxy = ActionView::Base.empty
@ -93,21 +99,23 @@ def helpers
end end
end end
# Override modules_for_helpers to accept +:all+ as argument, which loads # Override modules_for_helpers to accept `:all` as argument, which loads all
# all helpers in helpers_path. # helpers in helpers_path.
# #
# ==== Parameters # #### Parameters
# * <tt>args</tt> - A list of helpers # * `args` - A list of helpers
#
#
# #### Returns
# * `array` - A normalized list of modules for the list of helpers provided.
# #
# ==== Returns
# * <tt>array</tt> - A normalized list of modules for the list of helpers provided.
def modules_for_helpers(args) def modules_for_helpers(args)
args += all_application_helpers if args.delete(:all) args += all_application_helpers if args.delete(:all)
super(args) super(args)
end end
private private
# Extract helper names from files in <tt>app/helpers/**/*_helper.rb</tt> # Extract helper names from files in `app/helpers/***/**_helper.rb`
def all_application_helpers def all_application_helpers
all_helpers_from_path(helpers_path) all_helpers_from_path(helpers_path)
end end

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
require "base64" require "base64"
require "active_support/security_utils" require "active_support/security_utils"
require "active_support/core_ext/array/access" require "active_support/core_ext/array/access"
@ -7,62 +9,63 @@
module ActionController module ActionController
# HTTP Basic, Digest, and Token authentication. # HTTP Basic, Digest, and Token authentication.
module HttpAuthentication module HttpAuthentication
# = HTTP \Basic authentication # # HTTP Basic authentication
# #
# === Simple \Basic example # ### Simple Basic example
# #
# class PostsController < ApplicationController # class PostsController < ApplicationController
# http_basic_authenticate_with name: "dhh", password: "secret", except: :index # http_basic_authenticate_with name: "dhh", password: "secret", except: :index
# #
# def index # def index
# render plain: "Everyone can see me!" # 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 are 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 # end
# #
# def authenticate # def edit
# case request.format # render plain: "I'm only accessible if you know the password"
# when Mime[:xml], Mime[:atom] # end
# if user = authenticate_with_http_basic { |u, p| @account.users.authenticate(u, p) } # end
# @current_user = user #
# ### Advanced Basic example
#
# Here is a more advanced Basic example where only Atom feeds and the XML API
# are 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 # else
# request_http_basic_authentication # if session_authenticated?
# end # @current_user = @account.users.find(session[:authenticated][:user_id])
# else # else
# if session_authenticated? # redirect_to(login_url) and return false
# @current_user = @account.users.find(session[:authenticated][:user_id]) # end
# else
# redirect_to(login_url) and return false
# end # end
# end # end
# end # end
# end
# #
# In your integration tests, you can do something like this: # In your integration tests, you can do something like this:
# #
# def test_access_granted_from_xml # def test_access_granted_from_xml
# authorization = ActionController::HttpAuthentication::Basic.encode_credentials(users(:dhh).name, users(:dhh).password) # authorization = ActionController::HttpAuthentication::Basic.encode_credentials(users(:dhh).name, users(:dhh).password)
# #
# get "/notes/1.xml", headers: { 'HTTP_AUTHORIZATION' => authorization } # get "/notes/1.xml", headers: { 'HTTP_AUTHORIZATION' => authorization }
# #
# assert_equal 200, status # assert_equal 200, status
# end # end
module Basic module Basic
extend self extend self
@ -70,7 +73,7 @@ module ControllerMethods
extend ActiveSupport::Concern extend ActiveSupport::Concern
module ClassMethods module ClassMethods
# Enables HTTP \Basic authentication. # Enables HTTP Basic authentication.
# #
# See ActionController::HttpAuthentication::Basic for example usage. # See ActionController::HttpAuthentication::Basic for example usage.
def http_basic_authenticate_with(name:, password:, realm: nil, **options) def http_basic_authenticate_with(name:, password:, realm: nil, **options)
@ -82,8 +85,8 @@ def http_basic_authenticate_with(name:, password:, realm: nil, **options)
def http_basic_authenticate_or_request_with(name:, password:, realm: nil, message: nil) def http_basic_authenticate_or_request_with(name:, password:, realm: nil, message: nil)
authenticate_or_request_with_http_basic(realm, message) do |given_name, given_password| authenticate_or_request_with_http_basic(realm, message) do |given_name, given_password|
# This comparison uses & so that it doesn't short circuit and # This comparison uses & so that it doesn't short circuit and uses
# uses `secure_compare` so that length information isn't leaked. # `secure_compare` so that length information isn't leaked.
ActiveSupport::SecurityUtils.secure_compare(given_name.to_s, name) & ActiveSupport::SecurityUtils.secure_compare(given_name.to_s, name) &
ActiveSupport::SecurityUtils.secure_compare(given_password.to_s, password) ActiveSupport::SecurityUtils.secure_compare(given_password.to_s, password)
end end
@ -140,67 +143,68 @@ def authentication_request(controller, realm, message)
end end
end end
# = HTTP \Digest authentication # # HTTP Digest authentication
# #
# === Simple \Digest example # ### Simple Digest example
# #
# require "openssl" # require "openssl"
# class PostsController < ApplicationController # class PostsController < ApplicationController
# REALM = "SuperSecret" # REALM = "SuperSecret"
# USERS = {"dhh" => "secret", #plain text password # USERS = {"dhh" => "secret", #plain text password
# "dap" => OpenSSL::Digest::MD5.hexdigest(["dap",REALM,"secret"].join(":"))} #ha1 digest password # "dap" => OpenSSL::Digest::MD5.hexdigest(["dap",REALM,"secret"].join(":"))} #ha1 digest password
# #
# before_action :authenticate, except: [:index] # before_action :authenticate, except: [:index]
# #
# def index # def index
# render plain: "Everyone can see me!" # 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
# end
# #
# === Notes # def edit
# render plain: "I'm only accessible if you know the password"
# end
# #
# The +authenticate_or_request_with_http_digest+ block must return the user's password # private
# or the ha1 digest hash so the framework can appropriately hash to check the user's # def authenticate
# credentials. Returning +nil+ will cause authentication to fail. # authenticate_or_request_with_http_digest(REALM) do |username|
# USERS[username]
# end
# end
# end
# #
# Storing the ha1 hash: MD5(username:realm:password), is better than storing a plain password. If # ### Notes
# 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 # The `authenticate_or_request_with_http_digest` block must return the user's
# they reach your application. You can debug this situation by logging all environment # password or the ha1 digest hash so the framework can appropriately hash to
# variables, and check for HTTP_AUTHORIZATION, amongst others. # 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 module Digest
extend self extend self
module ControllerMethods module ControllerMethods
# Authenticate using an HTTP \Digest, or otherwise render an HTTP header # Authenticate using an HTTP Digest, or otherwise render an HTTP header
# requesting the client to send a \Digest. # requesting the client to send a Digest.
# #
# See ActionController::HttpAuthentication::Digest for example usage. # See ActionController::HttpAuthentication::Digest for example usage.
def authenticate_or_request_with_http_digest(realm = "Application", message = nil, &password_procedure) 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) authenticate_with_http_digest(realm, &password_procedure) || request_http_digest_authentication(realm, message)
end end
# Authenticate using an HTTP \Digest. Returns true if authentication is # Authenticate using an HTTP Digest. Returns true if authentication is
# successful, false otherwise. # successful, false otherwise.
def authenticate_with_http_digest(realm = "Application", &password_procedure) def authenticate_with_http_digest(realm = "Application", &password_procedure)
HttpAuthentication::Digest.authenticate(request, realm, &password_procedure) HttpAuthentication::Digest.authenticate(request, realm, &password_procedure)
end end
# Render an HTTP header requesting the client to send a \Digest for # Render an HTTP header requesting the client to send a Digest for
# authentication. # authentication.
def request_http_digest_authentication(realm = "Application", message = nil) def request_http_digest_authentication(realm = "Application", message = nil)
HttpAuthentication::Digest.authentication_request(self, realm, message) HttpAuthentication::Digest.authentication_request(self, realm, message)
@ -212,9 +216,9 @@ def authenticate(request, realm, &password_procedure)
request.authorization && validate_digest_response(request, realm, &password_procedure) request.authorization && validate_digest_response(request, realm, &password_procedure)
end end
# Returns false unless the request credentials response value matches the expected value. # Returns false unless the request credentials response value matches the
# First try the password as a ha1 digest password. If this fails, then try it as a plain # expected value. First try the password as a ha1 digest password. If this
# text password. # fails, then try it as a plain text password.
def validate_digest_response(request, realm, &password_procedure) def validate_digest_response(request, realm, &password_procedure)
secret_key = secret_token(request) secret_key = secret_token(request)
credentials = decode_credentials_header(request) credentials = decode_credentials_header(request)
@ -237,9 +241,10 @@ def validate_digest_response(request, realm, &password_procedure)
end end
end end
# Returns the expected response for a request of +http_method+ to +uri+ with the decoded +credentials+ and the expected +password+ # Returns the expected response for a request of `http_method` to `uri` with the
# Optional parameter +password_is_ha1+ is set to +true+ by default, since best practice is to store ha1 digest instead # decoded `credentials` and the expected `password` Optional parameter
# of a plain-text password. # `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) def expected_response(http_method, uri, credentials, password, password_is_ha1 = true)
ha1 = password_is_ha1 ? password : ha1(credentials, password) ha1 = password_is_ha1 ? password : ha1(credentials, password)
ha2 = OpenSSL::Digest::MD5.hexdigest([http_method.to_s.upcase, uri].join(":")) ha2 = OpenSSL::Digest::MD5.hexdigest([http_method.to_s.upcase, uri].join(":"))
@ -288,36 +293,40 @@ def secret_token(request)
# Uses an MD5 digest based on time to generate a value to be used only once. # 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. # A server-specified data string which should be uniquely generated each time a
# It is recommended that this string be base64 or hexadecimal data. # 401 response is made. It is recommended that this string be base64 or
# Specifically, since the string is passed in the header lines as a quoted string, the double-quote character is not allowed. # 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 contents of the nonce are implementation dependent. The quality of the
# The quality of the implementation depends on a good choice. # implementation depends on a good choice. A nonce might, for example, be
# A nonce might, for example, be constructed as the base 64 encoding of # constructed as the base 64 encoding of
# #
# time-stamp H(time-stamp ":" ETag ":" private-key) # time-stamp H(time-stamp ":" ETag ":" private-key)
# #
# where time-stamp is a server-generated time or other non-repeating value, # where time-stamp is a server-generated time or other non-repeating value, ETag
# ETag is the value of the HTTP ETag header associated with the requested entity, # is the value of the HTTP ETag header associated with the requested entity, and
# and private-key is data known only to the server. # private-key is data known only to the server. With a nonce of this form a
# With a nonce of this form a server would recalculate the hash portion after receiving the client authentication header and # server would recalculate the hash portion after receiving the client
# reject the request if it did not match the nonce from that header or # authentication header and reject the request if it did not match the nonce
# if the time-stamp value is not recent enough. In this way the server can limit the time of the nonce's validity. # from that header or if the time-stamp value is not recent enough. In this way
# The inclusion of the ETag prevents a replay request for an updated version of the resource. # the server can limit the time of the nonce's validity. The inclusion of the
# (Note: including the IP address of the client in the nonce would appear to offer the server the ability # ETag prevents a replay request for an updated version of the resource. (Note:
# to limit the reuse of the nonce to the same client that originally got it. # including the IP address of the client in the nonce would appear to offer the
# However, that would break proxy farms, where requests from a single user often go through different proxies in the farm. # server the ability to limit the reuse of the nonce to the same client that
# Also, IP address spoofing is not that hard.) # 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 # An implementation might choose not to accept a previously used nonce or a
# protect against a replay attack. Or, an implementation might choose to use one-time nonces or digests for # previously used digest, in order to protect against a replay attack. Or, an
# POST, PUT, or PATCH requests, and a time-stamp for GET requests. For more details on the issues involved see Section 4 # implementation might choose to use one-time nonces or digests for POST, PUT,
# of this document. # 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 # The nonce is opaque to the client. Composed of Time, and hash of Time with
# key from the \Rails session secret generated upon creation of project. Ensures # secret key from the Rails session secret generated upon creation of project.
# the time cannot be modified by client. # Ensures the time cannot be modified by client.
def nonce(secret_key, time = Time.now) def nonce(secret_key, time = Time.now)
t = time.to_i t = time.to_i
hashed = [t, secret_key] hashed = [t, secret_key]
@ -325,11 +334,10 @@ def nonce(secret_key, time = Time.now)
::Base64.strict_encode64("#{t}:#{digest}") ::Base64.strict_encode64("#{t}:#{digest}")
end end
# Might want a shorter timeout depending on whether the request # Might want a shorter timeout depending on whether the request is a PATCH, PUT,
# is a PATCH, PUT, or POST, and if the client is a browser or web service. # or POST, and if the client is a browser or web service. Can be much shorter if
# Can be much shorter if the Stale directive is implemented. This would # the Stale directive is implemented. This would allow a user to use new nonce
# allow a user to use new nonce without prompting the user again for their # without prompting the user again for their username and password.
# username and password.
def validate_nonce(secret_key, request, value, seconds_to_timeout = 5 * 60) def validate_nonce(secret_key, request, value, seconds_to_timeout = 5 * 60)
return false if value.nil? return false if value.nil?
t = ::Base64.decode64(value).split(":").first.to_i t = ::Base64.decode64(value).split(":").first.to_i
@ -342,80 +350,78 @@ def opaque(secret_key)
end end
end end
# = HTTP \Token authentication # # HTTP Token authentication
# #
# === Simple \Token example # ### Simple Token example
# #
# class PostsController < ApplicationController # class PostsController < ApplicationController
# TOKEN = "secret" # TOKEN = "secret"
# #
# before_action :authenticate, except: [ :index ] # before_action :authenticate, except: [ :index ]
# #
# def index # def index
# render plain: "Everyone can see me!" # 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(token, TOKEN)
# end
# end
# end
#
#
# Here is a more advanced Token example where only Atom feeds and the XML API are 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 # end
# #
# def authenticate # def edit
# case request.format # render plain: "I'm only accessible if you know the password"
# when Mime[:xml], Mime[:atom] # end
# if user = authenticate_with_http_token { |t, o| @account.users.authenticate(t, o) } #
# @current_user = user # private
# else # def authenticate
# request_http_token_authentication # authenticate_or_request_with_http_token do |token, options|
# end # # Compare the tokens in a time-constant manner, to mitigate
# else # # timing attacks.
# if session_authenticated? # ActiveSupport::SecurityUtils.secure_compare(token, TOKEN)
# @current_user = @account.users.find(session[:authenticated][:user_id])
# else
# redirect_to(login_url) and return false
# end # end
# end # end
# end # end
# end
# #
# Here is a more advanced Token example where only Atom feeds and the XML API
# are 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: # In your integration tests, you can do something like this:
# #
# def test_access_granted_from_xml # def test_access_granted_from_xml
# authorization = ActionController::HttpAuthentication::Token.encode_credentials(users(:dhh).token) # authorization = ActionController::HttpAuthentication::Token.encode_credentials(users(:dhh).token)
# #
# get "/notes/1.xml", headers: { 'HTTP_AUTHORIZATION' => authorization } # get "/notes/1.xml", headers: { 'HTTP_AUTHORIZATION' => authorization }
# #
# assert_equal 200, status # assert_equal 200, status
# end # end
# #
# # On shared hosts, Apache sometimes doesn't pass authentication headers to FCGI
# On shared hosts, Apache sometimes doesn't pass authentication headers to # instances. If your environment matches this description and you cannot
# FCGI instances. If your environment matches this description and you cannot
# authenticate, try this rule in your Apache setup: # authenticate, try this rule in your Apache setup:
# #
# RewriteRule ^(.*)$ dispatch.fcgi [E=X-HTTP_AUTHORIZATION:%{HTTP:Authorization},QSA,L] # RewriteRule ^(.*)$ dispatch.fcgi [E=X-HTTP_AUTHORIZATION:%{HTTP:Authorization},QSA,L]
module Token module Token
TOKEN_KEY = "token=" TOKEN_KEY = "token="
TOKEN_REGEX = /^(Token|Bearer)\s+/ TOKEN_REGEX = /^(Token|Bearer)\s+/
@ -423,19 +429,18 @@ module Token
extend self extend self
module ControllerMethods module ControllerMethods
# Authenticate using an HTTP Bearer token, or otherwise render an HTTP # Authenticate using an HTTP Bearer token, or otherwise render an HTTP header
# header requesting the client to send a Bearer token. For the authentication # requesting the client to send a Bearer token. For the authentication to be
# to be considered successful, +login_procedure+ should return a non-nil # considered successful, `login_procedure` should return a non-nil value.
# value. Typically, the authenticated user is returned. # Typically, the authenticated user is returned.
# #
# See ActionController::HttpAuthentication::Token for example usage. # See ActionController::HttpAuthentication::Token for example usage.
def authenticate_or_request_with_http_token(realm = "Application", message = nil, &login_procedure) 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) authenticate_with_http_token(&login_procedure) || request_http_token_authentication(realm, message)
end end
# Authenticate using an HTTP Bearer token. # Authenticate using an HTTP Bearer token. Returns the return value of
# Returns the return value of +login_procedure+ if a # `login_procedure` if a token is found. Returns `nil` if no token is found.
# token is found. Returns +nil+ if no token is found.
# #
# See ActionController::HttpAuthentication::Token for example usage. # See ActionController::HttpAuthentication::Token for example usage.
def authenticate_with_http_token(&login_procedure) def authenticate_with_http_token(&login_procedure)
@ -449,19 +454,20 @@ def request_http_token_authentication(realm = "Application", message = nil)
end end
end end
# If token Authorization header is present, call the login # If token Authorization header is present, call the login procedure with the
# procedure with the present token and options. # present token and options.
# #
# Returns the return value of +login_procedure+ if a # Returns the return value of `login_procedure` if a token is found. Returns
# token is found. Returns +nil+ if no token is found. # `nil` if no token is found.
# #
# ==== Parameters # #### Parameters
# #
# * +controller+ - ActionController::Base instance for the current request. # * `controller` - ActionController::Base instance for the current request.
# * +login_procedure+ - Proc to call if a token is present. The Proc # * `login_procedure` - Proc to call if a token is present. The Proc should
# should take two arguments: # take two arguments:
#
# authenticate(controller) { |token, options| ... }
# #
# authenticate(controller) { |token, options| ... }
# #
def authenticate(controller, &login_procedure) def authenticate(controller, &login_procedure)
token, options = token_and_options(controller.request) token, options = token_and_options(controller.request)
@ -470,21 +476,21 @@ def authenticate(controller, &login_procedure)
end end
end end
# Parses the token and options out of the token Authorization header. # Parses the token and options out of the token Authorization header. The value
# The value for the Authorization header is expected to have the prefix # for the Authorization header is expected to have the prefix `"Token"` or
# <tt>"Token"</tt> or <tt>"Bearer"</tt>. If the header looks like this: # `"Bearer"`. If the header looks like this:
# #
# Authorization: Token token="abc", nonce="def" # Authorization: Token token="abc", nonce="def"
# #
# Then the returned token is <tt>"abc"</tt>, and the options are # Then the returned token is `"abc"`, and the options are `{nonce: "def"}`.
# <tt>{nonce: "def"}</tt>.
# #
# Returns an +Array+ of <tt>[String, Hash]</tt> if a token is present. # Returns an `Array` of `[String, Hash]` if a token is present. Returns `nil` if
# Returns +nil+ if no token is found. # no token is found.
# #
# ==== Parameters # #### Parameters
#
# * `request` - ActionDispatch::Request instance with the current headers.
# #
# * +request+ - ActionDispatch::Request instance with the current headers.
def token_and_options(request) def token_and_options(request)
authorization_request = request.authorization.to_s authorization_request = request.authorization.to_s
if authorization_request[TOKEN_REGEX] if authorization_request[TOKEN_REGEX]
@ -497,12 +503,12 @@ def token_params_from(auth)
rewrite_param_values params_array_from raw_params auth rewrite_param_values params_array_from raw_params auth
end end
# Takes +raw_params+ and turns it into an array of parameters. # Takes `raw_params` and turns it into an array of parameters.
def params_array_from(raw_params) def params_array_from(raw_params)
raw_params.map { |param| param.split %r/=(.+)?/ } raw_params.map { |param| param.split %r/=(.+)?/ }
end end
# This removes the <tt>"</tt> characters wrapping the value. # This removes the `"` characters wrapping the value.
def rewrite_param_values(array_params) def rewrite_param_values(array_params)
array_params.each { |param| (param[1] || +"").gsub! %r/^"|"$/, "" } array_params.each { |param| (param[1] || +"").gsub! %r/^"|"$/, "" }
end end
@ -510,9 +516,9 @@ def rewrite_param_values(array_params)
WHITESPACED_AUTHN_PAIR_DELIMITERS = /\s*#{AUTHN_PAIR_DELIMITERS}\s*/ WHITESPACED_AUTHN_PAIR_DELIMITERS = /\s*#{AUTHN_PAIR_DELIMITERS}\s*/
private_constant :WHITESPACED_AUTHN_PAIR_DELIMITERS private_constant :WHITESPACED_AUTHN_PAIR_DELIMITERS
# This method takes an authorization body and splits up the key-value # This method takes an authorization body and splits up the key-value pairs by
# pairs by the standardized <tt>:</tt>, <tt>;</tt>, or <tt>\t</tt> # the standardized `:`, `;`, or `\t` delimiters defined in
# delimiters defined in +AUTHN_PAIR_DELIMITERS+. # `AUTHN_PAIR_DELIMITERS`.
def raw_params(auth) def raw_params(auth)
_raw_params = auth.sub(TOKEN_REGEX, "").split(WHITESPACED_AUTHN_PAIR_DELIMITERS) _raw_params = auth.sub(TOKEN_REGEX, "").split(WHITESPACED_AUTHN_PAIR_DELIMITERS)
_raw_params.reject!(&:empty?) _raw_params.reject!(&:empty?)
@ -528,10 +534,11 @@ def raw_params(auth)
# #
# Returns String. # Returns String.
# #
# ==== Parameters # #### Parameters
#
# * `token` - String token.
# * `options` - Optional Hash of the options.
# #
# * +token+ - String token.
# * +options+ - Optional Hash of the options.
def encode_credentials(token, options = {}) def encode_credentials(token, options = {})
values = ["#{TOKEN_KEY}#{token.to_s.inspect}"] + options.map do |key, value| values = ["#{TOKEN_KEY}#{token.to_s.inspect}"] + options.map do |key, value|
"#{key}=#{value.to_s.inspect}" "#{key}=#{value.to_s.inspect}"
@ -543,10 +550,11 @@ def encode_credentials(token, options = {})
# #
# Returns nothing. # Returns nothing.
# #
# ==== Parameters # #### Parameters
#
# * `controller` - ActionController::Base instance for the outgoing response.
# * `realm` - String realm to use in the header.
# #
# * +controller+ - ActionController::Base instance for the outgoing response.
# * +realm+ - String realm to use in the header.
def authentication_request(controller, realm, message = nil) def authentication_request(controller, realm, message = nil)
message ||= "HTTP Token: Access denied.\n" message ||= "HTTP Token: Access denied.\n"
controller.headers["WWW-Authenticate"] = %(Token realm="#{realm.tr('"', "")}") controller.headers["WWW-Authenticate"] = %(Token realm="#{realm.tr('"', "")}")

@ -1,33 +1,35 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
module ActionController module ActionController
# = Action Controller Implicit Render # # Action Controller Implicit Render
# #
# Handles implicit rendering for a controller action that does not # Handles implicit rendering for a controller action that does not explicitly
# explicitly respond with +render+, +respond_to+, +redirect+, or +head+. # respond with `render`, `respond_to`, `redirect`, or `head`.
# #
# For API controllers, the implicit response is always <tt>204 No Content</tt>. # For API controllers, the implicit response is always `204 No Content`.
# #
# For all other controllers, we use these heuristics to decide whether to # For all other controllers, we use these heuristics to decide whether to render
# render a template, raise an error for a missing template, or respond with # a template, raise an error for a missing template, or respond with `204 No
# <tt>204 No Content</tt>: # Content`:
# #
# First, if we DO find a template, it's rendered. Template lookup accounts # First, if we DO find a template, it's rendered. Template lookup accounts for
# for the action name, locales, format, variant, template handlers, and more # the action name, locales, format, variant, template handlers, and more (see
# (see +render+ for details). # `render` for details).
# #
# Second, if we DON'T find a template but the controller action does have # 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 # templates for other formats, variants, etc., then we trust that you meant to
# to provide a template for this response, too, and we raise # provide a template for this response, too, and we raise
# ActionController::UnknownFormat with an explanation. # ActionController::UnknownFormat with an explanation.
# #
# Third, if we DON'T find a template AND the request is a page load in a web # 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 # browser (technically, a non-XHR GET request for an HTML response) where you
# you reasonably expect to have rendered a template, then we raise # reasonably expect to have rendered a template, then we raise
# ActionController::MissingExactTemplate with an explanation. # ActionController::MissingExactTemplate with an explanation.
# #
# Finally, if we DON'T find a template AND the request isn't a browser page # 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>. # load, then we implicitly respond with `204 No Content`.
module ImplicitRender module ImplicitRender
# :stopdoc: # :stopdoc:
include BasicImplicitRender include BasicImplicitRender

@ -1,14 +1,17 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
require "benchmark" require "benchmark"
require "abstract_controller/logger" require "abstract_controller/logger"
module ActionController module ActionController
# = Action Controller \Instrumentation # # Action Controller Instrumentation
# #
# Adds instrumentation to several ends in ActionController::Base. It also provides # Adds instrumentation to several ends in ActionController::Base. It also
# some hooks related with process_action. This allows an ORM like Active Record # provides some hooks related with process_action. This allows an ORM like
# and/or DataMapper to plug in ActionController and show related information. # Active Record and/or DataMapper to plug in ActionController and show related
# information.
# #
# Check ActiveRecord::Railties::ControllerRuntime for an example. # Check ActiveRecord::Railties::ControllerRuntime for an example.
module Instrumentation module Instrumentation
@ -91,23 +94,23 @@ def halted_callback_hook(filter, _)
# A hook which allows you to clean up any time, wrongly taken into account in # A hook which allows you to clean up any time, wrongly taken into account in
# views, like database querying time. # views, like database querying time.
# #
# def cleanup_view_runtime # def cleanup_view_runtime
# super - time_taken_in_something_expensive # super - time_taken_in_something_expensive
# end # end
def cleanup_view_runtime # :doc: def cleanup_view_runtime # :doc:
yield yield
end end
# Every time after an action is processed, this method is invoked # Every time after an action is processed, this method is invoked with the
# with the payload, so you can add more information. # payload, so you can add more information.
def append_info_to_payload(payload) # :doc: def append_info_to_payload(payload) # :doc:
payload[:view_runtime] = view_runtime payload[:view_runtime] = view_runtime
end end
module ClassMethods module ClassMethods
# A hook which allows other frameworks to log what happened during # A hook which allows other frameworks to log what happened during controller
# controller process action. This method should return an array # process action. This method should return an array with the messages to be
# with the messages to be added. # added.
def log_process_action(payload) # :nodoc: def log_process_action(payload) # :nodoc:
messages, view_runtime = [], payload[:view_runtime] messages, view_runtime = [], payload[:view_runtime]
messages << ("Views: %.1fms" % view_runtime.to_f) if view_runtime messages << ("Views: %.1fms" % view_runtime.to_f) if view_runtime

@ -1,56 +1,58 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
require "action_dispatch/http/response" require "action_dispatch/http/response"
require "delegate" require "delegate"
require "active_support/json" require "active_support/json"
module ActionController module ActionController
# = Action Controller \Live # # Action Controller Live
# #
# Mix this module into your controller, and all actions in that controller # Mix this module into your controller, and all actions in that controller will
# will be able to stream data to the client as it's written. # be able to stream data to the client as it's written.
# #
# class MyController < ActionController::Base # class MyController < ActionController::Base
# include ActionController::Live # include ActionController::Live
# #
# def stream # def stream
# response.headers['Content-Type'] = 'text/event-stream' # response.headers['Content-Type'] = 'text/event-stream'
# 100.times { # 100.times {
# response.stream.write "hello world\n" # response.stream.write "hello world\n"
# sleep 1 # sleep 1
# } # }
# ensure # ensure
# response.stream.close # response.stream.close
# end
# end # end
# end
# #
# There are a few caveats with this module. You *cannot* write headers after the # There are a few caveats with this module. You **cannot** write headers after
# response has been committed (Response#committed? will return truthy). # the response has been committed (Response#committed? will return truthy).
# Calling +write+ or +close+ on the response stream will cause the response # 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 # object to be committed. Make sure all headers are set before calling write or
# or close on your stream. # close on your stream.
# #
# You *must* call close on your stream when you're finished, otherwise the # You **must** call close on your stream when you're finished, otherwise the
# socket may be left open forever. # socket may be left open forever.
# #
# The final caveat is that your actions are executed in a separate thread than # 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 # the main thread. Make sure your actions are thread safe, and this shouldn't be
# be a problem (don't share state across threads, etc). # a problem (don't share state across threads, etc).
# #
# Note that \Rails includes +Rack::ETag+ by default, which will buffer your # Note that Rails includes `Rack::ETag` by default, which will buffer your
# response. As a result, streaming responses may not work properly with Rack # response. As a result, streaming responses may not work properly with Rack
# 2.2.x, and you may need to implement workarounds in your application. # 2.2.x, and you may need to implement workarounds in your application. You can
# You can either set the +ETag+ or +Last-Modified+ response headers or remove # either set the `ETag` or `Last-Modified` response headers or remove
# +Rack::ETag+ from the middleware stack to address this issue. # `Rack::ETag` from the middleware stack to address this issue.
# #
# Here's an example of how you can set the +Last-Modified+ header if your Rack # Here's an example of how you can set the `Last-Modified` header if your Rack
# version is 2.2.x: # version is 2.2.x:
# #
# def stream # def stream
# response.headers["Content-Type"] = "text/event-stream" # response.headers["Content-Type"] = "text/event-stream"
# response.headers["Last-Modified"] = Time.now.httpdate # Add this line if your Rack version is 2.2.x # response.headers["Last-Modified"] = Time.now.httpdate # Add this line if your Rack version is 2.2.x
# ... # ...
# end # end
module Live module Live
extend ActiveSupport::Concern extend ActiveSupport::Concern
@ -66,44 +68,44 @@ def make_response!(request)
end end
end end
# = Action Controller \Live Server Sent Events # # Action Controller Live Server Sent Events
# #
# This class provides the ability to write an SSE (Server Sent Event) # This class provides the ability to write an SSE (Server Sent Event) to an IO
# to an IO stream. The class is initialized with a stream and can be used # stream. The class is initialized with a stream and can be used to either write
# to either write a JSON string or an object which can be converted to JSON. # a JSON string or an object which can be converted to JSON.
# #
# Writing an object will convert it into standard SSE format with whatever # Writing an object will convert it into standard SSE format with whatever
# options you have configured. You may choose to set the following options: # 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 # 1) Event. If specified, an event with this name will be dispatched on
# the browser. # the browser.
# 2) Retry. The reconnection time in milliseconds used when attempting # 2) Retry. The reconnection time in milliseconds used when attempting
# to send the event. # to send the event.
# 3) Id. If the connection dies while sending an SSE to the browser, then # 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+. # 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 # After setting an option in the constructor of the SSE object, all future SSEs
# SSEs sent across the stream will use those options unless overridden. # sent across the stream will use those options unless overridden.
# #
# Example Usage: # Example Usage:
# #
# class MyController < ActionController::Base # class MyController < ActionController::Base
# include ActionController::Live # include ActionController::Live
# #
# def index # def index
# response.headers['Content-Type'] = 'text/event-stream' # response.headers['Content-Type'] = 'text/event-stream'
# sse = SSE.new(response.stream, retry: 300, event: "event-name") # sse = SSE.new(response.stream, retry: 300, event: "event-name")
# sse.write({ name: 'John'}) # sse.write({ name: 'John'})
# sse.write({ name: 'John'}, id: 10) # sse.write({ name: 'John'}, id: 10)
# sse.write({ name: 'John'}, id: 10, event: "other-event") # sse.write({ name: 'John'}, id: 10, event: "other-event")
# sse.write({ name: 'John'}, id: 10, event: "other-event", retry: 500) # sse.write({ name: 'John'}, id: 10, event: "other-event", retry: 500)
# ensure # ensure
# sse.close # sse.close
# end
# end # end
# end
# #
# Note: SSEs are not currently supported by IE. However, they are supported # Note: SSEs are not currently supported by IE. However, they are supported by
# by Chrome, Firefox, Opera, and Safari. # Chrome, Firefox, Opera, and Safari.
class SSE class SSE
PERMITTED_OPTIONS = %w( retry event id ) PERMITTED_OPTIONS = %w( retry event id )
@ -153,10 +155,9 @@ class << self
# Ignore that the client has disconnected. # Ignore that the client has disconnected.
# #
# If this value is `true`, calling `write` after the client # If this value is `true`, calling `write` after the client disconnects will
# disconnects will result in the written content being silently # result in the written content being silently discarded. If this value is
# discarded. If this value is `false` (the default), a # `false` (the default), a ClientDisconnected exception will be raised.
# ClientDisconnected exception will be raised.
attr_accessor :ignore_disconnect attr_accessor :ignore_disconnect
def initialize(response) def initialize(response)
@ -167,9 +168,10 @@ def initialize(response)
@ignore_disconnect = false @ignore_disconnect = false
end end
# ActionDispatch::Response delegates #to_ary to the internal ActionDispatch::Response::Buffer, # ActionDispatch::Response delegates #to_ary to the internal
# defining #to_ary is an indicator that the response body can be buffered and/or cached by # ActionDispatch::Response::Buffer, defining #to_ary is an indicator that the
# Rack middlewares, this is not the case for Live responses so we undefine it for this Buffer subclass. # response body can be buffered and/or cached by Rack middlewares, this is not
# the case for Live responses so we undefine it for this Buffer subclass.
undef_method :to_ary undef_method :to_ary
def write(string) def write(string)
@ -184,21 +186,20 @@ def write(string)
@buf.clear @buf.clear
unless @ignore_disconnect unless @ignore_disconnect
# Raise ClientDisconnected, which is a RuntimeError (not an # Raise ClientDisconnected, which is a RuntimeError (not an IOError), because
# IOError), because that's more appropriate for something beyond # that's more appropriate for something beyond the developer's control.
# the developer's control.
raise ClientDisconnected, "client disconnected" raise ClientDisconnected, "client disconnected"
end end
end end
end end
# Same as +write+ but automatically include a newline at the end of the string. # Same as `write` but automatically include a newline at the end of the string.
def writeln(string) def writeln(string)
write string.end_with?("\n") ? string : "#{string}\n" write string.end_with?("\n") ? string : "#{string}\n"
end end
# Write a 'close' event to the buffer; the producer/writing thread # Write a 'close' event to the buffer; the producer/writing thread uses this to
# uses this to notify us that it's finished supplying content. # notify us that it's finished supplying content.
# #
# See also #abort. # See also #abort.
def close def close
@ -209,9 +210,8 @@ def close
end end
end end
# Inform the producer/writing thread that the client has # Inform the producer/writing thread that the client has disconnected; the
# disconnected; the reading thread is no longer interested in # reading thread is no longer interested in anything that's being written.
# anything that's being written.
# #
# See also #close. # See also #close.
def abort def abort
@ -223,8 +223,8 @@ def abort
# Is the client still connected and waiting for content? # Is the client still connected and waiting for content?
# #
# The result of calling `write` when this is `false` is determined # The result of calling `write` when this is `false` is determined by
# by `ignore_disconnect`. # `ignore_disconnect`.
def connected? def connected?
!@aborted !@aborted
end end
@ -275,15 +275,15 @@ def process(name)
locals = t1.keys.map { |key| [key, t1[key]] } locals = t1.keys.map { |key| [key, t1[key]] }
error = nil error = nil
# This processes the action in a child thread. It lets us return the # This processes the action in a child thread. It lets us return the response
# response code and headers back up the Rack stack, and still process # code and headers back up the Rack stack, and still process the body in
# the body in parallel with sending data to the client. # parallel with sending data to the client.
new_controller_thread { new_controller_thread {
ActiveSupport::Dependencies.interlock.running do ActiveSupport::Dependencies.interlock.running do
t2 = Thread.current t2 = Thread.current
# Since we're processing the view in a different thread, copy the # Since we're processing the view in a different thread, copy the thread locals
# thread locals from the main thread to the child thread. :'( # from the main thread to the child thread. :'(
locals.each { |k, v| t2[k] = v } locals.each { |k, v| t2[k] = v }
ActiveSupport::IsolatedExecutionState.share_with(t1) ActiveSupport::IsolatedExecutionState.share_with(t1)
@ -321,27 +321,30 @@ def response_body=(body)
response.close if response response.close if response
end end
# Sends a stream to the browser, which is helpful when you're generating exports or other running data where you # Sends a stream to the browser, which is helpful when you're generating exports
# don't want the entire file buffered in memory first. Similar to send_data, but where the data is generated live. # or other running data where you don't want the entire file buffered in memory
# first. Similar to send_data, but where the data is generated live.
# #
# Options: # Options:
# * <tt>:filename</tt> - suggests a filename for the browser to use. # * `:filename` - suggests a filename for the browser to use.
# * <tt>:type</tt> - specifies an HTTP content type. # * `:type` - specifies an HTTP content type. You can specify either a string
# You can specify either a string or a symbol for a registered type with <tt>Mime::Type.register</tt>, for example :json. # or a symbol for a registered type with `Mime::Type.register`, for example
# If omitted, type will be inferred from the file extension specified in <tt>:filename</tt>. # :json. If omitted, type will be inferred from the file extension specified
# If no content type is registered for the extension, the default type 'application/octet-stream' will be used. # in `:filename`. If no content type is registered for the extension, the
# * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded. # default type 'application/octet-stream' will be used.
# Valid values are 'inline' and 'attachment' (default). # * `:disposition` - specifies whether the file will be shown inline or
# downloaded. Valid values are 'inline' and 'attachment' (default).
#
# #
# Example of generating a csv export: # Example of generating a csv export:
# #
# send_stream(filename: "subscribers.csv") do |stream| # send_stream(filename: "subscribers.csv") do |stream|
# stream.write "email_address,updated_at\n" # stream.write "email_address,updated_at\n"
# #
# @subscribers.find_each do |subscriber| # @subscribers.find_each do |subscriber|
# stream.write "#{subscriber.email_address},#{subscriber.updated_at}\n" # stream.write "#{subscriber.email_address},#{subscriber.updated_at}\n"
# end # end
# end # end
def send_stream(filename:, disposition: "attachment", type: nil) def send_stream(filename:, disposition: "attachment", type: nil)
payload = { filename: filename, disposition: disposition, type: type } payload = { filename: filename, disposition: disposition, type: type }
ActiveSupport::Notifications.instrument("send_stream.action_controller", payload) do ActiveSupport::Notifications.instrument("send_stream.action_controller", payload) do
@ -360,10 +363,10 @@ def send_stream(filename:, disposition: "attachment", type: nil)
end end
private private
# Spawn a new thread to serve up the controller in. This is to get # Spawn a new thread to serve up the controller in. This is to get around the
# around the fact that Rack isn't based around IOs and we need to use # fact that Rack isn't based around IOs and we need to use a thread to stream
# a thread to stream data from the response bodies. Nobody should call # data from the response bodies. Nobody should call this method except in Rails
# this method except in Rails internals. Seriously! # internals. Seriously!
def new_controller_thread # :nodoc: def new_controller_thread # :nodoc:
Thread.new { Thread.new {
t2 = Thread.current t2 = Thread.current

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
module ActionController module ActionController
module Logging module Logging
extend ActiveSupport::Concern extend ActiveSupport::Concern
@ -7,10 +9,10 @@ module Logging
module ClassMethods module ClassMethods
# Set a different log level per request. # Set a different log level per request.
# #
# # Use the debug log level if a particular cookie is set. # # Use the debug log level if a particular cookie is set.
# class ApplicationController < ActionController::Base # class ApplicationController < ActionController::Base
# log_at :debug, if: -> { cookies[:debug] } # log_at :debug, if: -> { cookies[:debug] }
# end # end
# #
def log_at(level, **options) def log_at(level, **options)
around_action ->(_, action) { logger.log_at(level, &action) }, **options around_action ->(_, action) { logger.log_at(level, &action) }, **options

@ -1,203 +1,213 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
require "abstract_controller/collector" require "abstract_controller/collector"
module ActionController # :nodoc: module ActionController # :nodoc:
module MimeResponds module MimeResponds
# Without web-service support, an action which collects the data for displaying a list of people # Without web-service support, an action which collects the data for displaying
# might look something like this: # a list of people might look something like this:
# #
# def index # def index
# @people = Person.all # @people = Person.all
# end # end
# #
# That action implicitly responds to all formats, but formats can also be explicitly enumerated: # That action implicitly responds to all formats, but formats can also be
# explicitly enumerated:
# #
# def index # def index
# @people = Person.all # @people = Person.all
# respond_to :html, :js # respond_to :html, :js
# end # end
# #
# Here's the same action, with web-service support baked in: # Here's the same action, with web-service support baked in:
# #
# def index # def index
# @people = Person.all # @people = Person.all
# #
# respond_to do |format| # respond_to do |format|
# format.html # format.html
# format.js # format.js
# format.xml { render xml: @people } # format.xml { render xml: @people }
# end
# end # end
# end
# #
# What that says is, "if the client wants HTML or JS in response to this action, just respond as we # What that says is, "if the client wants HTML or JS in response to this action,
# would have before, but if the client wants XML, return them the list of people in XML format." # just respond as we would have before, but if the client wants XML, return them
# (\Rails determines the desired response format from the HTTP Accept header submitted by the client.) # 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 # Supposing you have an action that adds a new person, optionally creating their
# (by name) if it does not already exist, without web-services, it might look like this: # company (by name) if it does not already exist, without web-services, it might
# look like this:
# #
# def create # def create
# @company = Company.find_or_create_by(name: params[:company][:name]) # @company = Company.find_or_create_by(name: params[:company][:name])
# @person = @company.people.create(params[:person]) # @person = @company.people.create(params[:person])
# #
# redirect_to(person_list_url) # redirect_to(person_list_url)
# end # end
# #
# Here's the same action, with web-service support baked in: # Here's the same action, with web-service support baked in:
# #
# def create # def create
# company = params[:person].delete(:company) # company = params[:person].delete(:company)
# @company = Company.find_or_create_by(name: company[:name]) # @company = Company.find_or_create_by(name: company[:name])
# @person = @company.people.create(params[:person]) # @person = @company.people.create(params[:person])
# #
# respond_to do |format| # respond_to do |format|
# format.html { redirect_to(person_list_url) } # format.html { redirect_to(person_list_url) }
# format.js # format.js
# format.xml { render xml: @person.to_xml(include: @company) } # format.xml { render xml: @person.to_xml(include: @company) }
# end
# end # end
# end
# #
# If the client wants HTML, we just redirect them back to the person list. If they want JavaScript, # If the client wants HTML, we just redirect them back to the person list. If
# then it is an Ajax request and we render the JavaScript template associated with this action. # they want JavaScript, then it is an Ajax request and we render the JavaScript
# Lastly, if the client wants XML, we render the created person as XML, but with a twist: we also # template associated with this action. Lastly, if the client wants XML, we
# include the person's company in the rendered XML, so you get something like this: # 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> # <person>
# <id>...</id>
# ...
# <company>
# <id>...</id> # <id>...</id>
# <name>...</name>
# ... # ...
# </company> # <company>
# </person> # <id>...</id>
# <name>...</name>
# ...
# </company>
# </person>
# #
# Note, however, the extra bit at the top of that action: # Note, however, the extra bit at the top of that action:
# #
# company = params[:person].delete(:company) # company = params[:person].delete(:company)
# @company = Company.find_or_create_by(name: company[:name]) # @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 # This is because the incoming XML document (if a web-service request is in
# single root-node. So, we have to rearrange things so that the request looks like this (url-encoded): # 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]=...&... # person[name]=...&person[company][name]=...&...
# #
# And, like this (xml-encoded): # And, like this (xml-encoded):
# #
# <person> # <person>
# <name>...</name>
# <company>
# <name>...</name> # <name>...</name>
# </company> # <company>
# </person> # <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, # In other words, we make the request so that it operates on a single entity's
# we extract the company data from the request, find or create the company, and then create the new person # person. Then, in the action, we extract the company data from the request,
# with the remaining data. # 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 # Note that you can define your own XML parameter parser which would allow you
# in a single request (i.e., by wrapping them all in a single root node), but if you just go with the flow # to describe multiple entities in a single request (i.e., by wrapping them all
# and accept \Rails' defaults, life will be much easier. # 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 # If you need to use a MIME type which isn't supported by default, you can
# +config/initializers/mime_types.rb+ as follows. # register your own handlers in `config/initializers/mime_types.rb` as follows.
# #
# Mime::Type.register "image/jpeg", :jpg # Mime::Type.register "image/jpeg", :jpg
# #
# +respond_to+ also allows you to specify a common block for different formats by using +any+: # `respond_to` also allows you to specify a common block for different formats
# by using `any`:
# #
# def index # def index
# @people = Person.all # @people = Person.all
# #
# respond_to do |format| # respond_to do |format|
# format.html # format.html
# format.any(:xml, :json) { render request.format.to_sym => @people } # format.any(:xml, :json) { render request.format.to_sym => @people }
# end
# end # end
# end
# #
# In the example above, if the format is xml, it will render: # In the example above, if the format is xml, it will render:
# #
# render xml: @people # render xml: @people
# #
# Or if the format is json: # Or if the format is json:
# #
# render json: @people # render json: @people
# #
# +any+ can also be used with no arguments, in which case it will be used for any format requested by # `any` can also be used with no arguments, in which case it will be used for
# the user: # any format requested by the user:
# #
# respond_to do |format| # respond_to do |format|
# format.html # format.html
# format.any { redirect_to support_path } # format.any { redirect_to support_path }
# end # end
# #
# Formats can have different variants. # Formats can have different variants.
# #
# The request variant is a specialization of the request format, like <tt>:tablet</tt>, # The request variant is a specialization of the request format, like `:tablet`,
# <tt>:phone</tt>, or <tt>:desktop</tt>. # `:phone`, or `:desktop`.
# #
# We often want to render different html/json/xml templates for phones, # We often want to render different html/json/xml templates for phones, tablets,
# tablets, and desktop browsers. Variants make it easy. # and desktop browsers. Variants make it easy.
# #
# You can set the variant in a +before_action+: # You can set the variant in a `before_action`:
# #
# request.variant = :tablet if /iPad/.match?(request.user_agent) # request.variant = :tablet if /iPad/.match?(request.user_agent)
# #
# Respond to variants in the action just like you respond to formats: # Respond to variants in the action just like you respond to formats:
# #
# respond_to do |format| # respond_to do |format|
# format.html do |variant| # format.html do |variant|
# variant.tablet # renders app/views/projects/show.html+tablet.erb # variant.tablet # renders app/views/projects/show.html+tablet.erb
# variant.phone { extra_setup; render ... } # variant.phone { extra_setup; render ... }
# variant.none { special_setup } # executed only if there is no variant set # variant.none { special_setup } # executed only if there is no variant set
# end
# end # end
# end
# #
# Provide separate templates for each format and variant: # Provide separate templates for each format and variant:
# #
# app/views/projects/show.html.erb # app/views/projects/show.html.erb
# app/views/projects/show.html+tablet.erb # app/views/projects/show.html+tablet.erb
# app/views/projects/show.html+phone.erb # app/views/projects/show.html+phone.erb
# #
# When you're not sharing any code within the format, you can simplify defining variants # When you're not sharing any code within the format, you can simplify defining
# using the inline syntax: # variants using the inline syntax:
# #
# respond_to do |format| # respond_to do |format|
# format.js { render "trash" } # format.js { render "trash" }
# format.html.phone { redirect_to progress_path } # format.html.phone { redirect_to progress_path }
# format.html.none { render "trash" } # format.html.none { render "trash" }
# end # end
# #
# Variants also support common +any+/+all+ block that formats have. # Variants also support common `any`/`all` block that formats have.
# #
# It works for both inline: # It works for both inline:
# #
# respond_to do |format| # respond_to do |format|
# format.html.any { render html: "any" } # format.html.any { render html: "any" }
# format.html.phone { render html: "phone" } # format.html.phone { render html: "phone" }
# end # end
# #
# and block syntax: # and block syntax:
# #
# respond_to do |format| # respond_to do |format|
# format.html do |variant| # format.html do |variant|
# variant.any(:tablet, :phablet){ render html: "any" } # variant.any(:tablet, :phablet){ render html: "any" }
# variant.phone { render html: "phone" } # variant.phone { render html: "phone" }
# end
# end # end
# end
# #
# You can also set an array of variants: # You can also set an array of variants:
# #
# request.variant = [:tablet, :phone] # request.variant = [:tablet, :phone]
# #
# This will work similarly to formats and MIME types negotiation. If there # This will work similarly to formats and MIME types negotiation. If there is no
# is no +:tablet+ variant declared, the +:phone+ variant will be used: # `:tablet` variant declared, the `:phone` variant will be used:
# #
# respond_to do |format| # respond_to do |format|
# format.html.none # format.html.none
# format.html.phone # this gets rendered # format.html.phone # this gets rendered
# end # end
def respond_to(*mimes) def respond_to(*mimes)
raise ArgumentError, "respond_to takes either types or a block, never both" if mimes.any? && block_given? raise ArgumentError, "respond_to takes either types or a block, never both" if mimes.any? && block_given?
@ -217,27 +227,26 @@ def respond_to(*mimes)
end end
end end
# A container for responses available from the current controller for # A container for responses available from the current controller for requests
# requests for different mime-types sent to a particular action. # for different mime-types sent to a particular action.
# #
# The public controller methods +respond_to+ may be called with a block # The public controller methods `respond_to` may be called with a block that is
# that is used to define responses to different mime-types, e.g. # used to define responses to different mime-types, e.g. for `respond_to` :
# for +respond_to+ :
# #
# respond_to do |format| # respond_to do |format|
# format.html # format.html
# format.xml { render xml: @people } # format.xml { render xml: @people }
# end # end
# #
# In this usage, the argument passed to the block (+format+ above) is an # In this usage, the argument passed to the block (`format` above) is an
# instance of the ActionController::MimeResponds::Collector class. This # instance of the ActionController::MimeResponds::Collector class. This object
# object serves as a container in which available responses can be stored by # serves as a container in which available responses can be stored by calling
# calling any of the dynamically generated, mime-type-specific methods such # any of the dynamically generated, mime-type-specific methods such as `html`,
# as +html+, +xml+ etc on the Collector. Each response is represented by a # `xml` etc on the Collector. Each response is represented by a corresponding
# corresponding block if present. # block if present.
# #
# A subsequent call to #negotiate_format(request) will enable the Collector # A subsequent call to #negotiate_format(request) will enable the Collector to
# to determine which specific mime-type it should respond with for the current # determine which specific mime-type it should respond with for the current
# request, with this response then being accessible by calling #response. # request, with this response then being accessible by calling #response.
class Collector class Collector
include AbstractController::Collector include AbstractController::Collector

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
module ActionController module ActionController
# Specify binary encoding for parameters for a given action. # Specify binary encoding for parameters for a given action.
module ParameterEncoding module ParameterEncoding
@ -21,59 +23,59 @@ def action_encoding_template(action) # :nodoc:
end end
end end
# Specify that a given action's parameters should all be encoded as # Specify that a given action's parameters should all be encoded as ASCII-8BIT
# ASCII-8BIT (it "skips" the encoding default of UTF-8). # (it "skips" the encoding default of UTF-8).
# #
# For example, a controller would use it like this: # For example, a controller would use it like this:
# #
# class RepositoryController < ActionController::Base # class RepositoryController < ActionController::Base
# skip_parameter_encoding :show # skip_parameter_encoding :show
# #
# def show # def show
# @repo = Repository.find_by_filesystem_path params[:file_path] # @repo = Repository.find_by_filesystem_path params[:file_path]
# #
# # `repo_name` is guaranteed to be UTF-8, but was ASCII-8BIT, so # # `repo_name` is guaranteed to be UTF-8, but was ASCII-8BIT, so
# # tag it as such # # tag it as such
# @repo_name = params[:repo_name].force_encoding 'UTF-8' # @repo_name = params[:repo_name].force_encoding 'UTF-8'
# end
#
# def index
# @repositories = Repository.all
# end
# end # end
# #
# def index
# @repositories = Repository.all
# end
# end
#
# The show action in the above controller would have all parameter values # 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 # encoded as ASCII-8BIT. This is useful in the case where an application must
# must handle data but encoding of the data is unknown, like file system data. # handle data but encoding of the data is unknown, like file system data.
def skip_parameter_encoding(action) def skip_parameter_encoding(action)
@_parameter_encodings[action.to_s] = Hash.new { Encoding::ASCII_8BIT } @_parameter_encodings[action.to_s] = Hash.new { Encoding::ASCII_8BIT }
end end
# Specify the encoding for a parameter on an action. # Specify the encoding for a parameter on an action. If not specified the
# If not specified the default is UTF-8. # default is UTF-8.
# #
# You can specify a binary (ASCII_8BIT) parameter with: # You can specify a binary (ASCII_8BIT) parameter with:
# #
# class RepositoryController < ActionController::Base # class RepositoryController < ActionController::Base
# # This specifies that file_path is not UTF-8 and is instead ASCII_8BIT # # This specifies that file_path is not UTF-8 and is instead ASCII_8BIT
# param_encoding :show, :file_path, Encoding::ASCII_8BIT # param_encoding :show, :file_path, Encoding::ASCII_8BIT
# #
# def show # def show
# @repo = Repository.find_by_filesystem_path params[:file_path] # @repo = Repository.find_by_filesystem_path params[:file_path]
# #
# # params[:repo_name] remains UTF-8 encoded # # params[:repo_name] remains UTF-8 encoded
# @repo_name = params[:repo_name] # @repo_name = params[:repo_name]
# end
#
# def index
# @repositories = Repository.all
# end
# end # end
# #
# def index # The file_path parameter on the show action would be encoded as ASCII-8BIT, but
# @repositories = Repository.all # all other arguments will remain UTF-8 encoded. This is useful in the case
# end # where an application must handle data but encoding of the data is unknown,
# end # like file system data.
#
# The file_path parameter on the show action would be encoded as ASCII-8BIT,
# but all other arguments will remain UTF-8 encoded.
# This is useful in the case where an application must handle data
# but encoding of the data is unknown, like file system data.
def param_encoding(action, param, encoding) def param_encoding(action, param, encoding)
@_parameter_encodings[action.to_s][param.to_s] = encoding @_parameter_encodings[action.to_s][param.to_s] = encoding
end end

@ -1,12 +1,14 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
require "active_support/core_ext/hash/slice" require "active_support/core_ext/hash/slice"
require "active_support/core_ext/hash/except" require "active_support/core_ext/hash/except"
require "active_support/core_ext/module/anonymous" require "active_support/core_ext/module/anonymous"
require "action_dispatch/http/mime_type" require "action_dispatch/http/mime_type"
module ActionController module ActionController
# = Action Controller Params Wrapper # # Action Controller Params Wrapper
# #
# Wraps the parameters hash into a nested hash. This will allow clients to # Wraps the parameters hash into a nested hash. This will allow clients to
# submit requests without having to specify any root elements. # submit requests without having to specify any root elements.
@ -24,8 +26,8 @@ module ActionController
# wrap_parameters format: [:json, :xml, :url_encoded_form, :multipart_form] # wrap_parameters format: [:json, :xml, :url_encoded_form, :multipart_form]
# end # end
# #
# If you enable +ParamsWrapper+ for +:json+ format, instead of having to # If you enable `ParamsWrapper` for `:json` format, instead of having to send
# send JSON parameters like this: # JSON parameters like this:
# #
# {"user": {"name": "Konata"}} # {"user": {"name": "Konata"}}
# #
@ -34,45 +36,44 @@ module ActionController
# {"name": "Konata"} # {"name": "Konata"}
# #
# And it will be wrapped into a nested hash with the key name matching the # 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+, # controller's name. For example, if you're posting to `UsersController`, your
# your new +params+ hash will look like this: # new `params` hash will look like this:
# #
# {"name" => "Konata", "user" => {"name" => "Konata"}} # {"name" => "Konata", "user" => {"name" => "Konata"}}
# #
# You can also specify the key in which the parameters should be wrapped to, # You can also specify the key in which the parameters should be wrapped to, and
# and also the list of attributes it should wrap by using either +:include+ or # also the list of attributes it should wrap by using either `:include` or
# +:exclude+ options like this: # `:exclude` options like this:
# #
# class UsersController < ApplicationController # class UsersController < ApplicationController
# wrap_parameters :person, include: [:username, :password] # wrap_parameters :person, include: [:username, :password]
# end # end
# #
# On Active Record models with no +:include+ or +:exclude+ option set, # On Active Record models with no `:include` or `:exclude` option set, it will
# it will only wrap the parameters returned by the class method # only wrap the parameters returned by the class method `attribute_names`.
# <tt>attribute_names</tt>.
# #
# If you're going to pass the parameters to an +ActiveModel+ object (such as # 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 # `User.new(params[:user])`), you might consider passing the model class to the
# the method instead. The +ParamsWrapper+ will actually try to determine the # method instead. The `ParamsWrapper` will actually try to determine the list of
# list of attribute names from the model and only wrap those attributes: # attribute names from the model and only wrap those attributes:
# #
# class UsersController < ApplicationController # class UsersController < ApplicationController
# wrap_parameters Person # wrap_parameters Person
# end # end
# #
# You still could pass +:include+ and +:exclude+ to set the list of attributes # You still could pass `:include` and `:exclude` to set the list of attributes
# you want to wrap. # you want to wrap.
# #
# By default, if you don't specify the key in which the parameters would be # 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 # wrapped to, `ParamsWrapper` will actually try to determine if there's a model
# a model related to it or not. This controller, for example: # related to it or not. This controller, for example:
# #
# class Admin::UsersController < ApplicationController # class Admin::UsersController < ApplicationController
# end # end
# #
# will try to check if +Admin::User+ or +User+ model exists, and use it to # will try to check if `Admin::User` or `User` model exists, and use it to
# determine the wrapper key respectively. If both models don't exist, # determine the wrapper key respectively. If both models don't exist, it will
# it will then fall back to use +user+ as the key. # then fall back to use `user` as the key.
# #
# To disable this functionality for a controller: # To disable this functionality for a controller:
# #
@ -154,13 +155,13 @@ def name
end end
private private
# Determine the wrapper model from the controller's name. By convention, # Determine the wrapper model from the controller's name. By convention, this
# this could be done by trying to find the defined model that has the # could be done by trying to find the defined model that has the same singular
# same singular name as the controller. For example, +UsersController+ # name as the controller. For example, `UsersController` will try to find if the
# will try to find if the +User+ model exists. # `User` model exists.
# #
# This method also does namespace lookup. Foo::Bar::UsersController will # This method also does namespace lookup. Foo::Bar::UsersController will try to
# try to find Foo::Bar::User, Foo::User and finally User. # find Foo::Bar::User, Foo::User and finally User.
def _default_wrap_model def _default_wrap_model
return nil if klass.anonymous? return nil if klass.anonymous?
model_name = klass.name.delete_suffix("Controller").classify model_name = klass.name.delete_suffix("Controller").classify
@ -189,33 +190,34 @@ def _set_wrapper_options(options)
self._wrapper_options = Options.from_hash(options) self._wrapper_options = Options.from_hash(options)
end end
# Sets the name of the wrapper key, or the model which +ParamsWrapper+ # Sets the name of the wrapper key, or the model which `ParamsWrapper` would use
# would use to determine the attribute names from. # to determine the attribute names from.
# #
# ==== Examples # #### Examples
# wrap_parameters format: :xml # wrap_parameters format: :xml
# # enables the parameter wrapper for XML format # # enables the parameter wrapper for XML format
# #
# wrap_parameters :person # wrap_parameters :person
# # wraps parameters into +params[:person]+ hash # # wraps parameters into +params[:person]+ hash
# #
# wrap_parameters Person # wrap_parameters Person
# # wraps parameters by determining the wrapper key from Person class # # wraps parameters by determining the wrapper key from Person class
# # (+person+, in this case) and the list of attribute names # # (+person+, in this case) and the list of attribute names
# #
# wrap_parameters include: [:username, :title] # wrap_parameters include: [:username, :title]
# # wraps only +:username+ and +:title+ attributes from parameters. # # wraps only +:username+ and +:title+ attributes from parameters.
# #
# wrap_parameters false # wrap_parameters false
# # disables parameters wrapping for this controller altogether. # # disables parameters wrapping for this controller altogether.
#
# #### Options
# * `:format` - The list of formats in which the parameters wrapper will be
# enabled.
# * `:include` - The list of attribute names which parameters wrapper will
# wrap into a nested hash.
# * `:exclude` - The list of attribute names which parameters wrapper will
# exclude from a nested hash.
# #
# ==== 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 = {}) def wrap_parameters(name_or_model_or_options, options = {})
model = nil model = nil
@ -237,9 +239,8 @@ def wrap_parameters(name_or_model_or_options, options = {})
self._wrapper_options = opts self._wrapper_options = opts
end end
# Sets the default wrapper key or model which will be used to determine # Sets the default wrapper key or model which will be used to determine wrapper
# wrapper key and attribute names. Called automatically when the # key and attribute names. Called automatically when the module is inherited.
# module is inherited.
def inherited(klass) def inherited(klass)
if klass._wrapper_options.format.any? if klass._wrapper_options.format.any?
params = klass._wrapper_options.dup params = klass._wrapper_options.dup
@ -251,8 +252,8 @@ def inherited(klass)
end end
private private
# Performs parameters wrapping upon the request. Called automatically # Performs parameters wrapping upon the request. Called automatically by the
# by the metal call stack. # metal call stack.
def process_action(*) def process_action(*)
_perform_parameter_wrapping if _wrapper_enabled? _perform_parameter_wrapping if _wrapper_enabled?
super super

@ -1,27 +1,28 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
module ActionController # :nodoc: module ActionController # :nodoc:
module PermissionsPolicy module PermissionsPolicy
extend ActiveSupport::Concern extend ActiveSupport::Concern
module ClassMethods module ClassMethods
# Overrides parts of the globally configured +Feature-Policy+ # Overrides parts of the globally configured `Feature-Policy` header:
# header:
# #
# class PagesController < ApplicationController # class PagesController < ApplicationController
# permissions_policy do |policy| # permissions_policy do |policy|
# policy.geolocation "https://example.com" # policy.geolocation "https://example.com"
# end
# end # end
# end
# #
# Options can be passed similar to +before_action+. For example, pass # Options can be passed similar to `before_action`. For example, pass `only:
# <tt>only: :index</tt> to override the header on the index action only: # :index` to override the header on the index action only:
# #
# class PagesController < ApplicationController # class PagesController < ApplicationController
# permissions_policy(only: :index) do |policy| # permissions_policy(only: :index) do |policy|
# policy.camera :self # policy.camera :self
# end
# end # end
# end
# #
def permissions_policy(**options, &block) def permissions_policy(**options, &block)
before_action(options) do before_action(options) do

@ -1,39 +1,49 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
module ActionController # :nodoc: module ActionController # :nodoc:
module RateLimiting module RateLimiting
extend ActiveSupport::Concern extend ActiveSupport::Concern
module ClassMethods module ClassMethods
# Applies a rate limit to all actions or those specified by the normal <tt>before_action</tt> filters with <tt>only:</tt> and <tt>except:</tt>. # Applies a rate limit to all actions or those specified by the normal
# `before_action` filters with `only:` and `except:`.
# #
# The maximum number of requests allowed is specified <tt>to:</tt> and constrained to the window of time given by <tt>within:</tt>. # The maximum number of requests allowed is specified `to:` and constrained to
# the window of time given by `within:`.
# #
# Rate limits are by default unique to the ip address making the request, but you can provide your own identity function by passing a callable # Rate limits are by default unique to the ip address making the request, but
# in the <tt>by:</tt> parameter. It's evaluated within the context of the controller processing the request. # you can provide your own identity function by passing a callable in the `by:`
# parameter. It's evaluated within the context of the controller processing the
# request.
# #
# Requests that exceed the rate limit are refused with a <tt>429 Too Many Requests</tt> response. You can specialize this by passing a callable # Requests that exceed the rate limit are refused with a `429 Too Many Requests`
# in the <tt>with:</tt> parameter. It's evaluated within the context of the controller processing the request. # response. You can specialize this by passing a callable in the `with:`
# parameter. It's evaluated within the context of the controller processing the
# request.
# #
# Rate limiting relies on a backing <tt>ActiveSupport::Cache</tt> store and defaults to <tt>config.action_controller.cache_store</tt>, which # Rate limiting relies on a backing `ActiveSupport::Cache` store and defaults to
# itself defaults to the global <tt>config.cache_store</tt>. If you don't want to store rate limits in the same datastore as your general caches, # `config.action_controller.cache_store`, which itself defaults to the global
# you can pass a custom store in the <tt>store</tt> parameter. # `config.cache_store`. If you don't want to store rate limits in the same
# datastore as your general caches, you can pass a custom store in the `store`
# parameter.
# #
# Examples: # Examples:
# #
# class SessionsController < ApplicationController # class SessionsController < ApplicationController
# rate_limit to: 10, within: 3.minutes, only: :create # rate_limit to: 10, within: 3.minutes, only: :create
# end # end
# #
# class SignupsController < ApplicationController # class SignupsController < ApplicationController
# rate_limit to: 1000, within: 10.seconds, # rate_limit to: 1000, within: 10.seconds,
# by: -> { request.domain }, with: -> { redirect_to busy_controller_url, alert: "Too many signups on domain!" }, only: :new # by: -> { request.domain }, with: -> { redirect_to busy_controller_url, alert: "Too many signups on domain!" }, only: :new
# end # end
# #
# class APIController < ApplicationController # class APIController < ApplicationController
# RATE_LIMIT_STORE = ActiveSupport::Cache::RedisCacheStore.new(url: ENV["REDIS_URL"]) # RATE_LIMIT_STORE = ActiveSupport::Cache::RedisCacheStore.new(url: ENV["REDIS_URL"])
# rate_limit to: 10, within: 3.minutes, store: RATE_LIMIT_STORE # rate_limit to: 10, within: 3.minutes, store: RATE_LIMIT_STORE
# end # end
def rate_limit(to:, within:, by: -> { request.remote_ip }, with: -> { head :too_many_requests }, store: cache_store, **options) def rate_limit(to:, within:, by: -> { request.remote_ip }, with: -> { head :too_many_requests }, store: cache_store, **options)
before_action -> { rate_limiting(to: to, within: within, by: by, with: with, store: store) }, **options before_action -> { rate_limiting(to: to, within: within, by: by, with: with, store: store) }, **options
end end

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
module ActionController module ActionController
module Redirecting module Redirecting
extend ActiveSupport::Concern extend ActiveSupport::Concern
@ -15,72 +17,89 @@ class UnsafeRedirectError < StandardError; end
mattr_accessor :raise_on_open_redirects, default: false mattr_accessor :raise_on_open_redirects, default: false
end end
# Redirects the browser to the target specified in +options+. This parameter can be any one of: # 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+. # * `Hash` - 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. # * `Record` - The URL will be generated by calling url_for with the
# * <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. # `options`, which will reference a named URL for that record.
# * <tt>String</tt> not containing a protocol - The current protocol and host is prepended to the string. # * `String` starting with `protocol://` (like `http://`) or a protocol
# * <tt>Proc</tt> - A block that will be executed in the controller's context. Should return any option accepted by +redirect_to+. # relative reference (like `//`) - Is passed straight through as the target
# for redirection.
# * `String` not containing a protocol - The current protocol and host is
# prepended to the string.
# * `Proc` - 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 # ### Examples
# 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 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) }
# #
# redirect_to post_url(@post), status: :found # The redirection happens as a `302 Found` header unless otherwise specified
# redirect_to action: 'atom', status: :moved_permanently # using the `:status` option:
# redirect_to post_url(@post), status: 301
# redirect_to action: 'atom', status: 302
# #
# The status code can either be a standard {HTTP Status code}[https://www.iana.org/assignments/http-status-codes] as an # redirect_to post_url(@post), status: :found
# integer, or a symbol representing the downcased, underscored and symbolized description. # redirect_to action: 'atom', status: :moved_permanently
# Note that the status code must be a 3xx HTTP code, or redirection will not occur. # redirect_to post_url(@post), status: 301
# redirect_to action: 'atom', status: 302
#
# The status code can either be a standard [HTTP Status
# code](https://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 # 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 # 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 # 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 # around this you can return a `303 See Other` status code which will be
# followed using a GET request. # followed using a GET request.
# #
# redirect_to posts_url, status: :see_other # redirect_to posts_url, status: :see_other
# redirect_to action: 'index', status: 303 # 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 # It is also possible to assign a flash message as part of the redirection.
# +alert+ and +notice+ as well as a general purpose +flash+ bucket. # 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), alert: "Watch it, mister!"
# redirect_to post_url(@post), status: :found, notice: "Pay attention to the road" # 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 post_url(@post), status: 301, flash: { updated_post_id: @post.id }
# redirect_to({ action: 'atom' }, alert: "Something serious happened") # 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. # Statements after `redirect_to` in our controller get executed, so
# To terminate the execution of the function immediately after the +redirect_to+, use return. # `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 # redirect_to post_url(@post) and return
# #
# === Open Redirect protection # ### Open Redirect protection
# #
# By default, \Rails protects against redirecting to external hosts for your app's safety, so called open redirects. # By default, Rails protects against redirecting to external hosts for your
# Note: this was a new default in \Rails 7.0, after upgrading opt-in by uncommenting the line with +raise_on_open_redirects+ in <tt>config/initializers/new_framework_defaults_7_0.rb</tt> # app's safety, so called open redirects. Note: this was a new default in Rails
# 7.0, after upgrading opt-in by uncommenting the line with
# `raise_on_open_redirects` in
# `config/initializers/new_framework_defaults_7_0.rb`
# #
# Here #redirect_to automatically validates the potentially-unsafe URL: # Here #redirect_to automatically validates the potentially-unsafe URL:
# #
# redirect_to params[:redirect_url] # redirect_to params[:redirect_url]
# #
# Raises UnsafeRedirectError in the case of an unsafe redirect. # Raises UnsafeRedirectError in the case of an unsafe redirect.
# #
# To allow any external redirects pass <tt>allow_other_host: true</tt>, though using a user-provided param in that case is unsafe. # To allow any external redirects pass `allow_other_host: true`, though using a
# user-provided param in that case is unsafe.
# #
# redirect_to "https://rubyonrails.org", allow_other_host: true # redirect_to "https://rubyonrails.org", allow_other_host: true
# #
# See #url_from for more information on what an internal and safe URL is, or how to fall back to an alternate redirect URL in the unsafe case. # See #url_from for more information on what an internal and safe URL is, or how
# to fall back to an alternate redirect URL in the unsafe case.
def redirect_to(options = {}, response_options = {}) def redirect_to(options = {}, response_options = {})
raise ActionControllerError.new("Cannot redirect to nil!") unless options raise ActionControllerError.new("Cannot redirect to nil!") unless options
raise AbstractController::DoubleRenderError if response_body raise AbstractController::DoubleRenderError if response_body
@ -96,50 +115,53 @@ def redirect_to(options = {}, response_options = {})
self.response_body = "" self.response_body = ""
end end
# Soft deprecated alias for #redirect_back_or_to where the +fallback_location+ location is supplied as a keyword argument instead # Soft deprecated alias for #redirect_back_or_to where the `fallback_location`
# of the first positional argument. # location is supplied as a keyword argument instead of the first positional
# argument.
def redirect_back(fallback_location:, allow_other_host: _allow_other_host, **args) def redirect_back(fallback_location:, allow_other_host: _allow_other_host, **args)
redirect_back_or_to fallback_location, allow_other_host: allow_other_host, **args redirect_back_or_to fallback_location, allow_other_host: allow_other_host, **args
end end
# Redirects the browser to the page that issued the request (the referrer) # Redirects the browser to the page that issued the request (the referrer) if
# if possible, otherwise redirects to the provided default fallback # possible, otherwise redirects to the provided default fallback location.
# location.
# #
# The referrer information is pulled from the HTTP +Referer+ (sic) header on # The referrer information is pulled from the HTTP `Referer` (sic) header on the
# the request. This is an optional header and its presence on the request is # request. This is an optional header and its presence on the request is subject
# subject to browser security settings and user preferences. If the request # to browser security settings and user preferences. If the request is missing
# is missing this header, the <tt>fallback_location</tt> will be used. # this header, the `fallback_location` will be used.
# #
# redirect_back_or_to({ action: "show", id: 5 }) # redirect_back_or_to({ action: "show", id: 5 })
# redirect_back_or_to @post # redirect_back_or_to @post
# redirect_back_or_to "http://www.rubyonrails.org" # redirect_back_or_to "http://www.rubyonrails.org"
# redirect_back_or_to "/images/screenshot.jpg" # redirect_back_or_to "/images/screenshot.jpg"
# redirect_back_or_to posts_url # redirect_back_or_to posts_url
# redirect_back_or_to proc { edit_post_url(@post) } # redirect_back_or_to proc { edit_post_url(@post) }
# redirect_back_or_to '/', allow_other_host: false # redirect_back_or_to '/', allow_other_host: false
# #
# ==== Options # #### Options
# * <tt>:allow_other_host</tt> - Allow or disallow redirection to the host that is different to the current host, defaults to true. # * `:allow_other_host` - Allow or disallow redirection to the host that is
# different to the current host, defaults to true.
# #
# All other options that can be passed to #redirect_to are accepted as #
# options, and the behavior is identical. # All other options that can be passed to #redirect_to are accepted as options,
# and the behavior is identical.
def redirect_back_or_to(fallback_location, allow_other_host: _allow_other_host, **options) def redirect_back_or_to(fallback_location, allow_other_host: _allow_other_host, **options)
if request.referer && (allow_other_host || _url_host_allowed?(request.referer)) if request.referer && (allow_other_host || _url_host_allowed?(request.referer))
redirect_to request.referer, allow_other_host: allow_other_host, **options redirect_to request.referer, allow_other_host: allow_other_host, **options
else else
# The method level `allow_other_host` doesn't apply in the fallback case, omit and let the `redirect_to` handling take over. # The method level `allow_other_host` doesn't apply in the fallback case, omit
# and let the `redirect_to` handling take over.
redirect_to fallback_location, **options redirect_to fallback_location, **options
end end
end end
def _compute_redirect_to_location(request, options) # :nodoc: def _compute_redirect_to_location(request, options) # :nodoc:
case options case options
# The scheme name consist of a letter followed by any combination of # The scheme name consist of a letter followed by any combination of letters,
# letters, digits, and the plus ("+"), period ("."), or hyphen ("-") # digits, and the plus ("+"), period ("."), or hyphen ("-") characters; and is
# characters; and is terminated by a colon (":"). # terminated by a colon (":"). See
# See https://tools.ietf.org/html/rfc3986#section-3.1 # https://tools.ietf.org/html/rfc3986#section-3.1 The protocol relative scheme
# The protocol relative scheme starts with a double slash "//". # starts with a double slash "//".
when /\A([a-z][a-z\d\-+.]*:|\/\/).*/i when /\A([a-z][a-z\d\-+.]*:|\/\/).*/i
options.to_str options.to_str
when String when String
@ -153,25 +175,30 @@ def _compute_redirect_to_location(request, options) # :nodoc:
module_function :_compute_redirect_to_location module_function :_compute_redirect_to_location
public :_compute_redirect_to_location public :_compute_redirect_to_location
# Verifies the passed +location+ is an internal URL that's safe to redirect to and returns it, or nil if not. # Verifies the passed `location` is an internal URL that's safe to redirect to
# Useful to wrap a params provided redirect URL and fall back to an alternate URL to redirect to: # and returns it, or nil if not. Useful to wrap a params provided redirect URL
# and fall back to an alternate URL to redirect to:
# #
# redirect_to url_from(params[:redirect_url]) || root_url # redirect_to url_from(params[:redirect_url]) || root_url
# #
# The +location+ is considered internal, and safe, if it's on the same host as <tt>request.host</tt>: # The `location` is considered internal, and safe, if it's on the same host as
# `request.host`:
# #
# # If request.host is example.com: # # If request.host is example.com:
# url_from("https://example.com/profile") # => "https://example.com/profile" # url_from("https://example.com/profile") # => "https://example.com/profile"
# url_from("http://example.com/profile") # => "http://example.com/profile" # url_from("http://example.com/profile") # => "http://example.com/profile"
# url_from("http://evil.com/profile") # => nil # url_from("http://evil.com/profile") # => nil
# #
# Subdomains are considered part of the host: # Subdomains are considered part of the host:
# #
# # If request.host is on https://example.com or https://app.example.com, you'd get: # # If request.host is on https://example.com or https://app.example.com, you'd get:
# url_from("https://dev.example.com/profile") # => nil # url_from("https://dev.example.com/profile") # => nil
# #
# NOTE: there's a similarity with {url_for}[rdoc-ref:ActionDispatch::Routing::UrlFor#url_for], which generates an internal URL from various options from within the app, e.g. <tt>url_for(@post)</tt>. # NOTE: there's a similarity with
# However, #url_from is meant to take an external parameter to verify as in <tt>url_from(params[:redirect_url])</tt>. # [url_for](rdoc-ref:ActionDispatch::Routing::UrlFor#url_for), which generates
# an internal URL from various options from within the app, e.g.
# `url_for(@post)`. However, #url_from is meant to take an external parameter to
# verify as in `url_from(params[:redirect_url])`.
def url_from(location) def url_from(location)
location = location.presence location = location.presence
location if location && _url_host_allowed?(location) location if location && _url_host_allowed?(location)
@ -212,9 +239,8 @@ def _url_host_allowed?(url)
end end
def _ensure_url_is_http_header_safe(url) def _ensure_url_is_http_header_safe(url)
# Attempt to comply with the set of valid token characters # Attempt to comply with the set of valid token characters defined for an HTTP
# defined for an HTTP header value in # header value in https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6
# https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6
if url.match?(ILLEGAL_HEADER_VALUE_REGEX) if url.match?(ILLEGAL_HEADER_VALUE_REGEX)
msg = "The redirect URL #{url} contains one or more illegal HTTP header field character. " \ msg = "The redirect URL #{url} contains one or more illegal HTTP header field character. " \
"Set of legal characters defined in https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6" "Set of legal characters defined in https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6"

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
require "set" require "set"
module ActionController module ActionController
@ -13,7 +15,7 @@ def self.remove_renderer(key)
Renderers.remove(key) Renderers.remove(key)
end end
# See <tt>Responder#api_behavior</tt> # See `Responder#api_behavior`
class MissingRenderer < LoadError class MissingRenderer < LoadError
def initialize(format) def initialize(format)
super "No renderer defined for format: #{format}" super "No renderer defined for format: #{format}"
@ -24,7 +26,7 @@ module Renderers
extend ActiveSupport::Concern extend ActiveSupport::Concern
# A Set containing renderer names that correspond to available renderer procs. # A Set containing renderer names that correspond to available renderer procs.
# Default values are <tt>:json</tt>, <tt>:js</tt>, <tt>:xml</tt>. # Default values are `:json`, `:js`, `:xml`.
RENDERERS = Set.new RENDERERS = Set.new
included do included do
@ -42,35 +44,34 @@ module All
end end
end end
# Adds a new renderer to call within controller actions. # Adds a new renderer to call within controller actions. A renderer is invoked
# A renderer is invoked by passing its name as an option to # by passing its name as an option to AbstractController::Rendering#render. To
# AbstractController::Rendering#render. To create a renderer # create a renderer pass it a name and a block. The block takes two arguments,
# pass it a name and a block. The block takes two arguments, the first # the first is the value paired with its key and the second is the remaining
# is the value paired with its key and the second is the remaining # hash of options passed to `render`.
# hash of options passed to +render+.
# #
# Create a csv renderer: # Create a csv renderer:
# #
# ActionController::Renderers.add :csv do |obj, options| # ActionController::Renderers.add :csv do |obj, options|
# filename = options[:filename] || 'data' # filename = options[:filename] || 'data'
# str = obj.respond_to?(:to_csv) ? obj.to_csv : obj.to_s # str = obj.respond_to?(:to_csv) ? obj.to_csv : obj.to_s
# send_data str, type: Mime[:csv], # send_data str, type: Mime[:csv],
# disposition: "attachment; filename=#{filename}.csv" # disposition: "attachment; filename=#{filename}.csv"
# end # end
# #
# Note that we used Mime[:csv] for the csv mime type as it comes with \Rails. # 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 # For a custom renderer, you'll need to register a mime type with
# <tt>Mime::Type.register</tt>. # `Mime::Type.register`.
# #
# To use the csv renderer in a controller action: # To use the csv renderer in a controller action:
# #
# def show # def show
# @csvable = Csvable.find(params[:id]) # @csvable = Csvable.find(params[:id])
# respond_to do |format| # respond_to do |format|
# format.html # format.html
# format.csv { render csv: @csvable, filename: @csvable.name } # format.csv { render csv: @csvable, filename: @csvable.name }
# end
# end # end
# end
def self.add(key, &block) def self.add(key, &block)
define_method(_render_with_renderer_method_name(key), &block) define_method(_render_with_renderer_method_name(key), &block)
RENDERERS << key.to_sym RENDERERS << key.to_sym
@ -80,7 +81,7 @@ def self.add(key, &block)
# #
# To remove a csv renderer: # To remove a csv renderer:
# #
# ActionController::Renderers.remove(:csv) # ActionController::Renderers.remove(:csv)
def self.remove(key) def self.remove(key)
RENDERERS.delete(key.to_sym) RENDERERS.delete(key.to_sym)
method_name = _render_with_renderer_method_name(key) method_name = _render_with_renderer_method_name(key)
@ -92,39 +93,39 @@ def self._render_with_renderer_method_name(key)
end end
module ClassMethods module ClassMethods
# Adds, by name, a renderer or renderers to the +_renderers+ available # Adds, by name, a renderer or renderers to the `_renderers` available to call
# to call within controller actions. # within controller actions.
# #
# It is useful when rendering from an ActionController::Metal controller or # It is useful when rendering from an ActionController::Metal controller or
# otherwise to add an available renderer proc to a specific controller. # otherwise to add an available renderer proc to a specific controller.
# #
# Both ActionController::Base and ActionController::API # Both ActionController::Base and ActionController::API include
# include ActionController::Renderers::All, making all renderers # ActionController::Renderers::All, making all renderers available in the
# available in the controller. See Renderers::RENDERERS and Renderers.add. # controller. See Renderers::RENDERERS and Renderers.add.
# #
# Since ActionController::Metal controllers cannot render, the controller # Since ActionController::Metal controllers cannot render, the controller must
# must include AbstractController::Rendering, ActionController::Rendering, # include AbstractController::Rendering, ActionController::Rendering, and
# and ActionController::Renderers, and have at least one renderer. # ActionController::Renderers, and have at least one renderer.
# #
# Rather than including ActionController::Renderers::All and including all renderers, # Rather than including ActionController::Renderers::All and including all
# you may specify which renderers to include by passing the renderer name or names to # renderers, you may specify which renderers to include by passing the renderer
# +use_renderers+. For example, a controller that includes only the <tt>:json</tt> renderer # name or names to `use_renderers`. For example, a controller that includes only
# (+_render_with_renderer_json+) might look like: # the `:json` renderer (`_render_with_renderer_json`) might look like:
# #
# class MetalRenderingController < ActionController::Metal # class MetalRenderingController < ActionController::Metal
# include AbstractController::Rendering # include AbstractController::Rendering
# include ActionController::Rendering # include ActionController::Rendering
# include ActionController::Renderers # include ActionController::Renderers
# #
# use_renderers :json # use_renderers :json
# #
# def show # def show
# render json: record # render json: record
# end
# end # end
# end
# #
# You must specify a +use_renderer+, else the +controller.renderer+ and # You must specify a `use_renderer`, else the `controller.renderer` and
# +controller._renderers+ will be <tt>nil</tt>, and the action will fail. # `controller._renderers` will be `nil`, and the action will fail.
def use_renderers(*args) def use_renderers(*args)
renderers = _renderers + args renderers = _renderers + args
self._renderers = renderers.freeze self._renderers = renderers.freeze
@ -132,11 +133,11 @@ def use_renderers(*args)
alias use_renderer use_renderers alias use_renderer use_renderers
end end
# Called by +render+ in AbstractController::Rendering # Called by `render` in AbstractController::Rendering which sets the return
# which sets the return value as the +response_body+. # value as the `response_body`.
# #
# If no renderer is found, +super+ returns control to # If no renderer is found, `super` returns control to
# <tt>ActionView::Rendering.render_to_body</tt>, if present. # `ActionView::Rendering.render_to_body`, if present.
def render_to_body(options) def render_to_body(options)
_render_to_body_with_renderer(options) || super _render_to_body_with_renderer(options) || super
end end

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
module ActionController module ActionController
module Rendering module Rendering
extend ActiveSupport::Concern extend ActiveSupport::Concern
@ -10,8 +12,8 @@ module ClassMethods
# Documentation at ActionController::Renderer#render # Documentation at ActionController::Renderer#render
delegate :render, to: :renderer delegate :render, to: :renderer
# Returns a renderer instance (inherited from ActionController::Renderer) # Returns a renderer instance (inherited from ActionController::Renderer) for
# for the controller. # the controller.
attr_reader :renderer attr_reader :renderer
def setup_renderer! # :nodoc: def setup_renderer! # :nodoc:
@ -24,137 +26,139 @@ def inherited(klass)
end end
end end
# Renders a template and assigns the result to +self.response_body+. # Renders a template and assigns the result to `self.response_body`.
# #
# If no rendering mode option is specified, the template will be derived # If no rendering mode option is specified, the template will be derived from
# from the first argument. # the first argument.
# #
# render "posts/show" # render "posts/show"
# # => renders app/views/posts/show.html.erb # # => renders app/views/posts/show.html.erb
# #
# # In a PostsController action... # # In a PostsController action...
# render :show # render :show
# # => renders app/views/posts/show.html.erb # # => renders app/views/posts/show.html.erb
# #
# If the first argument responds to +render_in+, the template will be # If the first argument responds to `render_in`, the template will be rendered
# rendered by calling +render_in+ with the current view context. # by calling `render_in` with the current view context.
# #
# class Greeting # class Greeting
# def render_in(view_context) # def render_in(view_context)
# view_context.render html: "<h1>Hello, World</h1>" # view_context.render html: "<h1>Hello, World</h1>"
# end
#
# def format
# :html
# end
# end # end
# #
# def format # render(Greeting.new)
# :html # # => "<h1>Hello, World</h1>"
# end
# end
# #
# render(Greeting.new) # render(renderable: Greeting.new)
# # => "<h1>Hello, World</h1>" # # => "<h1>Hello, World</h1>"
# #
# render(renderable: Greeting.new) # #### Rendering Mode
# # => "<h1>Hello, World</h1>"
# #
# ==== \Rendering Mode # `:partial`
# : See ActionView::PartialRenderer for details.
# #
# [+:partial+] # render partial: "posts/form", locals: { post: Post.new }
# See ActionView::PartialRenderer for details. # # => renders app/views/posts/_form.html.erb
# #
# render partial: "posts/form", locals: { post: Post.new } # `:file`
# # => renders app/views/posts/_form.html.erb # : Renders the contents of a file. This option should **not** be used with
# unsanitized user input.
# #
# [+:file+] # render file: "/path/to/some/file"
# Renders the contents of a file. This option should <b>not</b> be used # # => renders /path/to/some/file
# with unsanitized user input.
# #
# render file: "/path/to/some/file" # `:inline`
# # => renders /path/to/some/file # : Renders an ERB template string.
# #
# [+:inline+] # @name = "World"
# Renders an ERB template string. # render inline: "<h1>Hello, <%= @name %>!</h1>"
# # => renders "<h1>Hello, World!</h1>"
# #
# @name = "World" # `:body`
# render inline: "<h1>Hello, <%= @name %>!</h1>" # : Renders the provided text, and sets the content type as `text/plain`.
# # => renders "<h1>Hello, World!</h1>"
# #
# [+:body+] # render body: "Hello, World!"
# Renders the provided text, and sets the content type as +text/plain+. # # => renders "Hello, World!"
# #
# render body: "Hello, World!" # `:plain`
# # => renders "Hello, World!" # : Renders the provided text, and sets the content type as `text/plain`.
# #
# [+:plain+] # render plain: "Hello, World!"
# Renders the provided text, and sets the content type as +text/plain+. # # => renders "Hello, World!"
# #
# render plain: "Hello, World!" # `:html`
# # => renders "Hello, World!" # : Renders the provided HTML string, and sets the content type as
# `text/html`. If the string is not `html_safe?`, performs HTML escaping on
# the string before rendering.
# #
# [+:html+] # render html: "<h1>Hello, World!</h1>".html_safe
# Renders the provided HTML string, and sets the content type as +text/html+. # # => renders "<h1>Hello, World!</h1>"
# If the string is not +html_safe?+, performs HTML escaping on the string
# before rendering.
# #
# render html: "<h1>Hello, World!</h1>".html_safe # render html: "<h1>Hello, World!</h1>"
# # => renders "<h1>Hello, World!</h1>" # # => renders "&lt;h1&gt;Hello, World!&lt;/h1&gt;"
# #
# render html: "<h1>Hello, World!</h1>" # `:json`
# # => renders "&lt;h1&gt;Hello, World!&lt;/h1&gt;" # : Renders the provided object as JSON, and sets the content type as
# `application/json`. If the object is not a string, it will be converted to
# JSON by calling `to_json`.
# #
# [+:json+] # render json: { hello: "world" }
# Renders the provided object as JSON, and sets the content type as # # => renders "{\"hello\":\"world\"}"
# +application/json+. If the object is not a string, it will be converted
# to JSON by calling +to_json+.
# #
# render json: { hello: "world" } # `:renderable`
# # => renders "{\"hello\":\"world\"}" # : Renders the provided object by calling `render_in` with the current view
# context. The response format is determined by calling `format` on the
# renderable if it responds to `format`, falling back to `text/html` by
# default.
# #
# [+:renderable+] # render renderable: Greeting.new
# Renders the provided object by calling +render_in+ with the current view # # => renders "<h1>Hello, World</h1>"
# context. The response format is determined by calling +format+ on the
# renderable if it responds to +format+, falling back to +text/html+ by default.
# #
# render renderable: Greeting.new
# # => renders "<h1>Hello, World</h1>"
# #
# By default, when a rendering mode is specified, no layout template is # By default, when a rendering mode is specified, no layout template is
# rendered. # rendered.
# #
# ==== Options # #### Options
# #
# [+:assigns+] # `:assigns`
# Hash of instance variable assignments for the template. # : Hash of instance variable assignments for the template.
# #
# render inline: "<h1>Hello, <%= @name %>!</h1>", assigns: { name: "World" } # render inline: "<h1>Hello, <%= @name %>!</h1>", assigns: { name: "World" }
# # => renders "<h1>Hello, World!</h1>" # # => renders "<h1>Hello, World!</h1>"
# #
# [+:locals+] # `:locals`
# Hash of local variable assignments for the template. # : Hash of local variable assignments for the template.
# #
# render inline: "<h1>Hello, <%= name %>!</h1>", locals: { name: "World" } # render inline: "<h1>Hello, <%= name %>!</h1>", locals: { name: "World" }
# # => renders "<h1>Hello, World!</h1>" # # => renders "<h1>Hello, World!</h1>"
# #
# [+:layout+] # `:layout`
# The layout template to render. Can also be +false+ or +true+ to disable # : The layout template to render. Can also be `false` or `true` to disable or
# or (re)enable the default layout template. # (re)enable the default layout template.
# #
# render "posts/show", layout: "holiday" # render "posts/show", layout: "holiday"
# # => renders app/views/posts/show.html.erb with the app/views/layouts/holiday.html.erb layout # # => renders app/views/posts/show.html.erb with the app/views/layouts/holiday.html.erb layout
# #
# render "posts/show", layout: false # render "posts/show", layout: false
# # => renders app/views/posts/show.html.erb with no layout # # => renders app/views/posts/show.html.erb with no layout
# #
# render inline: "<h1>Hello, World!</h1>", layout: true # render inline: "<h1>Hello, World!</h1>", layout: true
# # => renders "<h1>Hello, World!</h1>" with the default layout # # => renders "<h1>Hello, World!</h1>" with the default layout
# #
# [+:status+] # `:status`
# The HTTP status code to send with the response. Can be specified as a # : The HTTP status code to send with the response. Can be specified as a
# number or as the status name in Symbol form. Defaults to 200. # number or as the status name in Symbol form. Defaults to 200.
# #
# render "posts/new", status: 422 # render "posts/new", status: 422
# # => renders app/views/posts/new.html.erb with HTTP status code 422 # # => renders app/views/posts/new.html.erb with HTTP status code 422
# #
# render "posts/new", status: :unprocessable_entity # render "posts/new", status: :unprocessable_entity
# # => renders app/views/posts/new.html.erb with HTTP status code 422 # # => renders app/views/posts/new.html.erb with HTTP status code 422
# #
#-- #--
# Check for double render errors and set the content_type after rendering. # Check for double render errors and set the content_type after rendering.
@ -164,7 +168,7 @@ def render(*args)
end end
# Similar to #render, but only returns the rendered template as a string, # Similar to #render, but only returns the rendered template as a string,
# instead of setting +self.response_body+. # instead of setting `self.response_body`.
#-- #--
# Override render_to_string because body can now be set to a Rack body. # Override render_to_string because body can now be set to a Rack body.
def render_to_string(*) def render_to_string(*)

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
require "rack/session/abstract/id" require "rack/session/abstract/id"
require "action_controller/metal/exceptions" require "action_controller/metal/exceptions"
require "active_support/security_utils" require "active_support/security_utils"
@ -11,51 +13,53 @@ class InvalidAuthenticityToken < ActionControllerError # :nodoc:
class InvalidCrossOriginRequest < ActionControllerError # :nodoc: class InvalidCrossOriginRequest < ActionControllerError # :nodoc:
end end
# = Action Controller Request Forgery Protection # # Action Controller Request Forgery Protection
# #
# Controller actions are protected from Cross-Site Request Forgery (CSRF) attacks # Controller actions are protected from Cross-Site Request Forgery (CSRF)
# by including a token in the rendered HTML for your application. This token is # attacks by including a token in the rendered HTML for your application. This
# stored as a random string in the session, to which an attacker does not have # token is stored as a random string in the session, to which an attacker does
# access. When a request reaches your application, \Rails verifies the received # not have access. When a request reaches your application, Rails verifies the
# token with the token in the session. All requests are checked except GET requests # received token with the token in the session. All requests are checked except
# as these should be idempotent. Keep in mind that all session-oriented requests # GET requests as these should be idempotent. Keep in mind that all
# are CSRF protected by default, including JavaScript and HTML requests. # session-oriented requests are CSRF protected by default, including JavaScript
# and HTML requests.
# #
# Since HTML and JavaScript requests are typically made from the browser, we # 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 # need to ensure to verify request authenticity for the web browser. We can use
# use session-oriented authentication for these types of requests, by using # session-oriented authentication for these types of requests, by using the
# the <tt>protect_from_forgery</tt> method in our controllers. # `protect_from_forgery` method in our controllers.
# #
# GET requests are not protected since they don't have side effects like writing # 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 # 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 # an exception: a third-party site can use a <script> tag to reference a
# URL on your site. When your JavaScript response loads on their site, it executes. # JavaScript URL on your site. When your JavaScript response loads on their
# With carefully crafted JavaScript on their end, sensitive data in your JavaScript # site, it executes. With carefully crafted JavaScript on their end, sensitive
# response may be extracted. To prevent this, only XmlHttpRequest (known as XHR or # data in your JavaScript response may be extracted. To prevent this, only
# Ajax) requests are allowed to make requests for JavaScript responses. # XmlHttpRequest (known as XHR or Ajax) requests are allowed to make requests
# for JavaScript responses.
# #
# Subclasses of ActionController::Base are protected by default with the # Subclasses of ActionController::Base are protected by default with the
# <tt>:exception</tt> strategy, which raises an # `:exception` strategy, which raises an
# ActionController::InvalidAuthenticityToken error on unverified requests. # ActionController::InvalidAuthenticityToken error on unverified requests.
# #
# APIs may want to disable this behavior since they are typically designed to be # APIs may want to disable this behavior since they are typically designed to be
# state-less: that is, the request API client handles the session instead of \Rails. # state-less: that is, the request API client handles the session instead of
# One way to achieve this is to use the <tt>:null_session</tt> strategy instead, # Rails. One way to achieve this is to use the `:null_session` strategy instead,
# which allows unverified requests to be handled, but with an empty session: # which allows unverified requests to be handled, but with an empty session:
# #
# class ApplicationController < ActionController::Base # class ApplicationController < ActionController::Base
# protect_from_forgery with: :null_session # protect_from_forgery with: :null_session
# end # end
# #
# Note that API only applications don't include this module or a session middleware # Note that API only applications don't include this module or a session
# by default, and so don't require CSRF protection to be configured. # middleware by default, and so don't require CSRF protection to be configured.
# #
# The token parameter is named <tt>authenticity_token</tt> by default. The name and # The token parameter is named `authenticity_token` by default. The name and
# value of this token must be added to every layout that renders forms by including # value of this token must be added to every layout that renders forms by
# <tt>csrf_meta_tags</tt> in the HTML +head+. # including `csrf_meta_tags` in the HTML `head`.
# #
# Learn more about CSRF attacks and securing your application in the # Learn more about CSRF attacks and securing your application in the [Ruby on
# {Ruby on Rails Security Guide}[https://guides.rubyonrails.org/security.html]. # Rails Security Guide](https://guides.rubyonrails.org/security.html).
module RequestForgeryProtection module RequestForgeryProtection
CSRF_TOKEN = "action_controller.csrf_token" CSRF_TOKEN = "action_controller.csrf_token"
@ -65,8 +69,8 @@ module RequestForgeryProtection
include AbstractController::Callbacks include AbstractController::Callbacks
included do included do
# Sets the token parameter name for RequestForgery. Calling +protect_from_forgery+ # Sets the token parameter name for RequestForgery. Calling
# sets it to <tt>:authenticity_token</tt> by default. # `protect_from_forgery` sets it to `:authenticity_token` by default.
config_accessor :request_forgery_protection_token config_accessor :request_forgery_protection_token
self.request_forgery_protection_token ||= :authenticity_token self.request_forgery_protection_token ||= :authenticity_token
@ -74,7 +78,8 @@ module RequestForgeryProtection
config_accessor :forgery_protection_strategy config_accessor :forgery_protection_strategy
self.forgery_protection_strategy = nil self.forgery_protection_strategy = nil
# Controls whether request forgery protection is turned on or not. Turned off by default only in test mode. # Controls whether request forgery protection is turned on or not. Turned off by
# default only in test mode.
config_accessor :allow_forgery_protection config_accessor :allow_forgery_protection
self.allow_forgery_protection = true if allow_forgery_protection.nil? self.allow_forgery_protection = true if allow_forgery_protection.nil?
@ -99,79 +104,96 @@ module RequestForgeryProtection
end end
module ClassMethods module ClassMethods
# Turn on request forgery protection. Bear in mind that GET and HEAD requests are not checked. # Turn on request forgery protection. Bear in mind that GET and HEAD requests
# are not checked.
# #
# class ApplicationController < ActionController::Base # class ApplicationController < ActionController::Base
# protect_from_forgery # protect_from_forgery
# end # end
# #
# class FooController < ApplicationController # class FooController < ApplicationController
# protect_from_forgery except: :index # protect_from_forgery except: :index
# end # end
# #
# You can disable forgery protection on a controller using skip_forgery_protection: # You can disable forgery protection on a controller using
# skip_forgery_protection:
# #
# class BarController < ApplicationController # class BarController < ApplicationController
# skip_forgery_protection # skip_forgery_protection
# end # end
# #
# Valid Options: # Valid Options:
# #
# * <tt>:only</tt> / <tt>:except</tt> - Only apply forgery protection to a subset of actions. For example <tt>only: [ :create, :create_all ]</tt>. # * `:only` / `:except` - Only apply forgery protection to a subset of
# * <tt>:if</tt> / <tt>:unless</tt> - Turn off the forgery protection entirely depending on the passed Proc or method reference. # actions. For example `only: [ :create, :create_all ]`.
# * <tt>:prepend</tt> - By default, the verification of the authentication token will be added at the position of the # * `:if` / `:unless` - Turn off the forgery protection entirely depending on
# protect_from_forgery call in your application. This means any callbacks added before are run first. This is useful # the passed Proc or method reference.
# when you want your forgery protection to depend on other callbacks, like authentication methods (Oauth vs Cookie auth). # * `:prepend` - 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 `prepend: true`.
# * `:with` - Set the method to handle unverified request. Note if
# `default_protect_from_forgery` is true, Rails call protect_from_forgery
# with `with :exception`.
# #
# 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.
# Note if <tt>default_protect_from_forgery</tt> is true, Rails call protect_from_forgery with <tt>with :exception</tt>.
# #
# Built-in unverified request handling methods are: # Built-in unverified request handling methods are:
# * <tt>:exception</tt> - Raises ActionController::InvalidAuthenticityToken exception. # * `:exception` - Raises ActionController::InvalidAuthenticityToken
# * <tt>:reset_session</tt> - Resets the session. # exception.
# * <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. # * `:reset_session` - Resets the session.
# * `:null_session` - Provides an empty session during request but doesn't
# reset it completely. Used as default if `:with` option is not specified.
# #
# You can also implement custom strategy classes for unverified request handling:
# #
# class CustomStrategy # You can also implement custom strategy classes for unverified request
# def initialize(controller) # handling:
# @controller = controller
# end
# #
# def handle_unverified_request # class CustomStrategy
# # Custom behavior for unverfied request # def initialize(controller)
# end # @controller = controller
# end # end
#
# def handle_unverified_request
# # Custom behavior for unverfied request
# end
# end
#
# class ApplicationController < ActionController::Base
# protect_from_forgery with: CustomStrategy
# end
#
# * `:store` - Set the strategy to store and retrieve CSRF tokens.
# #
# class ApplicationController < ActionController::Base
# protect_from_forgery with: CustomStrategy
# end
# * <tt>:store</tt> - Set the strategy to store and retrieve CSRF tokens.
# #
# Built-in session token strategies are: # Built-in session token strategies are:
# * <tt>:session</tt> - Store the CSRF token in the session. Used as default if <tt>:store</tt> option is not specified. # * `:session` - Store the CSRF token in the session. Used as default if
# * <tt>:cookie</tt> - Store the CSRF token in an encrypted cookie. # `:store` option is not specified.
# * `:cookie` - Store the CSRF token in an encrypted cookie.
#
# #
# You can also implement custom strategy classes for CSRF token storage: # You can also implement custom strategy classes for CSRF token storage:
# #
# class CustomStore # class CustomStore
# def fetch(request) # def fetch(request)
# # Return the token from a custom location # # Return the token from a custom location
# end
#
# def store(request, csrf_token)
# # Store the token in a custom location
# end
#
# def reset(request)
# # Delete the stored session token
# end
# end # end
# #
# def store(request, csrf_token) # class ApplicationController < ActionController::Base
# # Store the token in a custom location # protect_from_forgery store: CustomStore.new
# end # end
#
# def reset(request)
# # Delete the stored session token
# end
# end
#
# class ApplicationController < ActionController::Base
# protect_from_forgery store: CustomStore.new
# end
def protect_from_forgery(options = {}) def protect_from_forgery(options = {})
options = options.reverse_merge(prepend: false) options = options.reverse_merge(prepend: false)
@ -186,9 +208,9 @@ def protect_from_forgery(options = {})
# Turn off request forgery protection. This is a wrapper for: # Turn off request forgery protection. This is a wrapper for:
# #
# skip_before_action :verify_authenticity_token # skip_before_action :verify_authenticity_token
# #
# See +skip_before_action+ for allowed options. # See `skip_before_action` for allowed options.
def skip_forgery_protection(options = {}) def skip_forgery_protection(options = {})
skip_before_action :verify_authenticity_token, options.reverse_merge(raise: false) skip_before_action :verify_authenticity_token, options.reverse_merge(raise: false)
end end
@ -232,7 +254,8 @@ def initialize(controller)
@controller = controller @controller = controller
end end
# This is the method that defines the application behavior when a request is found to be unverified. # This is the method that defines the application behavior when a request is
# found to be unverified.
def handle_unverified_request def handle_unverified_request
request = @controller.request request = @controller.request
request.session = NullSessionHash.new(request) request.session = NullSessionHash.new(request)
@ -354,16 +377,15 @@ def commit_csrf_token(request) # :doc:
end end
private private
# The actual before_action that is used to verify the CSRF token. # The actual before_action that is used to verify the CSRF token. Don't override
# Don't override this directly. Provide your own forgery protection # this directly. Provide your own forgery protection strategy instead. If you
# strategy instead. If you override, you'll disable same-origin # override, you'll disable same-origin `<script>` verification.
# <tt><script></tt> verification.
# #
# Lean on the protect_from_forgery declaration to mark which actions are # Lean on the protect_from_forgery declaration to mark which actions are due for
# due for same-origin request verification. If protect_from_forgery is # same-origin request verification. If protect_from_forgery is enabled on an
# enabled on an action, this before_action flags its after_action to # action, this before_action flags its after_action to verify that JavaScript
# verify that JavaScript responses are for XHR requests, ensuring they # responses are for XHR requests, ensuring they follow the browser's same-origin
# follow the browser's same-origin policy. # policy.
def verify_authenticity_token # :doc: def verify_authenticity_token # :doc:
mark_for_same_origin_verification! mark_for_same_origin_verification!
@ -400,9 +422,9 @@ def unverified_request_warning_message # :nodoc:
private_constant :CROSS_ORIGIN_JAVASCRIPT_WARNING private_constant :CROSS_ORIGIN_JAVASCRIPT_WARNING
# :startdoc: # :startdoc:
# If +verify_authenticity_token+ was run (indicating that we have # If `verify_authenticity_token` was run (indicating that we have
# forgery protection enabled for this request) then also verify that # forgery protection enabled for this request) then also verify that we aren't
# we aren't serving an unauthorized cross-origin response. # serving an unauthorized cross-origin response.
def verify_same_origin_request # :doc: def verify_same_origin_request # :doc:
if marked_for_same_origin_verification? && non_xhr_javascript_response? if marked_for_same_origin_verification? && non_xhr_javascript_response?
if logger && log_warning_on_csrf_failure if logger && log_warning_on_csrf_failure
@ -417,8 +439,8 @@ def mark_for_same_origin_verification! # :doc:
@_marked_for_same_origin_verification = request.get? @_marked_for_same_origin_verification = request.get?
end end
# If the +verify_authenticity_token+ before_action ran, verify that # If the `verify_authenticity_token` before_action ran, verify that JavaScript
# JavaScript responses are only served to same-origin GET requests. # responses are only served to same-origin GET requests.
def marked_for_same_origin_verification? # :doc: def marked_for_same_origin_verification? # :doc:
@_marked_for_same_origin_verification ||= false @_marked_for_same_origin_verification ||= false
end end
@ -432,9 +454,11 @@ def non_xhr_javascript_response? # :doc:
# Returns true or false if a request is verified. Checks: # Returns true or false if a request is verified. Checks:
# #
# * Is it a GET or HEAD request? GETs should be safe and idempotent # * 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 form_authenticity_token match the given token value from the
# * Does the +X-CSRF-Token+ header match the form_authenticity_token? # params?
# * Does the `X-CSRF-Token` header match the form_authenticity_token?
#
def verified_request? # :doc: def verified_request? # :doc:
!protect_against_forgery? || request.get? || request.head? || !protect_against_forgery? || request.get? || request.head? ||
(valid_request_origin? && any_authenticity_token_valid?) (valid_request_origin? && any_authenticity_token_valid?)
@ -457,9 +481,8 @@ def form_authenticity_token(form_options: {}) # :doc:
masked_authenticity_token(form_options: form_options) masked_authenticity_token(form_options: form_options)
end end
# Creates a masked version of the authenticity token that varies # Creates a masked version of the authenticity token that varies on each
# on each request. The masking is used to mitigate SSL attacks # request. The masking is used to mitigate SSL attacks like BREACH.
# like BREACH.
def masked_authenticity_token(form_options: {}) def masked_authenticity_token(form_options: {})
action, method = form_options.values_at(:action, :method) action, method = form_options.values_at(:action, :method)
@ -473,9 +496,8 @@ def masked_authenticity_token(form_options: {})
mask_token(raw_token) mask_token(raw_token)
end end
# Checks the client's masked token to see if it matches the # Checks the client's masked token to see if it matches the session token.
# session token. Essentially the inverse of # Essentially the inverse of `masked_authenticity_token`.
# +masked_authenticity_token+.
def valid_authenticity_token?(session, encoded_masked_token) # :doc: def valid_authenticity_token?(session, encoded_masked_token) # :doc:
if encoded_masked_token.nil? || encoded_masked_token.empty? || !encoded_masked_token.is_a?(String) if encoded_masked_token.nil? || encoded_masked_token.empty? || !encoded_masked_token.is_a?(String)
return false return false
@ -487,14 +509,12 @@ def valid_authenticity_token?(session, encoded_masked_token) # :doc:
return false return false
end end
# See if it's actually a masked token or not. In order to # See if it's actually a masked token or not. In order to deploy this code, we
# deploy this code, we should be able to handle any unmasked # should be able to handle any unmasked tokens that we've issued without error.
# tokens that we've issued without error.
if masked_token.length == AUTHENTICITY_TOKEN_LENGTH if masked_token.length == AUTHENTICITY_TOKEN_LENGTH
# This is actually an unmasked token. This is expected if # This is actually an unmasked token. This is expected if you have just upgraded
# you have just upgraded to masked tokens, but should stop # to masked tokens, but should stop happening shortly after installing this gem.
# happening shortly after installing this gem.
compare_with_real_token masked_token compare_with_real_token masked_token
elsif masked_token.length == AUTHENTICITY_TOKEN_LENGTH * 2 elsif masked_token.length == AUTHENTICITY_TOKEN_LENGTH * 2
@ -509,8 +529,7 @@ def valid_authenticity_token?(session, encoded_masked_token) # :doc:
end end
def unmask_token(masked_token) # :doc: def unmask_token(masked_token) # :doc:
# Split the token into the one-time pad and the encrypted # Split the token into the one-time pad and the encrypted value and decrypt it.
# value and decrypt it.
one_time_pad = masked_token[0...AUTHENTICITY_TOKEN_LENGTH] one_time_pad = masked_token[0...AUTHENTICITY_TOKEN_LENGTH]
encrypted_csrf_token = masked_token[AUTHENTICITY_TOKEN_LENGTH..-1] encrypted_csrf_token = masked_token[AUTHENTICITY_TOKEN_LENGTH..-1]
xor_byte_strings(one_time_pad, encrypted_csrf_token) xor_byte_strings(one_time_pad, encrypted_csrf_token)
@ -602,8 +621,8 @@ def protect_against_forgery? # :doc:
Rails.application.config.action_controller.forgery_protection_origin_check setting. Rails.application.config.action_controller.forgery_protection_origin_check setting.
MSG MSG
# Checks if the request originated from the same origin by looking at the # Checks if the request originated from the same origin by looking at the Origin
# Origin header. # header.
def valid_request_origin? # :doc: def valid_request_origin? # :doc:
if forgery_protection_origin_check if forgery_protection_origin_check
# We accept blank origin headers because some user agents don't send it. # We accept blank origin headers because some user agents don't send it.

@ -1,21 +1,23 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
module ActionController # :nodoc: module ActionController # :nodoc:
# = Action Controller \Rescue # # Action Controller Rescue
# #
# This module is responsible for providing # This module is responsible for providing
# {rescue_from}[rdoc-ref:ActiveSupport::Rescuable::ClassMethods#rescue_from] # [rescue_from](rdoc-ref:ActiveSupport::Rescuable::ClassMethods#rescue_from) to
# to controllers, wrapping actions to handle configured errors, and # controllers, wrapping actions to handle configured errors, and configuring
# configuring when detailed exceptions must be shown. # when detailed exceptions must be shown.
module Rescue module Rescue
extend ActiveSupport::Concern extend ActiveSupport::Concern
include ActiveSupport::Rescuable include ActiveSupport::Rescuable
# Override this method if you want to customize when detailed # Override this method if you want to customize when detailed exceptions must be
# exceptions must be shown. This method is only called when # shown. This method is only called when `consider_all_requests_local` is
# +consider_all_requests_local+ is +false+. By default, it returns # `false`. By default, it returns `false`, but someone may set it to
# +false+, but someone may set it to <tt>request.local?</tt> so local # `request.local?` so local requests in production still show the detailed
# requests in production still show the detailed exception pages. # exception pages.
def show_detailed_exceptions? def show_detailed_exceptions?
false false
end end

@ -1,208 +1,210 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
module ActionController # :nodoc: module ActionController # :nodoc:
# = Action Controller \Streaming # # Action Controller Streaming
# #
# Allows views to be streamed back to the client as they are rendered. # Allows views to be streamed back to the client as they are rendered.
# #
# By default, \Rails renders views by first rendering the template # By default, Rails renders views by first rendering the template and then the
# and then the layout. The response is sent to the client after the whole # layout. The response is sent to the client after the whole template is
# template is rendered, all queries are made, and the layout is processed. # rendered, all queries are made, and the layout is processed.
# #
# \Streaming inverts the rendering flow by rendering the layout first and # Streaming inverts the rendering flow by rendering the layout first and
# subsequently each part of the layout as they are processed. This allows the # subsequently 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 # header of the HTML (which is usually in the layout) to be streamed back to
# to client very quickly, enabling JavaScripts and stylesheets to be loaded # client very quickly, enabling JavaScripts and stylesheets to be loaded earlier
# earlier than usual. # than usual.
# #
# Several Rack middlewares may not work and you need to be careful when streaming. # Several Rack middlewares may not work and you need to be careful when
# This is covered in more detail below, see the Streaming@Middlewares section. # streaming. This is covered in more detail below, see the Streaming@Middlewares
# section.
# #
# \Streaming can be added to a given template easily, all you need to do is # Streaming can be added to a given template easily, all you need to do is to
# to pass the +:stream+ option to +render+. # pass the `:stream` option to `render`.
# #
# class PostsController # class PostsController
# def index # def index
# @posts = Post.all # @posts = Post.all
# render stream: true # render stream: true
# end
# end # end
# end
# #
# == When to use streaming # ## When to use streaming
# #
# \Streaming may be considered to be overkill for lightweight actions like # Streaming may be considered to be overkill for lightweight actions like `new`
# +new+ or +edit+. The real benefit of streaming is on expensive actions # or `edit`. The real benefit of streaming is on expensive actions that, for
# that, for example, do a lot of queries on the database. # example, do a lot of queries on the database.
# #
# In such actions, you want to delay queries execution as much as you can. # In such actions, you want to delay queries execution as much as you can. For
# For example, imagine the following +dashboard+ action: # example, imagine the following `dashboard` action:
# #
# def dashboard # def dashboard
# @posts = Post.all # @posts = Post.all
# @pages = Page.all # @pages = Page.all
# @articles = Article.all # @articles = Article.all
# end # end
# #
# Most of the queries here are happening in the controller. In order to benefit # Most of the queries here are happening in the controller. In order to benefit
# from streaming you would want to rewrite it as: # from streaming you would want to rewrite it as:
# #
# def dashboard # def dashboard
# # Allow lazy execution of the queries # # Allow lazy execution of the queries
# @posts = Post.all # @posts = Post.all
# @pages = Page.all # @pages = Page.all
# @articles = Article.all # @articles = Article.all
# render stream: true # render stream: true
# end # end
# #
# Notice that +:stream+ only works with templates. \Rendering +:json+ # Notice that `:stream` only works with templates. Rendering `:json` or `:xml`
# or +:xml+ with +:stream+ won't work. # with `:stream` won't work.
# #
# == Communication between layout and template # ## Communication between layout and template
# #
# When streaming, rendering happens top-down instead of inside-out. # When streaming, rendering happens top-down instead of inside-out. Rails starts
# \Rails starts with the layout, and the template is rendered later, # with the layout, and the template is rendered later, when its `yield` is
# when its +yield+ is reached. # reached.
# #
# This means that, if your application currently relies on instance # This means that, if your application currently relies on instance variables
# variables set in the template to be used in the layout, they won't # set in the template to be used in the layout, they won't work once you move to
# work once you move to streaming. The proper way to communicate # streaming. The proper way to communicate between layout and template,
# between layout and template, regardless of whether you use streaming # regardless of whether you use streaming or not, is by using `content_for`,
# or not, is by using +content_for+, +provide+, and +yield+. # `provide`, and `yield`.
# #
# Take a simple example where the layout expects the template to tell # Take a simple example where the layout expects the template to tell which
# which title to use: # title to use:
# #
# <html> # <html>
# <head><title><%= yield :title %></title></head> # <head><title><%= yield :title %></title></head>
# <body><%= yield %></body> # <body><%= yield %></body>
# </html> # </html>
# #
# You would use +content_for+ in your template to specify the title: # You would use `content_for` in your template to specify the title:
# #
# <%= content_for :title, "Main" %> # <%= content_for :title, "Main" %>
# Hello # Hello
# #
# And the final result would be: # And the final result would be:
# #
# <html> # <html>
# <head><title>Main</title></head> # <head><title>Main</title></head>
# <body>Hello</body> # <body>Hello</body>
# </html> # </html>
# #
# However, if +content_for+ is called several times, the final result # However, if `content_for` is called several times, the final result would have
# would have all calls concatenated. For instance, if we have the following # all calls concatenated. For instance, if we have the following template:
# template:
# #
# <%= content_for :title, "Main" %> # <%= content_for :title, "Main" %>
# Hello # Hello
# <%= content_for :title, " page" %> # <%= content_for :title, " page" %>
# #
# The final result would be: # The final result would be:
# #
# <html> # <html>
# <head><title>Main page</title></head> # <head><title>Main page</title></head>
# <body>Hello</body> # <body>Hello</body>
# </html> # </html>
# #
# This means that, if you have <code>yield :title</code> in your layout # This means that, if you have `yield :title` in your layout and you want to use
# and you want to use streaming, you would have to render the whole template # streaming, you would have to render the whole template (and eventually trigger
# (and eventually trigger all queries) before streaming the title and all # all queries) before streaming the title and all assets, which defeats the
# assets, which defeats the purpose of streaming. Alternatively, you can use # purpose of streaming. Alternatively, you can use a helper called `provide`
# a helper called +provide+ that does the same as +content_for+ but tells the # that does the same as `content_for` but tells the layout to stop searching for
# layout to stop searching for other entries and continue rendering. # other entries and continue rendering.
# #
# For instance, the template above using +provide+ would be: # For instance, the template above using `provide` would be:
# #
# <%= provide :title, "Main" %> # <%= provide :title, "Main" %>
# Hello # Hello
# <%= content_for :title, " page" %> # <%= content_for :title, " page" %>
# #
# Resulting in: # Resulting in:
# #
# <html> # <html>
# <head><title>Main</title></head> # <head><title>Main</title></head>
# <body>Hello</body> # <body>Hello</body>
# </html> # </html>
# #
# That said, when streaming, you need to properly check your templates # That said, when streaming, you need to properly check your templates and
# and choose when to use +provide+ and +content_for+. # choose when to use `provide` and `content_for`.
# #
# See also ActionView::Helpers::CaptureHelper for more information. # See also ActionView::Helpers::CaptureHelper for more information.
# #
# == Headers, cookies, session, and flash # ## Headers, cookies, session, and flash
# #
# When streaming, the HTTP headers are sent to the client right before # When streaming, the HTTP headers are sent to the client right before it
# it renders the first line. This means that, modifying headers, cookies, # renders the first line. This means that, modifying headers, cookies, session
# session or flash after the template starts rendering will not propagate # or flash after the template starts rendering will not propagate to the client.
# to the client.
# #
# == Middlewares # ## Middlewares
# #
# Middlewares that need to manipulate the body won't work with streaming. # Middlewares that need to manipulate the body won't work with streaming. You
# You should disable those middlewares whenever streaming in development # should disable those middlewares whenever streaming in development or
# or production. For instance, +Rack::Bug+ won't work when streaming as it # production. For instance, `Rack::Bug` won't work when streaming as it needs to
# needs to inject contents in the HTML body. # inject contents in the HTML body.
# #
# Also +Rack::Cache+ won't work with streaming as it does not support # Also `Rack::Cache` won't work with streaming as it does not support streaming
# streaming bodies yet. Whenever streaming +Cache-Control+ is automatically # bodies yet. Whenever streaming `Cache-Control` is automatically set to
# set to "no-cache". # "no-cache".
# #
# == Errors # ## Errors
# #
# When it comes to streaming, exceptions get a bit more complicated. This # When it comes to streaming, exceptions get a bit more complicated. This
# happens because part of the template was already rendered and streamed to # happens because part of the template was already rendered and streamed to the
# the client, making it impossible to render a whole exception page. # client, making it impossible to render a whole exception page.
# #
# Currently, when an exception happens in development or production, \Rails # Currently, when an exception happens in development or production, Rails will
# will automatically stream to the client: # automatically stream to the client:
# #
# "><script>window.location = "/500.html"</script></html> # "><script>window.location = "/500.html"</script></html>
# #
# The first two characters (<tt>"></tt>) are required in case the exception # The first two characters (`">`) are required in case the exception happens
# happens while rendering attributes for a given tag. You can check the real # while rendering attributes for a given tag. You can check the real cause for
# cause for the exception in your logger. # the exception in your logger.
# #
# == Web server support # ## Web server support
# #
# Not all web servers support streaming out-of-the-box. You need to check # Not all web servers support streaming out-of-the-box. You need to check the
# the instructions for each of them. # instructions for each of them.
# #
# ==== Unicorn # #### Unicorn
# #
# Unicorn supports streaming but it needs to be configured. For this, you # Unicorn supports streaming but it needs to be configured. For this, you need
# need to create a config file as follow: # to create a config file as follow:
# #
# # unicorn.config.rb # # unicorn.config.rb
# listen 3000, tcp_nopush: false # listen 3000, tcp_nopush: false
# #
# And use it on initialization: # And use it on initialization:
# #
# unicorn_rails --config-file unicorn.config.rb # unicorn_rails --config-file unicorn.config.rb
# #
# You may also want to configure other parameters like <tt>:tcp_nodelay</tt>. # You may also want to configure other parameters like `:tcp_nodelay`.
# #
# For more information, please check the # For more information, please check the
# {documentation}[https://bogomips.org/unicorn/Unicorn/Configurator.html#method-i-listen]. # [documentation](https://bogomips.org/unicorn/Unicorn/Configurator.html#method-
# i-listen).
# #
# If you are using Unicorn with NGINX, you may need to tweak NGINX. # If you are using Unicorn with NGINX, you may need to tweak NGINX. Streaming
# \Streaming should work out of the box on Rainbows. # should work out of the box on Rainbows.
# #
# ==== Passenger # #### Passenger
# #
# Phusion Passenger with NGINX, offers two streaming mechanisms out of the box. # Phusion Passenger with NGINX, offers two streaming mechanisms out of the box.
# #
# 1. NGINX response buffering mechanism which is dependent on the value of # 1. NGINX response buffering mechanism which is dependent on the value of
# +passenger_buffer_response+ option (default is "off"). # `passenger_buffer_response` option (default is "off").
# 2. Passenger buffering system which is always 'on' irrespective of the value # 2. Passenger buffering system which is always 'on' irrespective of the value
# of +passenger_buffer_response+. # of `passenger_buffer_response`.
# #
# When +passenger_buffer_response+ is turned "on", then streaming would be
# done at the NGINX level which waits until the application is done sending
# the response back to the client.
# #
# For more information, please check the # When `passenger_buffer_response` is turned "on", then streaming would be done
# {documentation}[https://www.phusionpassenger.com/docs/references/config_reference/nginx/#passenger_buffer_response]. # at the NGINX level which waits until the application is done sending the
# response back to the client.
# #
# For more information, please check the [documentation]
# (https://www.phusionpassenger.com/docs/references/config_reference/nginx/#passenger_buffer_response).
module Streaming module Streaming
class Body # :nodoc: class Body # :nodoc:
TERM = "\r\n" TERM = "\r\n"
@ -213,8 +215,8 @@ def initialize(body)
@body = body @body = body
end end
# For each element yielded by the response body, yield # For each element yielded by the response body, yield the element in chunked
# the element in chunked encoding. # encoding.
def each(&block) def each(&block)
term = TERM term = TERM
@body.each do |chunk| @body.each do |chunk|
@ -248,7 +250,7 @@ def _process_options(options)
end end
end end
# Call render_body if we are streaming instead of usual +render+. # Call render_body if we are streaming instead of usual `render`.
def _render_template(options) def _render_template(options)
if options.delete(:stream) if options.delete(:stream)
Body.new view_renderer.render_body(view_context, options) Body.new view_renderer.render_body(view_context, options)

File diff suppressed because it is too large Load Diff

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
module ActionController module ActionController
module Testing module Testing
# Behavior specific to functional tests # Behavior specific to functional tests

@ -1,27 +1,29 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
module ActionController module ActionController
# = Action Controller \UrlFor # # Action Controller UrlFor
# #
# Includes +url_for+ into the host class. The class has to provide a +RouteSet+ by implementing # Includes `url_for` into the host class. The class has to provide a `RouteSet`
# the <tt>_routes</tt> method. Otherwise, an exception will be raised. # by implementing the `_routes` method. Otherwise, an exception will be raised.
# #
# In addition to AbstractController::UrlFor, this module accesses the HTTP layer to define # In addition to AbstractController::UrlFor, this module accesses the HTTP layer
# URL options like the +host+. In order to do so, this module requires the host class # to define URL options like the `host`. In order to do so, this module requires
# to implement +env+ which needs to be Rack-compatible, and +request+ which # the host class to implement `env` which needs to be Rack-compatible, and
# returns an ActionDispatch::Request instance. # `request` which returns an ActionDispatch::Request instance.
# #
# class RootUrl # class RootUrl
# include ActionController::UrlFor # include ActionController::UrlFor
# include Rails.application.routes.url_helpers # include Rails.application.routes.url_helpers
# #
# delegate :env, :request, to: :controller # delegate :env, :request, to: :controller
# #
# def initialize(controller) # def initialize(controller)
# @controller = controller # @controller = controller
# @url = root_path # named route from the application. # @url = root_path # named route from the application.
# end
# end # end
# end
module UrlFor module UrlFor
extend ActiveSupport::Concern extend ActiveSupport::Concern

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
require "rails" require "rails"
require "action_controller" require "action_controller"
require "action_dispatch/railtie" require "action_dispatch/railtie"

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
module ActionController module ActionController
module Railties module Railties
module Helpers module Helpers

@ -1,25 +1,28 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
module ActionController module ActionController
# = Action Controller \Renderer # # Action Controller Renderer
# #
# ActionController::Renderer allows you to render arbitrary templates without # ActionController::Renderer allows you to render arbitrary templates without
# being inside a controller action. # being inside a controller action.
# #
# You can get a renderer instance by calling +renderer+ on a controller class: # You can get a renderer instance by calling `renderer` on a controller class:
# #
# ApplicationController.renderer # ApplicationController.renderer
# PostsController.renderer # PostsController.renderer
# #
# and render a template by calling the #render method: # and render a template by calling the #render method:
# #
# ApplicationController.renderer.render template: "posts/show", assigns: { post: Post.first } # ApplicationController.renderer.render template: "posts/show", assigns: { post: Post.first }
# PostsController.renderer.render :show, assigns: { post: Post.first } # PostsController.renderer.render :show, assigns: { post: Post.first }
# #
# As a shortcut, you can also call +render+ directly on the controller class itself: # As a shortcut, you can also call `render` directly on the controller class
# itself:
# #
# ApplicationController.render template: "posts/show", assigns: { post: Post.first } # ApplicationController.render template: "posts/show", assigns: { post: Post.first }
# PostsController.render :show, assigns: { post: Post.first } # PostsController.render :show, assigns: { post: Post.first }
# #
class Renderer class Renderer
attr_reader :controller attr_reader :controller
@ -64,45 +67,47 @@ def self.for(controller, env = nil, defaults = DEFAULTS)
# Creates a new renderer using the same controller, but with a new Rack env. # Creates a new renderer using the same controller, but with a new Rack env.
# #
# ApplicationController.renderer.new(method: "post") # ApplicationController.renderer.new(method: "post")
# #
def new(env = nil) def new(env = nil)
self.class.new controller, env, @defaults self.class.new controller, env, @defaults
end end
# Creates a new renderer using the same controller, but with the given # Creates a new renderer using the same controller, but with the given defaults
# defaults merged on top of the previous defaults. # merged on top of the previous defaults.
def with_defaults(defaults) def with_defaults(defaults)
self.class.new controller, @env, @defaults.merge(defaults) self.class.new controller, @env, @defaults.merge(defaults)
end end
# Initializes a new Renderer. # Initializes a new Renderer.
# #
# ==== Parameters # #### Parameters
# #
# * +controller+ - The controller class to instantiate for rendering. # * `controller` - The controller class to instantiate for rendering.
# * +env+ - The Rack env to use for mocking a request when rendering. # * `env` - The Rack env to use for mocking a request when rendering. Entries
# Entries can be typical Rack env keys and values, or they can be any of # can be typical Rack env keys and values, or they can be any of the
# the following, which will be converted appropriately: # following, which will be converted appropriately:
# * +:http_host+ - The HTTP host for the incoming request. Converts to # * `:http_host` - The HTTP host for the incoming request. Converts to
# Rack's +HTTP_HOST+. # Rack's `HTTP_HOST`.
# * +:https+ - Boolean indicating whether the incoming request uses HTTPS. # * `:https` - Boolean indicating whether the incoming request uses HTTPS.
# Converts to Rack's +HTTPS+. # Converts to Rack's `HTTPS`.
# * +:method+ - The HTTP method for the incoming request, case-insensitive. # * `:method` - The HTTP method for the incoming request,
# Converts to Rack's +REQUEST_METHOD+. # case-insensitive. Converts to Rack's `REQUEST_METHOD`.
# * +:script_name+ - The portion of the incoming request's URL path that # * `:script_name` - The portion of the incoming request's URL path that
# corresponds to the application. Converts to Rack's +SCRIPT_NAME+. # corresponds to the application. Converts to Rack's `SCRIPT_NAME`.
# * +:input+ - The input stream. Converts to Rack's +rack.input+. # * `:input` - The input stream. Converts to Rack's `rack.input`.
# * +defaults+ - Default values for the Rack env. Entries are specified in
# the same format as +env+. +env+ will be merged on top of these values.
# +defaults+ will be retained when calling #new on a renderer instance.
# #
# If no +http_host+ is specified, the env HTTP host will be derived from the # * `defaults` - Default values for the Rack env. Entries are specified in the
# routes' +default_url_options+. In this case, the +https+ boolean and the # same format as `env`. `env` will be merged on top of these values.
# +script_name+ will also be derived from +default_url_options+ if they were # `defaults` will be retained when calling #new on a renderer instance.
# not specified. Additionally, the +https+ boolean will fall back to #
# +Rails.application.config.force_ssl+ if +default_url_options+ does not #
# specify a +protocol+. # If no `http_host` is specified, the env HTTP host will be derived from the
# routes' `default_url_options`. In this case, the `https` boolean and the
# `script_name` will also be derived from `default_url_options` if they were not
# specified. Additionally, the `https` boolean will fall back to
# `Rails.application.config.force_ssl` if `default_url_options` does not specify
# a `protocol`.
def initialize(controller, env, defaults) def initialize(controller, env, defaults)
@controller = controller @controller = controller
@defaults = defaults @defaults = defaults
@ -119,7 +124,8 @@ def defaults
@defaults @defaults
end end
# Renders a template to a string, just like ActionController::Rendering#render_to_string. # Renders a template to a string, just like
# ActionController::Rendering#render_to_string.
def render(*args) def render(*args)
request = ActionDispatch::Request.new(env_for_request) request = ActionDispatch::Request.new(env_for_request)
request.routes = controller._routes request.routes = controller._routes

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
module ActionController module ActionController
module TemplateAssertions # :nodoc: module TemplateAssertions # :nodoc:
def assert_template(options = {}, message = nil) def assert_template(options = {}, message = nil)

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
require "rack/session/abstract/id" require "rack/session/abstract/id"
require "active_support/core_ext/hash/conversions" require "active_support/core_ext/hash/conversions"
require "active_support/core_ext/object/to_query" require "active_support/core_ext/object/to_query"
@ -16,10 +18,10 @@ class Metal
end end
module Live module Live
# Disable controller / rendering threads in tests. User tests can access # Disable controller / rendering threads in tests. User tests can access the
# the database on the main thread, so they could open a txn, then the # database on the main thread, so they could open a txn, then the controller
# controller thread will open a new connection and try to access data # thread will open a new connection and try to access data that's only visible
# that's only visible to the main thread's txn. This is the problem in #23483. # to the main thread's txn. This is the problem in #23483.
silence_redefinition_of_method :new_controller_thread silence_redefinition_of_method :new_controller_thread
def new_controller_thread # :nodoc: def new_controller_thread # :nodoc:
yield yield
@ -29,8 +31,8 @@ def new_controller_thread # :nodoc:
Buffer.queue_size = nil Buffer.queue_size = nil
end end
# ActionController::TestCase will be deprecated and moved to a gem in the future. # ActionController::TestCase will be deprecated and moved to a gem in the
# Please use ActionDispatch::IntegrationTest going forward. # future. Please use ActionDispatch::IntegrationTest going forward.
class TestRequest < ActionDispatch::TestRequest # :nodoc: class TestRequest < ActionDispatch::TestRequest # :nodoc:
DEFAULT_ENV = ActionDispatch::TestRequest::DEFAULT_ENV.dup DEFAULT_ENV = ActionDispatch::TestRequest::DEFAULT_ENV.dup
DEFAULT_ENV.delete "PATH_INFO" DEFAULT_ENV.delete "PATH_INFO"
@ -232,116 +234,125 @@ def load!
end end
end end
# = Action Controller Test Case # # Action Controller Test Case
# #
# Superclass for ActionController functional tests. Functional tests allow you to # Superclass for ActionController functional tests. Functional tests allow you
# test a single controller action per test method. # to test a single controller action per test method.
# #
# == Use integration style controller tests over functional style controller tests. # ## Use integration style controller tests over functional style controller tests.
# #
# \Rails discourages the use of functional tests in favor of integration tests # Rails discourages the use of functional tests in favor of integration tests
# (use ActionDispatch::IntegrationTest). # (use ActionDispatch::IntegrationTest).
# #
# New \Rails applications no longer generate functional style controller tests and they should # New Rails applications no longer generate functional style controller tests
# only be used for backward compatibility. Integration style controller tests perform actual # and they should only be used for backward compatibility. Integration style
# requests, whereas functional style controller tests merely simulate a request. Besides, # controller tests perform actual requests, whereas functional style controller
# integration tests are as fast as functional tests and provide lot of helpers such as +as+, # tests merely simulate a request. Besides, integration tests are as fast as
# +parsed_body+ for effective testing of controller actions including even API endpoints. # functional tests and provide lot of helpers such as `as`, `parsed_body` for
# effective testing of controller actions including even API endpoints.
# #
# == Basic example # ## Basic example
# #
# Functional tests are written as follows: # Functional tests are written as follows:
# 1. First, one uses the +get+, +post+, +patch+, +put+, +delete+, or +head+ method to simulate # 1. First, one uses the `get`, `post`, `patch`, `put`, `delete`, or `head`
# an HTTP request. # method to simulate an HTTP request.
# 2. Then, one asserts whether the current state is as expected. "State" can be anything: # 2. Then, one asserts whether the current state is as expected. "State" can be
# the controller's HTTP response, the database contents, etc. # anything: the controller's HTTP response, the database contents, etc.
#
# #
# For example: # For example:
# #
# class BooksControllerTest < ActionController::TestCase # class BooksControllerTest < ActionController::TestCase
# def test_create # def test_create
# # Simulate a POST response with the given HTTP parameters. # # Simulate a POST response with the given HTTP parameters.
# post(:create, params: { book: { title: "Love Hina" }}) # post(:create, params: { book: { title: "Love Hina" }})
# #
# # Asserts that the controller tried to redirect us to # # Asserts that the controller tried to redirect us to
# # the created book's URI. # # the created book's URI.
# assert_response :found # assert_response :found
# #
# # Asserts that the controller really put the book in the database. # # Asserts that the controller really put the book in the database.
# assert_not_nil Book.find_by(title: "Love Hina") # assert_not_nil Book.find_by(title: "Love Hina")
# end
# end # end
# end
# #
# You can also send a real document in the simulated HTTP request. # You can also send a real document in the simulated HTTP request.
# #
# def test_create # def test_create
# json = {book: { title: "Love Hina" }}.to_json # json = {book: { title: "Love Hina" }}.to_json
# post :create, body: json # post :create, body: json
# end # end
# #
# == Special instance variables # ## Special instance variables
# #
# ActionController::TestCase will also automatically provide the following instance # ActionController::TestCase will also automatically provide the following
# variables for use in the tests: # instance variables for use in the tests:
# #
# <b>@controller</b>:: # **@controller**
# The controller instance that will be tested. # : The controller instance that will be tested.
# <b>@request</b>:: # **@request**
# An ActionController::TestRequest, representing the current HTTP # : An ActionController::TestRequest, representing the current HTTP request.
# request. You can modify this object before sending the HTTP request. For example, # You can modify this object before sending the HTTP request. For example,
# you might want to set some session properties before sending a GET request. # you might want to set some session properties before sending a GET
# <b>@response</b>:: # request.
# An ActionDispatch::TestResponse object, representing the response # **@response**
# of the last HTTP response. In the above example, <tt>@response</tt> becomes valid # : An ActionDispatch::TestResponse object, representing the response of the
# after calling +post+. If the various assert methods are not sufficient, then you # last HTTP response. In the above example, `@response` becomes valid after
# may use this object to inspect the HTTP response in detail. # calling `post`. If the various assert methods are not sufficient, then you
# may use this object to inspect the HTTP response in detail.
# #
# == Controller is automatically inferred #
# ## Controller is automatically inferred
# #
# ActionController::TestCase will automatically infer the controller under test # ActionController::TestCase will automatically infer the controller under test
# from the test class name. If the controller cannot be inferred from the test # from the test class name. If the controller cannot be inferred from the test
# class name, you can explicitly set it with +tests+. # class name, you can explicitly set it with `tests`.
# #
# class SpecialEdgeCaseWidgetsControllerTest < ActionController::TestCase # class SpecialEdgeCaseWidgetsControllerTest < ActionController::TestCase
# tests WidgetController # tests WidgetController
# end # end
# #
# == \Testing controller internals # ## Testing controller internals
# #
# In addition to these specific assertions, you also have easy access to various collections that the regular test/unit assertions # In addition to these specific assertions, you also have easy access to various
# can be used against. These collections are: # collections that the regular test/unit assertions can be used against. These
# collections are:
#
# * session: Objects being saved in the session.
# * flash: The flash objects currently in the session.
# * cookies: Cookies being sent to the user on this request.
# #
# * session: Objects being saved in the session.
# * flash: The flash objects currently in the session.
# * cookies: \Cookies being sent to the user on this request.
# #
# These collections can be used just like any other hash: # These collections can be used just like any other hash:
# #
# assert_equal "Dave", cookies[:name] # makes sure that a cookie called :name was set as "Dave" # assert_equal "Dave", cookies[:name] # makes sure that a cookie called :name was set as "Dave"
# assert flash.empty? # makes sure that there's nothing in the flash # assert flash.empty? # makes sure that there's nothing in the flash
# #
# On top of the collections, you have the complete URL that a given action redirected to available in <tt>redirect_to_url</tt>. # On top of the collections, you have the complete URL that a given action
# redirected to available in `redirect_to_url`.
# #
# For redirects within the same controller, you can even call follow_redirect and the redirect will be followed, triggering another # For redirects within the same controller, you can even call follow_redirect
# action call which can then be asserted against. # and the redirect will be followed, triggering another action call which can
# then be asserted against.
# #
# == Manipulating session and cookie variables # ## Manipulating session and cookie variables
# #
# Sometimes you need to set up the session and cookie variables for a test. # Sometimes you need to set up the session and cookie variables for a test. To
# To do this just assign a value to the session or cookie collection: # do this just assign a value to the session or cookie collection:
# #
# session[:key] = "value" # session[:key] = "value"
# cookies[:key] = "value" # cookies[:key] = "value"
# #
# To clear the cookies for a test just clear the cookie collection: # To clear the cookies for a test just clear the cookie collection:
# #
# cookies.clear # cookies.clear
# #
# == \Testing named routes # ## Testing named routes
# #
# If you're using named routes, they can be easily tested using the original named routes' methods straight in the test case. # If you're using named routes, they can be easily tested using the original
# named routes' methods straight in the test case.
# #
# assert_redirected_to page_url(title: 'foo') # assert_redirected_to page_url(title: 'foo')
class TestCase < ActiveSupport::TestCase class TestCase < ActiveSupport::TestCase
singleton_class.attr_accessor :executor_around_each_request singleton_class.attr_accessor :executor_around_each_request
@ -354,12 +365,12 @@ module Behavior
attr_reader :response, :request attr_reader :response, :request
module ClassMethods module ClassMethods
# Sets the controller class name. Useful if the name can't be inferred from test class. # Sets the controller class name. Useful if the name can't be inferred from test
# Normalizes +controller_class+ before using. # class. Normalizes `controller_class` before using.
# #
# tests WidgetController # tests WidgetController
# tests :widget # tests :widget
# tests 'widget' # tests 'widget'
def tests(controller_class) def tests(controller_class)
case controller_class case controller_class
when String, Symbol when String, Symbol
@ -392,21 +403,24 @@ def determine_default_controller_class(name)
# Simulate a GET request with the given parameters. # Simulate a GET request with the given parameters.
# #
# - +action+: The controller action to call. # * `action`: The controller action to call.
# - +params+: The hash with HTTP parameters that you want to pass. This may be +nil+. # * `params`: The hash with HTTP parameters that you want to pass. This may be
# - +body+: The request body with a string that is appropriately encoded # `nil`.
# (<tt>application/x-www-form-urlencoded</tt> or <tt>multipart/form-data</tt>). # * `body`: The request body with a string that is appropriately encoded
# - +session+: A hash of parameters to store in the session. This may be +nil+. # (`application/x-www-form-urlencoded` or `multipart/form-data`).
# - +flash+: A hash of parameters to store in the flash. This may be +nil+. # * `session`: A hash of parameters to store in the session. This may be
# `nil`.
# * `flash`: A hash of parameters to store in the flash. This may be `nil`.
# #
# You can also simulate POST, PATCH, PUT, DELETE, and HEAD requests with
# +post+, +patch+, +put+, +delete+, and +head+.
# Example sending parameters, session, and setting a flash message:
# #
# get :show, # You can also simulate POST, PATCH, PUT, DELETE, and HEAD requests with `post`,
# params: { id: 7 }, # `patch`, `put`, `delete`, and `head`. Example sending parameters, session, and
# session: { user_id: 1 }, # setting a flash message:
# flash: { notice: 'This is flash message' } #
# get :show,
# params: { id: 7 },
# session: { user_id: 1 },
# flash: { notice: 'This is flash message' }
# #
# Note that the request method is not verified. The different methods are # Note that the request method is not verified. The different methods are
# available to make the tests more expressive. # available to make the tests more expressive.
@ -417,67 +431,71 @@ def get(action, **args)
end end
# Simulate a POST request with the given parameters and set/volley the response. # Simulate a POST request with the given parameters and set/volley the response.
# See +get+ for more details. # See `get` for more details.
def post(action, **args) def post(action, **args)
process(action, method: "POST", **args) process(action, method: "POST", **args)
end end
# Simulate a PATCH request with the given parameters and set/volley the response. # Simulate a PATCH request with the given parameters and set/volley the
# See +get+ for more details. # response. See `get` for more details.
def patch(action, **args) def patch(action, **args)
process(action, method: "PATCH", **args) process(action, method: "PATCH", **args)
end end
# Simulate a PUT request with the given parameters and set/volley the response. # Simulate a PUT request with the given parameters and set/volley the response.
# See +get+ for more details. # See `get` for more details.
def put(action, **args) def put(action, **args)
process(action, method: "PUT", **args) process(action, method: "PUT", **args)
end end
# Simulate a DELETE request with the given parameters and set/volley the response. # Simulate a DELETE request with the given parameters and set/volley the
# See +get+ for more details. # response. See `get` for more details.
def delete(action, **args) def delete(action, **args)
process(action, method: "DELETE", **args) process(action, method: "DELETE", **args)
end end
# Simulate a HEAD request with the given parameters and set/volley the response. # Simulate a HEAD request with the given parameters and set/volley the response.
# See +get+ for more details. # See `get` for more details.
def head(action, **args) def head(action, **args)
process(action, method: "HEAD", **args) process(action, method: "HEAD", **args)
end end
# Simulate an HTTP request to +action+ by specifying request method, # Simulate an HTTP request to `action` by specifying request method, parameters
# parameters and set/volley the response. # and set/volley the response.
# #
# - +action+: The controller action to call. # * `action`: The controller action to call.
# - +method+: Request method used to send the HTTP request. Possible values # * `method`: Request method used to send the HTTP request. Possible values
# are +GET+, +POST+, +PATCH+, +PUT+, +DELETE+, +HEAD+. Defaults to +GET+. Can be a symbol. # are `GET`, `POST`, `PATCH`, `PUT`, `DELETE`, `HEAD`. Defaults to `GET`.
# - +params+: The hash with HTTP parameters that you want to pass. This may be +nil+. # Can be a symbol.
# - +body+: The request body with a string that is appropriately encoded # * `params`: The hash with HTTP parameters that you want to pass. This may be
# (<tt>application/x-www-form-urlencoded</tt> or <tt>multipart/form-data</tt>). # `nil`.
# - +session+: A hash of parameters to store in the session. This may be +nil+. # * `body`: The request body with a string that is appropriately encoded
# - +flash+: A hash of parameters to store in the flash. This may be +nil+. # (`application/x-www-form-urlencoded` or `multipart/form-data`).
# - +format+: Request format. Defaults to +nil+. Can be string or symbol. # * `session`: A hash of parameters to store in the session. This may be
# - +as+: Content type. Defaults to +nil+. Must be a symbol that corresponds # `nil`.
# to a mime type. # * `flash`: A hash of parameters to store in the flash. This may be `nil`.
# * `format`: Request format. Defaults to `nil`. Can be string or symbol.
# * `as`: Content type. Defaults to `nil`. Must be a symbol that corresponds
# to a mime type.
# #
# Example calling +create+ action and sending two params:
# #
# process :create, # Example calling `create` action and sending two params:
# method: 'POST',
# params: {
# user: { name: 'Gaurish Sharma', email: 'user@example.com' }
# },
# session: { user_id: 1 },
# flash: { notice: 'This is flash message' }
# #
# To simulate +GET+, +POST+, +PATCH+, +PUT+, +DELETE+, and +HEAD+ requests # process :create,
# prefer using #get, #post, #patch, #put, #delete and #head methods # method: 'POST',
# respectively which will make tests more expressive. # params: {
# user: { name: 'Gaurish Sharma', email: 'user@example.com' }
# },
# session: { user_id: 1 },
# flash: { notice: 'This is flash message' }
#
# To simulate `GET`, `POST`, `PATCH`, `PUT`, `DELETE`, and `HEAD` requests
# prefer using #get, #post, #patch, #put, #delete and #head methods respectively
# which will make tests more expressive.
# #
# It's not recommended to make more than one request in the same test. Instance # It's not recommended to make more than one request in the same test. Instance
# variables that are set in one request will not persist to the next request, # variables that are set in one request will not persist to the next request,
# but it's not guaranteed that all \Rails internal state will be reset. Prefer # but it's not guaranteed that all Rails internal state will be reset. Prefer
# ActionDispatch::IntegrationTest for making multiple requests in the same test. # ActionDispatch::IntegrationTest for making multiple requests in the same test.
# #
# Note that the request method is not verified. # Note that the request method is not verified.
@ -654,8 +672,8 @@ def document_root_element
end end
def check_required_ivars def check_required_ivars
# Check for required instance variables so we can give an # Check for required instance variables so we can give an understandable error
# understandable error message. # message.
[:@routes, :@controller, :@request, :@response].each do |iv_name| [:@routes, :@controller, :@request, :@response].each do |iv_name|
if !instance_variable_defined?(iv_name) || instance_variable_get(iv_name).nil? if !instance_variable_defined?(iv_name) || instance_variable_get(iv_name).nil?
raise "#{iv_name} is nil: make sure you set it in your test's setup method." raise "#{iv_name} is nil: make sure you set it in your test's setup method."

@ -3,26 +3,27 @@
#-- #--
# Copyright (c) David Heinemeier Hansson # Copyright (c) David Heinemeier Hansson
# #
# Permission is hereby granted, free of charge, to any person obtaining # Permission is hereby granted, free of charge, to any person obtaining a copy
# a copy of this software and associated documentation files (the # of this software and associated documentation files (the "Software"), to deal
# "Software"), to deal in the Software without restriction, including # in the Software without restriction, including without limitation the rights
# without limitation the rights to use, copy, modify, merge, publish, # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# distribute, sublicense, and/or sell copies of the Software, and to # copies of the Software, and to permit persons to whom the Software is
# permit persons to whom the Software is furnished to do so, subject to # furnished to do so, subject to the following conditions:
# the following conditions:
# #
# The above copyright notice and this permission notice shall be # The above copyright notice and this permission notice shall be included in all
# included in all copies or substantial portions of the Software. # copies or substantial portions of the Software.
# #
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # SOFTWARE.
#++ #++
# :markup: markdown
require "active_support" require "active_support"
require "active_support/rails" require "active_support/rails"
require "active_support/core_ext/module/attribute_accessors" require "active_support/core_ext/module/attribute_accessors"
@ -35,14 +36,14 @@ module Rack # :nodoc:
autoload :Test, "rack/test" autoload :Test, "rack/test"
end end
# = Action Dispatch # # Action Dispatch
# #
# Action Dispatch is a module of Action Pack. # Action Dispatch is a module of Action Pack.
# #
# Action Dispatch parses information about the web request, handles # Action Dispatch parses information about the web request, handles routing as
# routing as defined by the user, and does advanced processing related to HTTP # defined by the user, and does advanced processing related to HTTP such as
# such as MIME-type negotiation, decoding parameters in POST, PATCH, or PUT # MIME-type negotiation, decoding parameters in POST, PATCH, or PUT bodies,
# bodies, handling HTTP caching logic, cookies and sessions. # handling HTTP caching logic, cookies and sessions.
module ActionDispatch module ActionDispatch
extend ActiveSupport::Autoload extend ActiveSupport::Autoload

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
require "rack/version" require "rack/version"
module ActionDispatch module ActionDispatch

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
module ActionDispatch module ActionDispatch
def self.deprecator # :nodoc: def self.deprecator # :nodoc:
@deprecator ||= ActiveSupport::Deprecation.new @deprecator ||= ActiveSupport::Deprecation.new

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
module ActionDispatch module ActionDispatch
module Http module Http
module Cache module Cache
@ -32,8 +34,8 @@ def etag_matches?(etag)
end end
end end
# Check response freshness (+Last-Modified+ and ETag) against request # Check response freshness (`Last-Modified` and ETag) against request
# +If-Modified-Since+ and +If-None-Match+ conditions. If both headers are # `If-Modified-Since` and `If-None-Match` conditions. If both headers are
# supplied, both must match, or the request is not considered fresh. # supplied, both must match, or the request is not considered fresh.
def fresh?(response) def fresh?(response)
last_modified = if_modified_since last_modified = if_modified_since
@ -79,25 +81,24 @@ def date=(utc_time)
set_header DATE, utc_time.httpdate set_header DATE, utc_time.httpdate
end end
# This method sets a weak ETag validator on the response so browsers # This method sets a weak ETag validator on the response so browsers and proxies
# and proxies may cache the response, keyed on the ETag. On subsequent # may cache the response, keyed on the ETag. On subsequent requests, the
# requests, the +If-None-Match+ header is set to the cached ETag. If it # `If-None-Match` header is set to the cached ETag. If it matches the current
# matches the current ETag, we can return a <tt>304 Not Modified</tt> response # ETag, we can return a `304 Not Modified` response with no body, letting the
# with no body, letting the browser or proxy know that their cache is # browser or proxy know that their cache is current. Big savings in request time
# current. Big savings in request time and network bandwidth. # and network bandwidth.
# #
# Weak ETags are considered to be semantically equivalent but not # Weak ETags are considered to be semantically equivalent but not byte-for-byte
# byte-for-byte identical. This is perfect for browser caching of HTML # identical. This is perfect for browser caching of HTML pages where we don't
# pages where we don't care about exact equality, just what the user # care about exact equality, just what the user is viewing.
# is viewing.
# #
# Strong ETags are considered byte-for-byte identical. They allow a # Strong ETags are considered byte-for-byte identical. They allow a browser or
# browser or proxy cache to support +Range+ requests, useful for paging # proxy cache to support `Range` requests, useful for paging through a PDF file
# through a PDF file or scrubbing through a video. Some CDNs only # or scrubbing through a video. Some CDNs only support strong ETags and will
# support strong ETags and will ignore weak ETags entirely. # ignore weak ETags entirely.
# #
# Weak ETags are what we almost always need, so they're the default. # Weak ETags are what we almost always need, so they're the default. Check out
# Check out #strong_etag= to provide a strong ETag validator. # #strong_etag= to provide a strong ETag validator.
def etag=(weak_validators) def etag=(weak_validators)
self.weak_etag = weak_validators self.weak_etag = weak_validators
end end
@ -112,12 +113,13 @@ def strong_etag=(strong_validators)
def etag?; etag; end def etag?; etag; end
# True if an ETag is set, and it's a weak validator (preceded with <tt>W/</tt>). # True if an ETag is set, and it's a weak validator (preceded with `W/`).
def weak_etag? def weak_etag?
etag? && etag.start_with?('W/"') etag? && etag.start_with?('W/"')
end end
# True if an ETag is set, and it isn't a weak validator (not preceded with <tt>W/</tt>). # True if an ETag is set, and it isn't a weak validator (not preceded with
# `W/`).
def strong_etag? def strong_etag?
etag? && !weak_etag? etag? && !weak_etag?
end end
@ -171,10 +173,9 @@ def prepare_cache_control!
MUST_REVALIDATE = "must-revalidate" MUST_REVALIDATE = "must-revalidate"
def handle_conditional_get! def handle_conditional_get!
# Normally default cache control setting is handled by ETag # Normally default cache control setting is handled by ETag middleware. But, if
# middleware. But, if an etag is already set, the middleware # an etag is already set, the middleware defaults to `no-cache` unless a default
# defaults to `no-cache` unless a default `Cache-Control` value is # `Cache-Control` value is previously set. So, set a default one here.
# previously set. So, set a default one here.
if (etag? || last_modified?) && !self._cache_control if (etag? || last_modified?) && !self._cache_control
self._cache_control = DEFAULT_CACHE_CONTROL self._cache_control = DEFAULT_CACHE_CONTROL
end end
@ -186,8 +187,8 @@ def merge_and_normalize_cache_control!(cache_control)
return if control.empty? && cache_control.empty? # Let middleware handle default behavior return if control.empty? && cache_control.empty? # Let middleware handle default behavior
if cache_control.any? if cache_control.any?
# Any caching directive coming from a controller overrides # Any caching directive coming from a controller overrides no-cache/no-store in
# no-cache/no-store in the default Cache-Control header. # the default Cache-Control header.
control.delete(:no_cache) control.delete(:no_cache)
control.delete(:no_store) control.delete(:no_store)

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
module ActionDispatch module ActionDispatch
module Http module Http
class ContentDisposition # :nodoc: class ContentDisposition # :nodoc:

@ -1,28 +1,31 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
require "active_support/core_ext/object/deep_dup" require "active_support/core_ext/object/deep_dup"
require "active_support/core_ext/array/wrap" require "active_support/core_ext/array/wrap"
module ActionDispatch # :nodoc: module ActionDispatch # :nodoc:
# = Action Dispatch Content Security Policy # # Action Dispatch Content Security Policy
# #
# Configures the HTTP # Configures the HTTP [Content-Security-Policy]
# {Content-Security-Policy}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy] # (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy)
# response header to help protect against XSS and injection attacks. # response header to help protect against XSS and
# injection attacks.
# #
# Example global policy: # Example global policy:
# #
# Rails.application.config.content_security_policy do |policy| # Rails.application.config.content_security_policy do |policy|
# policy.default_src :self, :https # policy.default_src :self, :https
# policy.font_src :self, :https, :data # policy.font_src :self, :https, :data
# policy.img_src :self, :https, :data # policy.img_src :self, :https, :data
# policy.object_src :none # policy.object_src :none
# policy.script_src :self, :https # policy.script_src :self, :https
# policy.style_src :self, :https # policy.style_src :self, :https
# #
# # Specify URI for violation reports # # Specify URI for violation reports
# policy.report_uri "/csp-violation-report-endpoint" # policy.report_uri "/csp-violation-report-endpoint"
# end # end
class ContentSecurityPolicy class ContentSecurityPolicy
class Middleware class Middleware
def initialize(app) def initialize(app)
@ -32,8 +35,8 @@ def initialize(app)
def call(env) def call(env)
status, headers, _ = response = @app.call(env) status, headers, _ = response = @app.call(env)
# Returning CSP headers with a 304 Not Modified is harmful, since nonces in the new # Returning CSP headers with a 304 Not Modified is harmful, since nonces in the
# CSP headers might not match nonces in the cached HTML. # new CSP headers might not match nonces in the cached HTML.
return response if status == 304 return response if status == 304
return response if policy_present?(headers) return response if policy_present?(headers)
@ -190,14 +193,14 @@ def initialize_copy(other)
end end
end end
# Specify whether to prevent the user agent from loading any assets over # Specify whether to prevent the user agent from loading any assets over HTTP
# HTTP when the page uses HTTPS: # when the page uses HTTPS:
# #
# policy.block_all_mixed_content # policy.block_all_mixed_content
# #
# Pass +false+ to allow it again: # Pass `false` to allow it again:
# #
# policy.block_all_mixed_content false # policy.block_all_mixed_content false
# #
def block_all_mixed_content(enabled = true) def block_all_mixed_content(enabled = true)
if enabled if enabled
@ -209,11 +212,11 @@ def block_all_mixed_content(enabled = true)
# Restricts the set of plugins that can be embedded: # Restricts the set of plugins that can be embedded:
# #
# policy.plugin_types "application/x-shockwave-flash" # policy.plugin_types "application/x-shockwave-flash"
# #
# Leave empty to allow all plugins: # Leave empty to allow all plugins:
# #
# policy.plugin_types # policy.plugin_types
# #
def plugin_types(*types) def plugin_types(*types)
if types.first if types.first
@ -223,23 +226,25 @@ def plugin_types(*types)
end end
end end
# Enable the {report-uri}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/report-uri] # Enable the [report-uri]
# directive. Violation reports will be sent to the specified URI: # (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/report-uri)
# directive. Violation reports will be sent to the
# specified URI:
# #
# policy.report_uri "/csp-violation-report-endpoint" # policy.report_uri "/csp-violation-report-endpoint"
# #
def report_uri(uri) def report_uri(uri)
@directives["report-uri"] = [uri] @directives["report-uri"] = [uri]
end end
# Specify asset types for which {Subresource Integrity}[https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity] # Specify asset types for which [Subresource Integrity]
# is required: # (https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity) is required:
# #
# policy.require_sri_for :script, :style # policy.require_sri_for :script, :style
# #
# Leave empty to not require Subresource Integrity: # Leave empty to not require Subresource Integrity:
# #
# policy.require_sri_for # policy.require_sri_for
# #
def require_sri_for(*types) def require_sri_for(*types)
if types.first if types.first
@ -249,18 +254,19 @@ def require_sri_for(*types)
end end
end end
# Specify whether a {sandbox}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/sandbox] # Specify whether a [sandbox]
# (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/sandbox)
# should be enabled for the requested resource: # should be enabled for the requested resource:
# #
# policy.sandbox # policy.sandbox
# #
# Values can be passed as arguments: # Values can be passed as arguments:
# #
# policy.sandbox "allow-scripts", "allow-modals" # policy.sandbox "allow-scripts", "allow-modals"
# #
# Pass +false+ to disable the sandbox: # Pass `false` to disable the sandbox:
# #
# policy.sandbox false # policy.sandbox false
# #
def sandbox(*values) def sandbox(*values)
if values.empty? if values.empty?
@ -274,11 +280,11 @@ def sandbox(*values)
# Specify whether user agents should treat any assets over HTTP as HTTPS: # Specify whether user agents should treat any assets over HTTP as HTTPS:
# #
# policy.upgrade_insecure_requests # policy.upgrade_insecure_requests
# #
# Pass +false+ to disable it: # Pass `false` to disable it:
# #
# policy.upgrade_insecure_requests false # policy.upgrade_insecure_requests false
# #
def upgrade_insecure_requests(enabled = true) def upgrade_insecure_requests(enabled = true)
if enabled if enabled

@ -1,18 +1,21 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
require "active_support/parameter_filter" require "active_support/parameter_filter"
module ActionDispatch module ActionDispatch
module Http module Http
# = Action Dispatch HTTP Filter Parameters # # Action Dispatch HTTP Filter Parameters
# #
# Allows you to specify sensitive query string and POST parameters to filter # Allows you to specify sensitive query string and POST parameters to filter
# from the request log. # from the request log.
# #
# # Replaces values with "[FILTERED]" for keys that match /foo|bar/i. # # Replaces values with "[FILTERED]" for keys that match /foo|bar/i.
# env["action_dispatch.parameter_filter"] = [:foo, "bar"] # env["action_dispatch.parameter_filter"] = [:foo, "bar"]
# #
# For more information about filter behavior, see ActiveSupport::ParameterFilter. # For more information about filter behavior, see
# ActiveSupport::ParameterFilter.
module FilterParameters module FilterParameters
ENV_MATCH = [/RAW_POST_DATA/, "rack.request.form_vars"] # :nodoc: ENV_MATCH = [/RAW_POST_DATA/, "rack.request.form_vars"] # :nodoc:
NULL_PARAM_FILTER = ActiveSupport::ParameterFilter.new # :nodoc: NULL_PARAM_FILTER = ActiveSupport::ParameterFilter.new # :nodoc:
@ -43,7 +46,8 @@ def filtered_path
@filtered_path ||= query_string.empty? ? path : "#{path}?#{filtered_query_string}" @filtered_path ||= query_string.empty? ? path : "#{path}?#{filtered_query_string}"
end end
# Returns the +ActiveSupport::ParameterFilter+ object used to filter in this request. # Returns the `ActiveSupport::ParameterFilter` object used to filter in this
# request.
def parameter_filter def parameter_filter
@parameter_filter ||= if has_header?("action_dispatch.parameter_filter") @parameter_filter ||= if has_header?("action_dispatch.parameter_filter")
parameter_filter_for get_header("action_dispatch.parameter_filter") parameter_filter_for get_header("action_dispatch.parameter_filter")

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
module ActionDispatch module ActionDispatch
module Http module Http
module FilterRedirect module FilterRedirect

@ -1,28 +1,30 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
module ActionDispatch module ActionDispatch
module Http module Http
# = Action Dispatch HTTP \Headers # # Action Dispatch HTTP Headers
# #
# Provides access to the request's HTTP headers from the environment. # Provides access to the request's HTTP headers from the environment.
# #
# env = { "CONTENT_TYPE" => "text/plain", "HTTP_USER_AGENT" => "curl/7.43.0" } # env = { "CONTENT_TYPE" => "text/plain", "HTTP_USER_AGENT" => "curl/7.43.0" }
# headers = ActionDispatch::Http::Headers.from_hash(env) # headers = ActionDispatch::Http::Headers.from_hash(env)
# headers["Content-Type"] # => "text/plain" # headers["Content-Type"] # => "text/plain"
# headers["User-Agent"] # => "curl/7.43.0" # headers["User-Agent"] # => "curl/7.43.0"
# #
# Also note that when headers are mapped to CGI-like variables by the Rack # Also note that when headers are mapped to CGI-like variables by the Rack
# server, both dashes and underscores are converted to underscores. This # server, both dashes and underscores are converted to underscores. This
# ambiguity cannot be resolved at this stage anymore. Both underscores and # ambiguity cannot be resolved at this stage anymore. Both underscores and
# dashes have to be interpreted as if they were originally sent as dashes. # dashes have to be interpreted as if they were originally sent as dashes.
# #
# # GET / HTTP/1.1 # # GET / HTTP/1.1
# # ... # # ...
# # User-Agent: curl/7.43.0 # # User-Agent: curl/7.43.0
# # X_Custom_Header: token # # X_Custom_Header: token
# #
# headers["X_Custom_Header"] # => nil # headers["X_Custom_Header"] # => nil
# headers["X-Custom-Header"] # => "token" # headers["X-Custom-Header"] # => "token"
class Headers class Headers
CGI_VARIABLES = Set.new(%W[ CGI_VARIABLES = Set.new(%W[
AUTH_TYPE AUTH_TYPE
@ -67,7 +69,7 @@ def []=(key, value)
@req.set_header env_name(key), value @req.set_header env_name(key), value
end end
# Add a value to a multivalued header like +Vary+ or +Accept-Encoding+. # Add a value to a multivalued header like `Vary` or `Accept-Encoding`.
def add(key, value) def add(key, value)
@req.add_header env_name(key), value @req.add_header env_name(key), value
end end
@ -81,11 +83,10 @@ def key?(key)
# Returns the value for the given key mapped to @env. # Returns the value for the given key mapped to @env.
# #
# If the key is not found and an optional code block is not provided, # If the key is not found and an optional code block is not provided, raises a
# raises a <tt>KeyError</tt> exception. # `KeyError` exception.
# #
# If the code block is provided, then it will be run and # If the code block is provided, then it will be run and its result returned.
# its result returned.
def fetch(key, default = DEFAULT) def fetch(key, default = DEFAULT)
@req.fetch_header(env_name(key)) do @req.fetch_header(env_name(key)) do
return default unless default == DEFAULT return default unless default == DEFAULT
@ -99,16 +100,15 @@ def each(&block)
end end
# Returns a new Http::Headers instance containing the contents of # Returns a new Http::Headers instance containing the contents of
# <tt>headers_or_env</tt> and the original instance. # `headers_or_env` and the original instance.
def merge(headers_or_env) def merge(headers_or_env)
headers = @req.dup.headers headers = @req.dup.headers
headers.merge!(headers_or_env) headers.merge!(headers_or_env)
headers headers
end end
# Adds the contents of <tt>headers_or_env</tt> to original instance # Adds the contents of `headers_or_env` to original instance entries; duplicate
# entries; duplicate keys are overwritten with the values from # keys are overwritten with the values from `headers_or_env`.
# <tt>headers_or_env</tt>.
def merge!(headers_or_env) def merge!(headers_or_env)
headers_or_env.each do |key, value| headers_or_env.each do |key, value|
@req.set_header env_name(key), value @req.set_header env_name(key), value
@ -118,8 +118,8 @@ def merge!(headers_or_env)
def env; @req.env.dup; end def env; @req.env.dup; end
private private
# Converts an HTTP header name to an environment variable name if it is # Converts an HTTP header name to an environment variable name if it is not
# not contained within the headers hash. # contained within the headers hash.
def env_name(key) def env_name(key)
key = key.to_s key = key.to_s
if HTTP_HEADER.match?(key) if HTTP_HEADER.match?(key)

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
require "active_support/core_ext/module/attribute_accessors" require "active_support/core_ext/module/attribute_accessors"
module ActionDispatch module ActionDispatch
@ -18,7 +20,7 @@ class InvalidType < ::Mime::Type::InvalidMimeType; end
mattr_accessor :ignore_accept_header, default: false mattr_accessor :ignore_accept_header, default: false
end end
# The MIME type of the HTTP request, such as Mime[:xml]. # The MIME type of the HTTP request, such as [Mime](:xml).
def content_mime_type def content_mime_type
fetch_header("action_dispatch.request.content_type") do |k| fetch_header("action_dispatch.request.content_type") do |k|
v = if get_header("CONTENT_TYPE") =~ /^([^,;]*)/ v = if get_header("CONTENT_TYPE") =~ /^([^,;]*)/
@ -52,11 +54,11 @@ def accepts
end end
end end
# Returns the MIME type for the \format used in the request. # Returns the MIME type for the format used in the request.
# #
# GET /posts/5.xml | request.format => Mime[:xml] # GET /posts/5.xml | request.format => Mime[:xml]
# GET /posts/5.xhtml | request.format => Mime[:html] # GET /posts/5.xhtml | request.format => Mime[:html]
# GET /posts/5 | request.format => Mime[:html] or Mime[:js], or request.accepts.first # GET /posts/5 | request.format => Mime[:html] or Mime[:js], or request.accepts.first
# #
def format(_view_path = nil) def format(_view_path = nil)
formats.first || Mime::NullType.instance formats.first || Mime::NullType.instance
@ -84,7 +86,7 @@ def formats
end end
end end
# Sets the \variant for template. # Sets the variant for template.
def variant=(variant) def variant=(variant)
variant = Array(variant) variant = Array(variant)
@ -99,36 +101,37 @@ def variant
@variant ||= ActiveSupport::ArrayInquirer.new @variant ||= ActiveSupport::ArrayInquirer.new
end end
# Sets the \format by string extension, which can be used to force custom formats # Sets the format by string extension, which can be used to force custom formats
# that are not controlled by the extension. # that are not controlled by the extension.
# #
# class ApplicationController < ActionController::Base # class ApplicationController < ActionController::Base
# before_action :adjust_format_for_iphone # before_action :adjust_format_for_iphone
# #
# private # private
# def adjust_format_for_iphone # def adjust_format_for_iphone
# request.format = :iphone if request.env["HTTP_USER_AGENT"][/iPhone/] # request.format = :iphone if request.env["HTTP_USER_AGENT"][/iPhone/]
# end # end
# end # end
def format=(extension) def format=(extension)
parameters[:format] = extension.to_s parameters[:format] = extension.to_s
set_header "action_dispatch.request.formats", [Mime::Type.lookup_by_extension(parameters[:format])] set_header "action_dispatch.request.formats", [Mime::Type.lookup_by_extension(parameters[:format])]
end end
# Sets the \formats by string extensions. This differs from #format= by allowing you # Sets the formats by string extensions. This differs from #format= by allowing
# to set multiple, ordered formats, which is useful when you want to have a fallback. # you to set multiple, ordered formats, which is useful when you want to have a
# fallback.
# #
# In this example, the +:iphone+ format will be used if it's available, otherwise it'll fall back # In this example, the `:iphone` format will be used if it's available,
# to the +:html+ format. # otherwise it'll fall back to the `:html` format.
# #
# class ApplicationController < ActionController::Base # class ApplicationController < ActionController::Base
# before_action :adjust_format_for_iphone_with_html_fallback # before_action :adjust_format_for_iphone_with_html_fallback
# #
# private # private
# def adjust_format_for_iphone_with_html_fallback # def adjust_format_for_iphone_with_html_fallback
# request.formats = [ :iphone, :html ] if request.env["HTTP_USER_AGENT"][/iPhone/] # request.formats = [ :iphone, :html ] if request.env["HTTP_USER_AGENT"][/iPhone/]
# end # end
# end # end
def formats=(extensions) def formats=(extensions)
parameters[:format] = extensions.first.to_s parameters[:format] = extensions.first.to_s
set_header "action_dispatch.request.formats", extensions.collect { |extension| set_header "action_dispatch.request.formats", extensions.collect { |extension|
@ -154,8 +157,8 @@ def should_apply_vary_header?
end end
private private
# We use normal content negotiation unless you include */* in your list, # We use normal content negotiation unless you include **/** in your list, in
# in which case we assume you're a browser and send HTML. # which case we assume you're a browser and send HTML.
BROWSER_LIKE_ACCEPTS = /,\s*\*\/\*|\*\/\*\s*,/ BROWSER_LIKE_ACCEPTS = /,\s*\*\/\*|\*\/\*\s*,/
def params_readable? def params_readable?

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
require "singleton" require "singleton"
module Mime module Mime
@ -65,19 +67,20 @@ def fetch(type, &block)
end end
end end
# Encapsulates the notion of a MIME type. Can be used at render time, for example, with: # Encapsulates the notion of a MIME type. Can be used at render time, for
# example, with:
# #
# class PostsController < ActionController::Base # class PostsController < ActionController::Base
# def show # def show
# @post = Post.find(params[:id]) # @post = Post.find(params[:id])
# #
# respond_to do |format| # respond_to do |format|
# format.html # format.html
# format.ics { render body: @post.to_ics, mime_type: Mime::Type.lookup("text/calendar") } # format.ics { render body: @post.to_ics, mime_type: Mime::Type.lookup("text/calendar") }
# format.xml { render xml: @post } # format.xml { render xml: @post }
# end
# end # end
# end # end
# end
class Type class Type
attr_reader :symbol attr_reader :symbol
@ -170,8 +173,9 @@ def lookup_by_extension(extension)
EXTENSION_LOOKUP[extension.to_s] EXTENSION_LOOKUP[extension.to_s]
end end
# Registers an alias that's not used on MIME type lookup, but can be referenced directly. Especially useful for # Registers an alias that's not used on MIME type lookup, but can be referenced
# rendering different HTML versions depending on the user agent, like an iPhone. # directly. Especially useful for rendering different HTML versions depending on
# the user agent, like an iPhone.
def register_alias(string, symbol, extension_synonyms = []) def register_alias(string, symbol, extension_synonyms = [])
register(string, symbol, [], extension_synonyms, true) register(string, symbol, [], extension_synonyms, true)
end end
@ -221,11 +225,11 @@ def parse_trailing_star(accept_header)
parse_data_with_trailing_star($1) if accept_header =~ TRAILING_STAR_REGEXP parse_data_with_trailing_star($1) if accept_header =~ TRAILING_STAR_REGEXP
end end
# For an input of <tt>'text'</tt>, returns <tt>[Mime[:json], Mime[:xml], Mime[:ics], # For an input of `'text'`, returns `[Mime[:json], Mime[:xml], Mime[:ics],
# Mime[:html], Mime[:css], Mime[:csv], Mime[:js], Mime[:yaml], Mime[:text]]</tt>. # Mime[:html], Mime[:css], Mime[:csv], Mime[:js], Mime[:yaml], Mime[:text]]`.
# #
# For an input of <tt>'application'</tt>, returns <tt>[Mime[:html], Mime[:js], # For an input of `'application'`, returns `[Mime[:html], Mime[:js], Mime[:xml],
# Mime[:xml], Mime[:yaml], Mime[:atom], Mime[:json], Mime[:rss], Mime[:url_encoded_form]]</tt>. # Mime[:yaml], Mime[:atom], Mime[:json], Mime[:rss], Mime[:url_encoded_form]]`.
def parse_data_with_trailing_star(type) def parse_data_with_trailing_star(type)
Mime::SET.select { |m| m.match?(type) } Mime::SET.select { |m| m.match?(type) }
end end
@ -234,7 +238,7 @@ def parse_data_with_trailing_star(type)
# #
# To unregister a MIME type: # To unregister a MIME type:
# #
# Mime::Type.unregister(:mobile) # Mime::Type.unregister(:mobile)
def unregister(symbol) def unregister(symbol)
symbol = symbol.downcase symbol = symbol.downcase
if mime = Mime[symbol] if mime = Mime[symbol]
@ -350,9 +354,9 @@ def all?; true; end
def html?; true; end def html?; true; end
end end
# ALL isn't a real MIME type, so we don't register it for lookup with the # ALL isn't a real MIME type, so we don't register it for lookup with the other
# other concrete types. It's a wildcard match that we use for +respond_to+ # concrete types. It's a wildcard match that we use for `respond_to` negotiation
# negotiation internals. # internals.
ALL = AllType.instance ALL = AllType.instance
class NullType class NullType

@ -3,6 +3,8 @@
# Build list of Mime types for HTTP responses # Build list of Mime types for HTTP responses
# https://www.iana.org/assignments/media-types/ # https://www.iana.org/assignments/media-types/
# :markup: markdown
Mime::Type.register "text/html", :html, %w( application/xhtml+xml ), %w( xhtml ) Mime::Type.register "text/html", :html, %w( application/xhtml+xml ), %w( xhtml )
Mime::Type.register "text/plain", :text, [], %w(txt) Mime::Type.register "text/plain", :text, [], %w(txt)
Mime::Type.register "text/javascript", :js, %w( application/javascript application/x-javascript ) Mime::Type.register "text/javascript", :js, %w( application/javascript application/x-javascript )

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
module ActionDispatch module ActionDispatch
module Http module Http
module Parameters module Parameters
@ -14,8 +16,8 @@ module Parameters
} }
} }
# Raised when raw data from the request cannot be parsed by the parser # Raised when raw data from the request cannot be parsed by the parser defined
# defined for request's content MIME type. # for request's content MIME type.
class ParseError < StandardError class ParseError < StandardError
def initialize(message = $!.message) def initialize(message = $!.message)
super(message) super(message)
@ -34,8 +36,8 @@ class << self
module ClassMethods module ClassMethods
# Configure the parameter parser for a given MIME type. # Configure the parameter parser for a given MIME type.
# #
# It accepts a hash where the key is the symbol of the MIME type # It accepts a hash where the key is the symbol of the MIME type and the value
# and the value is a proc. # is a proc.
# #
# original_parsers = ActionDispatch::Request.parameter_parsers # original_parsers = ActionDispatch::Request.parameter_parsers
# xml_parser = -> (raw_post) { Hash.from_xml(raw_post) || {} } # xml_parser = -> (raw_post) { Hash.from_xml(raw_post) || {} }
@ -46,7 +48,7 @@ def parameter_parsers=(parsers)
end end
end end
# Returns both GET and POST \parameters in a single hash. # Returns both GET and POST parameters in a single hash.
def parameters def parameters
params = get_header("action_dispatch.request.parameters") params = get_header("action_dispatch.request.parameters")
return params if params return params if params
@ -66,8 +68,8 @@ def path_parameters=(parameters) # :nodoc:
delete_header("action_dispatch.request.parameters") delete_header("action_dispatch.request.parameters")
parameters = Request::Utils.set_binary_encoding(self, parameters, parameters[:controller], parameters[:action]) parameters = Request::Utils.set_binary_encoding(self, parameters, parameters[:controller], parameters[:action])
# If any of the path parameters has an invalid encoding then # If any of the path parameters has an invalid encoding then raise since it's
# raise since it's likely to trigger errors further on. # likely to trigger errors further on.
Request::Utils.check_param_encoding(parameters) Request::Utils.check_param_encoding(parameters)
set_header PARAMETERS_KEY, parameters set_header PARAMETERS_KEY, parameters
@ -75,10 +77,10 @@ def path_parameters=(parameters) # :nodoc:
raise ActionController::BadRequest.new("Invalid path parameters: #{e.message}") raise ActionController::BadRequest.new("Invalid path parameters: #{e.message}")
end end
# Returns a hash with the \parameters used to form the \path of the request. # Returns a hash with the parameters used to form the path of the request.
# Returned hash keys are symbols: # Returned hash keys are symbols:
# #
# { action: "my_action", controller: "my_controller" } # { action: "my_action", controller: "my_controller" }
def path_parameters def path_parameters
get_header(PARAMETERS_KEY) || set_header(PARAMETERS_KEY, {}) get_header(PARAMETERS_KEY) || set_header(PARAMETERS_KEY, {})
end end

@ -1,31 +1,33 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
require "active_support/core_ext/object/deep_dup" require "active_support/core_ext/object/deep_dup"
module ActionDispatch # :nodoc: module ActionDispatch # :nodoc:
# = Action Dispatch \PermissionsPolicy # # Action Dispatch PermissionsPolicy
# #
# Configures the HTTP # Configures the HTTP
# {Feature-Policy}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Feature-Policy] # [Feature-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Feature-Policy)
# response header to specify which browser features the current document and # response header to specify which browser features the current
# its iframes can use. # document and its iframes can use.
# #
# Example global policy: # Example global policy:
# #
# Rails.application.config.permissions_policy do |policy| # Rails.application.config.permissions_policy do |policy|
# policy.camera :none # policy.camera :none
# policy.gyroscope :none # policy.gyroscope :none
# policy.microphone :none # policy.microphone :none
# policy.usb :none # policy.usb :none
# policy.fullscreen :self # policy.fullscreen :self
# policy.payment :self, "https://secure.example.com" # policy.payment :self, "https://secure.example.com"
# end # end
# #
# The Feature-Policy header has been renamed to Permissions-Policy. # The Feature-Policy header has been renamed to Permissions-Policy. The
# The Permissions-Policy requires a different implementation and isn't # Permissions-Policy requires a different implementation and isn't yet supported
# yet supported by all browsers. To avoid having to rename this # by all browsers. To avoid having to rename this middleware in the future we
# middleware in the future we use the new name for the middleware but # use the new name for the middleware but keep the old header name and
# keep the old header name and implementation for now. # implementation for now.
class PermissionsPolicy class PermissionsPolicy
class Middleware class Middleware
def initialize(app) def initialize(app)

@ -2,6 +2,8 @@
# :enddoc: # :enddoc:
# :markup: markdown
require "rack/cache" require "rack/cache"
require "rack/cache/context" require "rack/cache/context"
require "active_support/cache" require "active_support/cache"

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
require "stringio" require "stringio"
require "active_support/inflector" require "active_support/inflector"
@ -102,26 +104,26 @@ def controller_class_for(name)
# Returns true if the request has a header matching the given key parameter. # Returns true if the request has a header matching the given key parameter.
# #
# request.key? :ip_spoofing_check # => true # request.key? :ip_spoofing_check # => true
def key?(key) def key?(key)
has_header? key has_header? key
end end
# HTTP methods from {RFC 2616: Hypertext Transfer Protocol -- HTTP/1.1}[https://www.ietf.org/rfc/rfc2616.txt] # HTTP methods from [RFC 2616: Hypertext Transfer Protocol -- HTTP/1.1](https://www.ietf.org/rfc/rfc2616.txt)
RFC2616 = %w(OPTIONS GET HEAD POST PUT DELETE TRACE CONNECT) RFC2616 = %w(OPTIONS GET HEAD POST PUT DELETE TRACE CONNECT)
# HTTP methods from {RFC 2518: HTTP Extensions for Distributed Authoring -- WEBDAV}[https://www.ietf.org/rfc/rfc2518.txt] # HTTP methods from [RFC 2518: HTTP Extensions for Distributed Authoring -- WEBDAV](https://www.ietf.org/rfc/rfc2518.txt)
RFC2518 = %w(PROPFIND PROPPATCH MKCOL COPY MOVE LOCK UNLOCK) RFC2518 = %w(PROPFIND PROPPATCH MKCOL COPY MOVE LOCK UNLOCK)
# HTTP methods from {RFC 3253: Versioning Extensions to WebDAV}[https://www.ietf.org/rfc/rfc3253.txt] # HTTP methods from [RFC 3253: Versioning Extensions to WebDAV](https://www.ietf.org/rfc/rfc3253.txt)
RFC3253 = %w(VERSION-CONTROL REPORT CHECKOUT CHECKIN UNCHECKOUT MKWORKSPACE UPDATE LABEL MERGE BASELINE-CONTROL MKACTIVITY) RFC3253 = %w(VERSION-CONTROL REPORT CHECKOUT CHECKIN UNCHECKOUT MKWORKSPACE UPDATE LABEL MERGE BASELINE-CONTROL MKACTIVITY)
# HTTP methods from {RFC 3648: WebDAV Ordered Collections Protocol}[https://www.ietf.org/rfc/rfc3648.txt] # HTTP methods from [RFC 3648: WebDAV Ordered Collections Protocol](https://www.ietf.org/rfc/rfc3648.txt)
RFC3648 = %w(ORDERPATCH) RFC3648 = %w(ORDERPATCH)
# HTTP methods from {RFC 3744: WebDAV Access Control Protocol}[https://www.ietf.org/rfc/rfc3744.txt] # HTTP methods from [RFC 3744: WebDAV Access Control Protocol](https://www.ietf.org/rfc/rfc3744.txt)
RFC3744 = %w(ACL) RFC3744 = %w(ACL)
# HTTP methods from {RFC 5323: WebDAV SEARCH}[https://www.ietf.org/rfc/rfc5323.txt] # HTTP methods from [RFC 5323: WebDAV SEARCH](https://www.ietf.org/rfc/rfc5323.txt)
RFC5323 = %w(SEARCH) RFC5323 = %w(SEARCH)
# HTTP methods from {RFC 4791: Calendaring Extensions to WebDAV}[https://www.ietf.org/rfc/rfc4791.txt] # HTTP methods from [RFC 4791: Calendaring Extensions to WebDAV](https://www.ietf.org/rfc/rfc4791.txt)
RFC4791 = %w(MKCALENDAR) RFC4791 = %w(MKCALENDAR)
# HTTP methods from {RFC 5789: PATCH Method for HTTP}[https://www.ietf.org/rfc/rfc5789.txt] # HTTP methods from [RFC 5789: PATCH Method for HTTP](https://www.ietf.org/rfc/rfc5789.txt)
RFC5789 = %w(PATCH) RFC5789 = %w(PATCH)
HTTP_METHODS = RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC4791 + RFC5789 HTTP_METHODS = RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC4791 + RFC5789
@ -135,20 +137,19 @@ def key?(key)
alias raw_request_method request_method # :nodoc: alias raw_request_method request_method # :nodoc:
# Returns the HTTP \method that the application should see. # Returns the HTTP method that the application should see. In the case where the
# In the case where the \method was overridden by a middleware # method was overridden by a middleware (for instance, if a HEAD request was
# (for instance, if a HEAD request was converted to a GET, # converted to a GET, or if a _method parameter was used to determine the method
# or if a _method parameter was used to determine the \method # the application should use), this method returns the overridden value, not the
# the application should use), this \method returns the overridden # original.
# value, not the original.
def request_method def request_method
@request_method ||= check_method(super) @request_method ||= check_method(super)
end end
# Returns the URI pattern of the matched route for the request, # Returns the URI pattern of the matched route for the request, using the same
# using the same format as <tt>bin/rails routes</tt>: # format as `bin/rails routes`:
# #
# request.route_uri_pattern # => "/:controller(/:action(/:id))(.:format)" # request.route_uri_pattern # => "/:controller(/:action(/:id))(.:format)"
def route_uri_pattern def route_uri_pattern
get_header("action_dispatch.route_uri_pattern") get_header("action_dispatch.route_uri_pattern")
end end
@ -196,12 +197,11 @@ def request_method_symbol
HTTP_METHOD_LOOKUP[request_method] HTTP_METHOD_LOOKUP[request_method]
end end
# Returns the original value of the environment's REQUEST_METHOD, # Returns the original value of the environment's REQUEST_METHOD, even if it was
# even if it was overridden by middleware. See #request_method for # overridden by middleware. See #request_method for more information.
# more information.
# #
# For debugging purposes, when called with arguments this method will # For debugging purposes, when called with arguments this method will fall back
# fall back to Object#method # to Object#method
def method(*args) def method(*args)
if args.empty? if args.empty?
@method ||= check_method( @method ||= check_method(
@ -221,60 +221,61 @@ def method_symbol
# Provides access to the request's HTTP headers, for example: # Provides access to the request's HTTP headers, for example:
# #
# request.headers["Content-Type"] # => "text/plain" # request.headers["Content-Type"] # => "text/plain"
def headers def headers
@headers ||= Http::Headers.new(self) @headers ||= Http::Headers.new(self)
end end
# Early Hints is an HTTP/2 status code that indicates hints to help a client start # Early Hints is an HTTP/2 status code that indicates hints to help a client
# making preparations for processing the final response. # start making preparations for processing the final response.
# #
# If the env contains +rack.early_hints+ then the server accepts HTTP2 push for Link headers. # If the env contains `rack.early_hints` then the server accepts HTTP2 push for
# Link headers.
# #
# The +send_early_hints+ method accepts a hash of links as follows: # The `send_early_hints` method accepts a hash of links as follows:
# #
# send_early_hints("Link" => "</style.css>; rel=preload; as=style\n</script.js>; rel=preload") # send_early_hints("Link" => "</style.css>; rel=preload; as=style\n</script.js>; rel=preload")
# #
# If you are using +javascript_include_tag+ or +stylesheet_link_tag+ the # If you are using `javascript_include_tag` or `stylesheet_link_tag` the Early
# Early Hints headers are included by default if supported. # Hints headers are included by default if supported.
def send_early_hints(links) def send_early_hints(links)
env["rack.early_hints"]&.call(links) env["rack.early_hints"]&.call(links)
end end
# Returns a +String+ with the last requested path including their params. # Returns a `String` with the last requested path including their params.
# #
# # get '/foo' # # get '/foo'
# request.original_fullpath # => '/foo' # request.original_fullpath # => '/foo'
# #
# # get '/foo?bar' # # get '/foo?bar'
# request.original_fullpath # => '/foo?bar' # request.original_fullpath # => '/foo?bar'
def original_fullpath def original_fullpath
@original_fullpath ||= (get_header("ORIGINAL_FULLPATH") || fullpath) @original_fullpath ||= (get_header("ORIGINAL_FULLPATH") || fullpath)
end end
# Returns the +String+ full path including params of the last URL requested. # Returns the `String` full path including params of the last URL requested.
# #
# # get "/articles" # # get "/articles"
# request.fullpath # => "/articles" # request.fullpath # => "/articles"
# #
# # get "/articles?page=2" # # get "/articles?page=2"
# request.fullpath # => "/articles?page=2" # request.fullpath # => "/articles?page=2"
def fullpath def fullpath
@fullpath ||= super @fullpath ||= super
end end
# Returns the original request URL as a +String+. # Returns the original request URL as a `String`.
# #
# # get "/articles?page=2" # # get "/articles?page=2"
# request.original_url # => "http://www.example.com/articles?page=2" # request.original_url # => "http://www.example.com/articles?page=2"
def original_url def original_url
base_url + original_fullpath base_url + original_fullpath
end end
# The +String+ MIME type of the request. # The `String` MIME type of the request.
# #
# # get "/articles" # # get "/articles"
# request.media_type # => "application/x-www-form-urlencoded" # request.media_type # => "application/x-www-form-urlencoded"
def media_type def media_type
content_mime_type&.to_s content_mime_type&.to_s
end end
@ -285,7 +286,7 @@ def content_length
super.to_i super.to_i
end end
# Returns true if the +X-Requested-With+ header contains "XMLHttpRequest" # Returns true if the `X-Requested-With` header contains "XMLHttpRequest"
# (case-insensitive), which may need to be manually added depending on the # (case-insensitive), which may need to be manually added depending on the
# choice of JavaScript libraries and frameworks. # choice of JavaScript libraries and frameworks.
def xml_http_request? def xml_http_request?
@ -293,13 +294,13 @@ def xml_http_request?
end end
alias :xhr? :xml_http_request? alias :xhr? :xml_http_request?
# Returns the IP address of client as a +String+. # Returns the IP address of client as a `String`.
def ip def ip
@ip ||= super @ip ||= super
end end
# Returns the IP address of client as a +String+, # Returns the IP address of client as a `String`, usually set by the RemoteIp
# usually set by the RemoteIp middleware. # middleware.
def remote_ip def remote_ip
@remote_ip ||= (get_header("action_dispatch.remote_ip") || ip).to_s @remote_ip ||= (get_header("action_dispatch.remote_ip") || ip).to_s
end end
@ -311,12 +312,14 @@ def remote_ip=(remote_ip)
ACTION_DISPATCH_REQUEST_ID = "action_dispatch.request_id" # :nodoc: ACTION_DISPATCH_REQUEST_ID = "action_dispatch.request_id" # :nodoc:
# Returns the unique request id, which is based on either the +X-Request-Id+ header that can # Returns the unique request id, which is based on either the `X-Request-Id`
# be generated by a firewall, load balancer, or web server, or by the RequestId middleware # header that can be generated by a firewall, load balancer, or web server, or
# (which sets the +action_dispatch.request_id+ environment variable). # by the RequestId middleware (which sets the `action_dispatch.request_id`
# environment variable).
# #
# This unique ID is useful for tracing a request from end-to-end as part of logging or debugging. # This unique ID is useful for tracing a request from end-to-end as part of
# This relies on the Rack variable set by the ActionDispatch::RequestId middleware. # logging or debugging. This relies on the Rack variable set by the
# ActionDispatch::RequestId middleware.
def request_id def request_id
get_header ACTION_DISPATCH_REQUEST_ID get_header ACTION_DISPATCH_REQUEST_ID
end end
@ -332,8 +335,8 @@ def server_software
(get_header("SERVER_SOFTWARE") && /^([a-zA-Z]+)/ =~ get_header("SERVER_SOFTWARE")) ? $1.downcase : nil (get_header("SERVER_SOFTWARE") && /^([a-zA-Z]+)/ =~ get_header("SERVER_SOFTWARE")) ? $1.downcase : nil
end end
# Read the request \body. This is useful for web services that need to # Read the request body. This is useful for web services that need to work with
# work with raw requests directly. # raw requests directly.
def raw_post def raw_post
unless has_header? "RAW_POST_DATA" unless has_header? "RAW_POST_DATA"
set_header("RAW_POST_DATA", read_body_stream) set_header("RAW_POST_DATA", read_body_stream)
@ -353,14 +356,13 @@ def body
end end
end end
# Determine whether the request body contains form-data by checking # Determine whether the request body contains form-data by checking the request
# the request +Content-Type+ for one of the media-types: # `Content-Type` for one of the media-types: `application/x-www-form-urlencoded`
# +application/x-www-form-urlencoded+ or +multipart/form-data+. The # or `multipart/form-data`. The list of form-data media types can be modified
# list of form-data media types can be modified through the # through the `FORM_DATA_MEDIA_TYPES` array.
# +FORM_DATA_MEDIA_TYPES+ array.
# #
# A request body is not assumed to contain form-data when no # A request body is not assumed to contain form-data when no `Content-Type`
# +Content-Type+ header is provided and the request_method is POST. # header is provided and the request_method is POST.
def form_data? def form_data?
FORM_DATA_MEDIA_TYPES.include?(media_type) FORM_DATA_MEDIA_TYPES.include?(media_type)
end end
@ -413,8 +415,8 @@ def POST
end end
alias :request_parameters :POST alias :request_parameters :POST
# Returns the authorization header regardless of whether it was specified directly or through one of the # Returns the authorization header regardless of whether it was specified
# proxy alternatives. # directly or through one of the proxy alternatives.
def authorization def authorization
get_header("HTTP_AUTHORIZATION") || get_header("HTTP_AUTHORIZATION") ||
get_header("X-HTTP_AUTHORIZATION") || get_header("X-HTTP_AUTHORIZATION") ||

@ -1,38 +1,40 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
require "active_support/core_ext/module/attribute_accessors" require "active_support/core_ext/module/attribute_accessors"
require "action_dispatch/http/filter_redirect" require "action_dispatch/http/filter_redirect"
require "action_dispatch/http/cache" require "action_dispatch/http/cache"
require "monitor" require "monitor"
module ActionDispatch # :nodoc: module ActionDispatch # :nodoc:
# = Action Dispatch \Response # # Action Dispatch Response
# #
# Represents an HTTP response generated by a controller action. Use it to # Represents an HTTP response generated by a controller action. Use it to
# retrieve the current state of the response, or customize the response. It can # retrieve the current state of the response, or customize the response. It can
# either represent a real HTTP response (i.e. one that is meant to be sent # either represent a real HTTP response (i.e. one that is meant to be sent back
# back to the web browser) or a TestResponse (i.e. one that is generated # to the web browser) or a TestResponse (i.e. one that is generated from
# from integration tests). # integration tests).
# #
# The \Response object for the current request is exposed on controllers as # The Response object for the current request is exposed on controllers as
# ActionController::Metal#response. ActionController::Metal also provides a # ActionController::Metal#response. ActionController::Metal also provides a few
# few additional methods that delegate to attributes of the \Response such as # additional methods that delegate to attributes of the Response such as
# ActionController::Metal#headers. # ActionController::Metal#headers.
# #
# Integration tests will likely also want to inspect responses in # Integration tests will likely also want to inspect responses in more detail.
# more detail. Methods such as Integration::RequestHelpers#get # Methods such as Integration::RequestHelpers#get and
# and Integration::RequestHelpers#post return instances of # Integration::RequestHelpers#post return instances of TestResponse (which
# TestResponse (which inherits from \Response) for this purpose. # inherits from Response) for this purpose.
# #
# For example, the following demo integration test prints the body of the # For example, the following demo integration test prints the body of the
# controller response to the console: # controller response to the console:
# #
# class DemoControllerTest < ActionDispatch::IntegrationTest # class DemoControllerTest < ActionDispatch::IntegrationTest
# def test_print_root_path_to_console # def test_print_root_path_to_console
# get('/') # get('/')
# puts response.body # puts response.body
# end # end
# end # end
class Response class Response
begin begin
# For `Rack::Headers` (Rack 3+): # For `Rack::Headers` (Rack 3+):
@ -55,17 +57,17 @@ class Response
# The headers for the response. # The headers for the response.
# #
# header["Content-Type"] # => "text/plain" # header["Content-Type"] # => "text/plain"
# header["Content-Type"] = "application/json" # header["Content-Type"] = "application/json"
# header["Content-Type"] # => "application/json" # header["Content-Type"] # => "application/json"
# #
# Also aliased as +headers+. # Also aliased as `headers`.
# #
# headers["Content-Type"] # => "text/plain" # headers["Content-Type"] # => "text/plain"
# headers["Content-Type"] = "application/json" # headers["Content-Type"] = "application/json"
# headers["Content-Type"] # => "application/json" # headers["Content-Type"] # => "application/json"
# #
# Also aliased as +header+ for compatibility. # Also aliased as `header` for compatibility.
attr_reader :headers attr_reader :headers
alias_method :header, :headers alias_method :header, :headers
@ -234,13 +236,13 @@ def status=(status)
@status = Rack::Utils.status_code(status) @status = Rack::Utils.status_code(status)
end end
# Sets the HTTP response's content MIME type. For example, in the controller # Sets the HTTP response's content MIME type. For example, in the controller you
# you could write this: # could write this:
# #
# response.content_type = "text/plain" # response.content_type = "text/plain"
# #
# If a character set has been defined for this response (see charset=) then # If a character set has been defined for this response (see charset=) then the
# the character set information will also be included in the content type # character set information will also be included in the content type
# information. # information.
def content_type=(content_type) def content_type=(content_type)
return unless content_type return unless content_type
@ -267,11 +269,11 @@ def sending_file=(v)
end end
end end
# Sets the HTTP character set. In case of +nil+ parameter # Sets the HTTP character set. In case of `nil` parameter it sets the charset to
# it sets the charset to +default_charset+. # `default_charset`.
# #
# response.charset = 'utf-16' # => 'utf-16' # response.charset = 'utf-16' # => 'utf-16'
# response.charset = nil # => 'utf-8' # response.charset = nil # => 'utf-8'
def charset=(charset) def charset=(charset)
content_type = parsed_content_type_header.mime_type content_type = parsed_content_type_header.mime_type
if false == charset if false == charset
@ -281,8 +283,8 @@ def charset=(charset)
end end
end end
# The charset of the response. HTML wants to know the encoding of the # The charset of the response. HTML wants to know the encoding of the content
# content you're giving them, so we need to send that along. # you're giving them, so we need to send that along.
def charset def charset
header_info = parsed_content_type_header header_info = parsed_content_type_header
header_info.charset || self.class.default_charset header_info.charset || self.class.default_charset
@ -293,26 +295,26 @@ def response_code
@status @status
end end
# Returns a string to ensure compatibility with +Net::HTTPResponse+. # Returns a string to ensure compatibility with `Net::HTTPResponse`.
def code def code
@status.to_s @status.to_s
end end
# Returns the corresponding message for the current HTTP status code: # Returns the corresponding message for the current HTTP status code:
# #
# response.status = 200 # response.status = 200
# response.message # => "OK" # response.message # => "OK"
# #
# response.status = 404 # response.status = 404
# response.message # => "Not Found" # response.message # => "Not Found"
# #
def message def message
Rack::Utils::HTTP_STATUS_CODES[@status] Rack::Utils::HTTP_STATUS_CODES[@status]
end end
alias_method :status_message, :message alias_method :status_message, :message
# Returns the content of the response as a string. This contains the contents # Returns the content of the response as a string. This contains the contents of
# of any calls to <tt>render</tt>. # any calls to `render`.
def body def body
@stream.body @stream.body
end end
@ -332,9 +334,9 @@ def body=(body)
end end
end end
# Avoid having to pass an open file handle as the response body. # Avoid having to pass an open file handle as the response body. Rack::Sendfile
# Rack::Sendfile will usually intercept the response and uses # will usually intercept the response and uses the path directly, so there is no
# the path directly, so there is no reason to open the file. # reason to open the file.
class FileBody # :nodoc: class FileBody # :nodoc:
attr_reader :to_path attr_reader :to_path
@ -356,7 +358,7 @@ def each
end end
end end
# Send the file stored at +path+ as the response body. # Send the file stored at `path` as the response body.
def send_file(path) def send_file(path)
commit! commit!
@stream = FileBody.new(path) @stream = FileBody.new(path)
@ -383,17 +385,16 @@ def abort
if stream.respond_to?(:abort) if stream.respond_to?(:abort)
stream.abort stream.abort
elsif stream.respond_to?(:close) elsif stream.respond_to?(:close)
# `stream.close` should really be reserved for a close from the # `stream.close` should really be reserved for a close from the other direction,
# other direction, but we must fall back to it for # but we must fall back to it for compatibility.
# compatibility.
stream.close stream.close
end end
end end
# Turns the Response into a Rack-compatible array of the status, headers, # Turns the Response into a Rack-compatible array of the status, headers, and
# and body. Allows explicit splatting: # body. Allows explicit splatting:
# #
# status, headers, body = *response # status, headers, body = *response
def to_a def to_a
commit! commit!
rack_response @status, @headers.to_hash rack_response @status, @headers.to_hash
@ -402,7 +403,7 @@ def to_a
# Returns the response cookies, converted to a Hash of (name => value) pairs # Returns the response cookies, converted to a Hash of (name => value) pairs
# #
# assert_equal 'AuthorOfNewPage', r.cookies['author'] # assert_equal 'AuthorOfNewPage', r.cookies['author']
def cookies def cookies
cookies = {} cookies = {}
if header = get_header(SET_COOKIE) if header = get_header(SET_COOKIE)
@ -456,11 +457,10 @@ def before_committed
end end
def before_sending def before_sending
# Normally we've already committed by now, but it's possible # Normally we've already committed by now, but it's possible (e.g., if the
# (e.g., if the controller action tries to read back its own # controller action tries to read back its own response) to get here before
# response) to get here before that. In that case, we must force # that. In that case, we must force an "early" commit: we're about to freeze the
# an "early" commit: we're about to freeze the headers, so this is # headers, so this is our last chance.
# our last chance.
commit! unless committed? commit! unless committed?
@request.commit_cookie_jar! unless committed? @request.commit_cookie_jar! unless committed?
@ -488,8 +488,8 @@ def initialize(response)
end end
def close def close
# Rack "close" maps to Response#abort, and *not* Response#close # Rack "close" maps to Response#abort, and **not** Response#close (which is used
# (which is used when the controller's finished writing) # when the controller's finished writing)
@response.abort @response.abort
end end

@ -1,17 +1,19 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
module ActionDispatch module ActionDispatch
module Http module Http
# = Action Dispatch HTTP \UploadedFile # # Action Dispatch HTTP UploadedFile
# #
# Models uploaded files. # Models uploaded files.
# #
# The actual file is accessible via the +tempfile+ accessor, though some # The actual file is accessible via the `tempfile` accessor, though some of its
# of its interface is available directly for convenience. # interface is available directly for convenience.
# #
# Uploaded files are temporary files whose lifespan is one request. When # Uploaded files are temporary files whose lifespan is one request. When the
# the object is finalized Ruby unlinks the file, so there is no need to # object is finalized Ruby unlinks the file, so there is no need to clean them
# clean them with a separate maintenance task. # with a separate maintenance task.
class UploadedFile class UploadedFile
# The basename of the file in the client. # The basename of the file in the client.
attr_accessor :original_filename attr_accessor :original_filename
@ -19,8 +21,8 @@ class UploadedFile
# A string with the MIME type of the file. # A string with the MIME type of the file.
attr_accessor :content_type attr_accessor :content_type
# A +Tempfile+ object with the actual uploaded file. Note that some of # A `Tempfile` object with the actual uploaded file. Note that some of its
# its interface is available directly. # interface is available directly.
attr_accessor :tempfile attr_accessor :tempfile
# A string with the headers of the multipart request. # A string with the headers of the multipart request.
@ -57,42 +59,42 @@ def initialize(hash) # :nodoc:
end end
end end
# Shortcut for +tempfile.read+. # Shortcut for `tempfile.read`.
def read(length = nil, buffer = nil) def read(length = nil, buffer = nil)
@tempfile.read(length, buffer) @tempfile.read(length, buffer)
end end
# Shortcut for +tempfile.open+. # Shortcut for `tempfile.open`.
def open def open
@tempfile.open @tempfile.open
end end
# Shortcut for +tempfile.close+. # Shortcut for `tempfile.close`.
def close(unlink_now = false) def close(unlink_now = false)
@tempfile.close(unlink_now) @tempfile.close(unlink_now)
end end
# Shortcut for +tempfile.path+. # Shortcut for `tempfile.path`.
def path def path
@tempfile.path @tempfile.path
end end
# Shortcut for +tempfile.to_path+. # Shortcut for `tempfile.to_path`.
def to_path def to_path
@tempfile.to_path @tempfile.to_path
end end
# Shortcut for +tempfile.rewind+. # Shortcut for `tempfile.rewind`.
def rewind def rewind
@tempfile.rewind @tempfile.rewind
end end
# Shortcut for +tempfile.size+. # Shortcut for `tempfile.size`.
def size def size
@tempfile.size @tempfile.size
end end
# Shortcut for +tempfile.eof?+. # Shortcut for `tempfile.eof?`.
def eof? def eof?
@tempfile.eof? @tempfile.eof?
end end

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
require "active_support/core_ext/module/attribute_accessors" require "active_support/core_ext/module/attribute_accessors"
module ActionDispatch module ActionDispatch
@ -15,20 +17,20 @@ module URL
class << self class << self
# Returns the domain part of a host given the domain level. # Returns the domain part of a host given the domain level.
# #
# # Top-level domain example # # Top-level domain example
# extract_domain('www.example.com', 1) # => "example.com" # extract_domain('www.example.com', 1) # => "example.com"
# # Second-level domain example # # Second-level domain example
# extract_domain('dev.www.example.co.uk', 2) # => "example.co.uk" # extract_domain('dev.www.example.co.uk', 2) # => "example.co.uk"
def extract_domain(host, tld_length) def extract_domain(host, tld_length)
extract_domain_from(host, tld_length) if named_host?(host) extract_domain_from(host, tld_length) if named_host?(host)
end end
# Returns the subdomains of a host as an Array given the domain level. # Returns the subdomains of a host as an Array given the domain level.
# #
# # Top-level domain example # # Top-level domain example
# extract_subdomains('www.example.com', 1) # => ["www"] # extract_subdomains('www.example.com', 1) # => ["www"]
# # Second-level domain example # # Second-level domain example
# extract_subdomains('dev.www.example.co.uk', 2) # => ["dev", "www"] # extract_subdomains('dev.www.example.co.uk', 2) # => ["dev", "www"]
def extract_subdomains(host, tld_length) def extract_subdomains(host, tld_length)
if named_host?(host) if named_host?(host)
extract_subdomains_from(host, tld_length) extract_subdomains_from(host, tld_length)
@ -39,10 +41,10 @@ def extract_subdomains(host, tld_length)
# Returns the subdomains of a host as a String given the domain level. # Returns the subdomains of a host as a String given the domain level.
# #
# # Top-level domain example # # Top-level domain example
# extract_subdomain('www.example.com', 1) # => "www" # extract_subdomain('www.example.com', 1) # => "www"
# # Second-level domain example # # Second-level domain example
# extract_subdomain('dev.www.example.co.uk', 2) # => "dev.www" # extract_subdomain('dev.www.example.co.uk', 2) # => "dev.www"
def extract_subdomain(host, tld_length) def extract_subdomain(host, tld_length)
extract_subdomains(host, tld_length).join(".") extract_subdomains(host, tld_length).join(".")
end end
@ -184,33 +186,33 @@ def initialize
# Returns the complete URL used for this request. # Returns the complete URL used for this request.
# #
# req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com' # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com'
# req.url # => "http://example.com" # req.url # => "http://example.com"
def url def url
protocol + host_with_port + fullpath protocol + host_with_port + fullpath
end end
# Returns 'https://' if this is an SSL request and 'http://' otherwise. # Returns 'https://' if this is an SSL request and 'http://' otherwise.
# #
# req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com' # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com'
# req.protocol # => "http://" # req.protocol # => "http://"
# #
# req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com', 'HTTPS' => 'on' # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com', 'HTTPS' => 'on'
# req.protocol # => "https://" # req.protocol # => "https://"
def protocol def protocol
@protocol ||= ssl? ? "https://" : "http://" @protocol ||= ssl? ? "https://" : "http://"
end end
# Returns the \host and port for this request, such as "example.com:8080". # Returns the host and port for this request, such as "example.com:8080".
# #
# req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com' # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com'
# req.raw_host_with_port # => "example.com" # req.raw_host_with_port # => "example.com"
# #
# req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:80' # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:80'
# req.raw_host_with_port # => "example.com:80" # req.raw_host_with_port # => "example.com:80"
# #
# req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080' # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080'
# req.raw_host_with_port # => "example.com:8080" # req.raw_host_with_port # => "example.com:8080"
def raw_host_with_port def raw_host_with_port
if forwarded = x_forwarded_host.presence if forwarded = x_forwarded_host.presence
forwarded.split(/,\s?/).last forwarded.split(/,\s?/).last
@ -221,35 +223,35 @@ def raw_host_with_port
# Returns the host for this request, such as "example.com". # Returns the host for this request, such as "example.com".
# #
# req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080' # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080'
# req.host # => "example.com" # req.host # => "example.com"
def host def host
raw_host_with_port.sub(/:\d+$/, "") raw_host_with_port.sub(/:\d+$/, "")
end end
# Returns a \host:\port string for this request, such as "example.com" or # Returns a host:port string for this request, such as "example.com" or
# "example.com:8080". Port is only included if it is not a default port # "example.com:8080". Port is only included if it is not a default port (80 or
# (80 or 443) # 443)
# #
# req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com' # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com'
# req.host_with_port # => "example.com" # req.host_with_port # => "example.com"
# #
# req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:80' # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:80'
# req.host_with_port # => "example.com" # req.host_with_port # => "example.com"
# #
# req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080' # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080'
# req.host_with_port # => "example.com:8080" # req.host_with_port # => "example.com:8080"
def host_with_port def host_with_port
"#{host}#{port_string}" "#{host}#{port_string}"
end end
# Returns the port number of this request as an integer. # Returns the port number of this request as an integer.
# #
# req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com' # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com'
# req.port # => 80 # req.port # => 80
# #
# req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080' # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080'
# req.port # => 8080 # req.port # => 8080
def port def port
@port ||= if raw_host_with_port =~ /:(\d+)$/ @port ||= if raw_host_with_port =~ /:(\d+)$/
$1.to_i $1.to_i
@ -258,10 +260,10 @@ def port
end end
end end
# Returns the standard \port number for this request's protocol. # Returns the standard port number for this request's protocol.
# #
# req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080' # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080'
# req.standard_port # => 80 # req.standard_port # => 80
def standard_port def standard_port
if "https://" == protocol if "https://" == protocol
443 443
@ -272,68 +274,68 @@ def standard_port
# Returns whether this request is using the standard port # Returns whether this request is using the standard port
# #
# req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:80' # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:80'
# req.standard_port? # => true # req.standard_port? # => true
# #
# req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080' # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080'
# req.standard_port? # => false # req.standard_port? # => false
def standard_port? def standard_port?
port == standard_port port == standard_port
end end
# Returns a number \port suffix like 8080 if the \port number of this request # Returns a number port suffix like 8080 if the port number of this request is
# is not the default HTTP \port 80 or HTTPS \port 443. # not the default HTTP port 80 or HTTPS port 443.
# #
# req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:80' # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:80'
# req.optional_port # => nil # req.optional_port # => nil
# #
# req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080' # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080'
# req.optional_port # => 8080 # req.optional_port # => 8080
def optional_port def optional_port
standard_port? ? nil : port standard_port? ? nil : port
end end
# Returns a string \port suffix, including colon, like ":8080" if the \port # Returns a string port suffix, including colon, like ":8080" if the port number
# number of this request is not the default HTTP \port 80 or HTTPS \port 443. # of this request is not the default HTTP port 80 or HTTPS port 443.
# #
# req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:80' # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:80'
# req.port_string # => "" # req.port_string # => ""
# #
# req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080' # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080'
# req.port_string # => ":8080" # req.port_string # => ":8080"
def port_string def port_string
standard_port? ? "" : ":#{port}" standard_port? ? "" : ":#{port}"
end end
# Returns the requested port, such as 8080, based on SERVER_PORT # Returns the requested port, such as 8080, based on SERVER_PORT
# #
# req = ActionDispatch::Request.new 'SERVER_PORT' => '80' # req = ActionDispatch::Request.new 'SERVER_PORT' => '80'
# req.server_port # => 80 # req.server_port # => 80
# #
# req = ActionDispatch::Request.new 'SERVER_PORT' => '8080' # req = ActionDispatch::Request.new 'SERVER_PORT' => '8080'
# req.server_port # => 8080 # req.server_port # => 8080
def server_port def server_port
get_header("SERVER_PORT").to_i get_header("SERVER_PORT").to_i
end end
# Returns the \domain part of a \host, such as "rubyonrails.org" in "www.rubyonrails.org". You can specify # Returns the domain part of a host, such as "rubyonrails.org" in
# a different <tt>tld_length</tt>, such as 2 to catch rubyonrails.co.uk in "www.rubyonrails.co.uk". # "www.rubyonrails.org". You can specify a different `tld_length`, such as 2 to
# catch rubyonrails.co.uk in "www.rubyonrails.co.uk".
def domain(tld_length = @@tld_length) def domain(tld_length = @@tld_length)
ActionDispatch::Http::URL.extract_domain(host, tld_length) ActionDispatch::Http::URL.extract_domain(host, tld_length)
end end
# Returns all the \subdomains as an array, so <tt>["dev", "www"]</tt> would be # Returns all the subdomains as an array, so `["dev", "www"]` would be returned
# returned for "dev.www.rubyonrails.org". You can specify a different <tt>tld_length</tt>, # for "dev.www.rubyonrails.org". You can specify a different `tld_length`, such
# such as 2 to catch <tt>["www"]</tt> instead of <tt>["www", "rubyonrails"]</tt> # as 2 to catch `["www"]` instead of `["www", "rubyonrails"]` in
# in "www.rubyonrails.co.uk". # "www.rubyonrails.co.uk".
def subdomains(tld_length = @@tld_length) def subdomains(tld_length = @@tld_length)
ActionDispatch::Http::URL.extract_subdomains(host, tld_length) ActionDispatch::Http::URL.extract_subdomains(host, tld_length)
end end
# Returns all the \subdomains as a string, so <tt>"dev.www"</tt> would be # Returns all the subdomains as a string, so `"dev.www"` would be returned for
# returned for "dev.www.rubyonrails.org". You can specify a different <tt>tld_length</tt>, # "dev.www.rubyonrails.org". You can specify a different `tld_length`, such as 2
# such as 2 to catch <tt>"www"</tt> instead of <tt>"www.rubyonrails"</tt> # to catch `"www"` instead of `"www.rubyonrails"` in "www.rubyonrails.co.uk".
# in "www.rubyonrails.co.uk".
def subdomain(tld_length = @@tld_length) def subdomain(tld_length = @@tld_length)
ActionDispatch::Http::URL.extract_subdomain(host, tld_length) ActionDispatch::Http::URL.extract_subdomain(host, tld_length)
end end

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
require "action_dispatch/journey/router" require "action_dispatch/journey/router"
require "action_dispatch/journey/gtg/builder" require "action_dispatch/journey/gtg/builder"
require "action_dispatch/journey/gtg/simulator" require "action_dispatch/journey/gtg/simulator"

@ -1,12 +1,14 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
require "action_controller/metal/exceptions" require "action_controller/metal/exceptions"
module ActionDispatch module ActionDispatch
# :stopdoc: # :stopdoc:
module Journey module Journey
# The Formatter class is used for formatting URLs. For example, parameters # The Formatter class is used for formatting URLs. For example, parameters
# passed to +url_for+ in Rails will eventually call Formatter#generate. # passed to `url_for` in Rails will eventually call Formatter#generate.
class Formatter class Formatter
attr_reader :routes attr_reader :routes
@ -66,16 +68,16 @@ def generate(name, options, path_parameters)
match_route(name, constraints) do |route| match_route(name, constraints) do |route|
parameterized_parts = extract_parameterized_parts(route, options, path_parameters) parameterized_parts = extract_parameterized_parts(route, options, path_parameters)
# Skip this route unless a name has been provided or it is a # Skip this route unless a name has been provided or it is a standard Rails
# standard Rails route since we can't determine whether an options # route since we can't determine whether an options hash passed to url_for
# hash passed to url_for matches a Rack application or a redirect. # matches a Rack application or a redirect.
next unless name || route.dispatcher? next unless name || route.dispatcher?
missing_keys = missing_keys(route, parameterized_parts) missing_keys = missing_keys(route, parameterized_parts)
next if missing_keys && !missing_keys.empty? next if missing_keys && !missing_keys.empty?
params = options.delete_if do |key, _| params = options.delete_if do |key, _|
# top-level params' normal behavior of generating query_params # top-level params' normal behavior of generating query_params should be
# should be preserved even if the same key is also a bind_param # preserved even if the same key is also a bind_param
parameterized_parts.key?(key) || route.defaults.key?(key) || parameterized_parts.key?(key) || route.defaults.key?(key) ||
(path_params.key?(key) && !original_options.key?(key)) (path_params.key?(key) && !original_options.key?(key))
end end

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
require "action_dispatch/journey/gtg/transition_table" require "action_dispatch/journey/gtg/transition_table"
module ActionDispatch module ActionDispatch
@ -66,9 +68,8 @@ def nullable?(node)
when Nodes::Group when Nodes::Group
true true
when Nodes::Star when Nodes::Star
# the default star regex is /(.+)/ which is NOT nullable # the default star regex is /(.+)/ which is NOT nullable but since different
# but since different constraints can be provided we must # constraints can be provided we must actually check if this is the case or not.
# actually check if this is the case or not.
node.regexp.match?("") node.regexp.match?("")
when Nodes::Or when Nodes::Or
node.children.any? { |c| nullable?(c) } node.children.any? { |c| nullable?(c) }

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
require "strscan" require "strscan"
module ActionDispatch module ActionDispatch

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
require "action_dispatch/journey/nfa/dot" require "action_dispatch/journey/nfa/dot"
module ActionDispatch module ActionDispatch
@ -55,8 +57,8 @@ def move(t, full_string, start_index, end_index)
t.each { |s, previous_start| t.each { |s, previous_start|
if previous_start.nil? if previous_start.nil?
# In the simple case of a "default" param regex do this fast-path # In the simple case of a "default" param regex do this fast-path and add all
# and add all next states. # next states.
if token_matches_default_component && states = @stdparam_states[s] if token_matches_default_component && states = @stdparam_states[s]
states.each { |re, v| next_states << [v, nil].freeze if !v.nil? } states.each { |re, v| next_states << [v, nil].freeze if !v.nil? }
end end
@ -67,10 +69,10 @@ def move(t, full_string, start_index, end_index)
end end
end end
# For regexes that aren't the "default" style, they may potentially # For regexes that aren't the "default" style, they may potentially not be
# not be terminated by the first "token" [./?], so we need to continue # terminated by the first "token" [./?], so we need to continue to attempt to
# to attempt to match this regexp as well as any successful paths that # match this regexp as well as any successful paths that continue out of it.
# continue out of it. both paths could be valid. # both paths could be valid.
if states = @regexp_states[s] if states = @regexp_states[s]
slice_start = if previous_start.nil? slice_start = if previous_start.nil?
start_index start_index
@ -86,8 +88,8 @@ def move(t, full_string, start_index, end_index)
next_states << [v, nil].freeze if !v.nil? && re.match?(curr_slice) next_states << [v, nil].freeze if !v.nil? && re.match?(curr_slice)
} }
# and regardless, we must continue accepting tokens and retrying this regexp. # and regardless, we must continue accepting tokens and retrying this regexp. we
# we need to remember where we started as well so we can take bigger slices. # need to remember where we started as well so we can take bigger slices.
next_states << [s, slice_start].freeze next_states << [s, slice_start].freeze
end end
} }

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
module ActionDispatch module ActionDispatch
module Journey # :nodoc: module Journey # :nodoc:
module NFA # :nodoc: module NFA # :nodoc:

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
require "action_dispatch/journey/visitors" require "action_dispatch/journey/visitors"
module ActionDispatch module ActionDispatch
@ -21,9 +23,8 @@ def initialize(tree, formatted)
end end
def requirements=(requirements) def requirements=(requirements)
# inject any regexp requirements for `star` nodes so they can be # inject any regexp requirements for `star` nodes so they can be determined
# determined nullable, which requires knowing if the regex accepts an # nullable, which requires knowing if the regex accepts an empty string.
# empty string.
(symbols + stars).each do |node| (symbols + stars).each do |node|
re = requirements[node.to_sym] re = requirements[node.to_sym]
node.regexp = re if re node.regexp = re if re
@ -51,8 +52,8 @@ def visit_tree(formatted)
stars << node stars << node
if formatted != false if formatted != false
# Add a constraint for wildcard route to make it non-greedy and # Add a constraint for wildcard route to make it non-greedy and match the
# match the optional format part of the route by default. # optional format part of the route by default.
wildcard_options[node.name.to_sym] ||= /.+?/m wildcard_options[node.name.to_sym] ||= /.+?/m
end end
end end

@ -1,8 +1,9 @@
# #
# DO NOT MODIFY!!!! # DO NOT MODIFY!!!!
# This file is automatically generated by Racc 1.4.16 # This file is automatically generated by Racc 1.4.16 from
# from Racc grammar file "". # Racc grammar file "".
#
# :markup: markdown
require 'racc/parser.rb' require 'racc/parser.rb'

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
require "action_dispatch/journey/scanner" require "action_dispatch/journey/scanner"
require "action_dispatch/journey/nodes/node" require "action_dispatch/journey/nodes/node"

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
module ActionDispatch module ActionDispatch
module Journey # :nodoc: module Journey # :nodoc:
module Path # :nodoc: module Path # :nodoc:
@ -32,7 +34,8 @@ def eager_load!
end end
def requirements_anchored? def requirements_anchored?
# each required param must not be surrounded by a literal, otherwise it isn't simple to chunk-match the url piecemeal # each required param must not be surrounded by a literal, otherwise it isn't
# simple to chunk-match the url piecemeal
terminals = ast.terminals terminals = ast.terminals
terminals.each_with_index { |s, index| terminals.each_with_index { |s, index|

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
module ActionDispatch module ActionDispatch
# :stopdoc: # :stopdoc:
module Journey module Journey
@ -52,7 +54,7 @@ def self.verb_matcher(verb)
## ##
# +path+ is a path constraint. # +path+ is a path constraint.
# +constraints+ is a hash of constraints to be applied to this route. # `constraints` is a hash of constraints to be applied to this route.
def initialize(name:, app: nil, path:, constraints: {}, required_defaults: [], defaults: {}, request_method_match: nil, precedence: 0, scope_options: {}, internal: false, source_location: nil) def initialize(name:, app: nil, path:, constraints: {}, required_defaults: [], defaults: {}, request_method_match: nil, precedence: 0, scope_options: {}, internal: false, source_location: nil)
@name = name @name = name
@app = app @app = app
@ -82,14 +84,14 @@ def eager_load!
nil nil
end end
# Needed for `bin/rails routes`. Picks up succinctly defined requirements # Needed for `bin/rails routes`. Picks up succinctly defined requirements for a
# for a route, for example route # route, for example route
# #
# get 'photo/:id', :controller => 'photos', :action => 'show', # get 'photo/:id', :controller => 'photos', :action => 'show',
# :id => /[A-Z]\d{5}/ # :id => /[A-Z]\d{5}/
# #
# will have {:controller=>"photos", :action=>"show", :id=>/[A-Z]\d{5}/} # will have {:controller=>"photos", :action=>"show", :[id=>/](A-Z){5}/} as
# as requirements. # requirements.
def requirements def requirements
@defaults.merge(path.requirements).delete_if { |_, v| @defaults.merge(path.requirements).delete_if { |_, v|
/.+?/m == v /.+?/m == v

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
require "action_dispatch/journey/router/utils" require "action_dispatch/journey/router/utils"
require "action_dispatch/journey/routes" require "action_dispatch/journey/routes"
require "action_dispatch/journey/formatter" require "action_dispatch/journey/formatter"
@ -22,8 +24,8 @@ def initialize(routes)
end end
def eager_load! def eager_load!
# Eagerly trigger the simulator's initialization so # Eagerly trigger the simulator's initialization so it doesn't happen during a
# it doesn't happen during a request cycle. # request cycle.
simulator simulator
nil nil
end end

@ -1,19 +1,21 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
module ActionDispatch module ActionDispatch
module Journey # :nodoc: module Journey # :nodoc:
class Router # :nodoc: class Router # :nodoc:
class Utils # :nodoc: class Utils # :nodoc:
# Normalizes URI path. # Normalizes URI path.
# #
# Strips off trailing slash and ensures there is a leading slash. # Strips off trailing slash and ensures there is a leading slash. Also converts
# Also converts downcase URL encoded string to uppercase. # downcase URL encoded string to uppercase.
# #
# normalize_path("/foo") # => "/foo" # normalize_path("/foo") # => "/foo"
# normalize_path("/foo/") # => "/foo" # normalize_path("/foo/") # => "/foo"
# normalize_path("foo") # => "/foo" # normalize_path("foo") # => "/foo"
# normalize_path("") # => "/" # normalize_path("") # => "/"
# normalize_path("/%ab") # => "/%AB" # normalize_path("/%ab") # => "/%AB"
def self.normalize_path(path) def self.normalize_path(path)
path ||= "" path ||= ""
encoding = path.encoding encoding = path.encoding
@ -28,8 +30,7 @@ def self.normalize_path(path)
path.force_encoding(encoding) path.force_encoding(encoding)
end end
# URI path and fragment escaping # URI path and fragment escaping https://tools.ietf.org/html/rfc3986
# https://tools.ietf.org/html/rfc3986
class UriEncoder # :nodoc: class UriEncoder # :nodoc:
ENCODE = "%%%02X" ENCODE = "%%%02X"
US_ASCII = Encoding::US_ASCII US_ASCII = Encoding::US_ASCII
@ -93,8 +94,8 @@ def self.escape_fragment(fragment)
# Replaces any escaped sequences with their unescaped representations. # Replaces any escaped sequences with their unescaped representations.
# #
# uri = "/topics?title=Ruby%20on%20Rails" # uri = "/topics?title=Ruby%20on%20Rails"
# unescape_uri(uri) #=> "/topics?title=Ruby on Rails" # unescape_uri(uri) #=> "/topics?title=Ruby on Rails"
def self.unescape_uri(uri) def self.unescape_uri(uri)
ENCODER.unescape_uri(uri) ENCODER.unescape_uri(uri)
end end

@ -1,9 +1,11 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
module ActionDispatch module ActionDispatch
module Journey # :nodoc: module Journey # :nodoc:
# The Routing table. Contains all routes for a system. Routes can be # The Routing table. Contains all routes for a system. Routes can be added to
# added to the table by calling Routes#add_route. # the table by calling Routes#add_route.
class Routes # :nodoc: class Routes # :nodoc:
include Enumerable include Enumerable

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
require "strscan" require "strscan"
module ActionDispatch module ActionDispatch
@ -33,8 +35,8 @@ def next_token
end end
private private
# takes advantage of String @- deduping capabilities in Ruby 2.5 upwards # takes advantage of String @- deduping capabilities in Ruby 2.5 upwards see:
# see: https://bugs.ruby-lang.org/issues/13077 # https://bugs.ruby-lang.org/issues/13077
def dedup_scan(regex) def dedup_scan(regex)
r = @ss.scan(regex) r = @ss.scan(regex)
r ? -r : nil r ? -r : nil

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
module ActionDispatch module ActionDispatch
# :stopdoc: # :stopdoc:
module Journey module Journey

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
module ActionDispatch module ActionDispatch
class LogSubscriber < ActiveSupport::LogSubscriber class LogSubscriber < ActiveSupport::LogSubscriber
def redirect(event) def redirect(event)

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
require "uri" require "uri"
require "active_support/actionable_error" require "active_support/actionable_error"

@ -1,12 +1,15 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
module ActionDispatch module ActionDispatch
# = Action Dispatch \AssumeSSL # # Action Dispatch AssumeSSL
# #
# When proxying through a load balancer that terminates SSL, the forwarded request will appear # When proxying through a load balancer that terminates SSL, the forwarded
# as though it's HTTP instead of HTTPS to the application. This makes redirects and cookie # request will appear as though it's HTTP instead of HTTPS to the application.
# security target HTTP instead of HTTPS. This middleware makes the server assume that the # This makes redirects and cookie security target HTTP instead of HTTPS. This
# proxy already terminated SSL, and that the request really is HTTPS. # middleware makes the server assume that the proxy already terminated SSL, and
# that the request really is HTTPS.
class AssumeSSL class AssumeSSL
def initialize(app) def initialize(app)
@app = app @app = app

@ -1,7 +1,9 @@
# frozen_string_literal: true # frozen_string_literal: true
# :markup: markdown
module ActionDispatch module ActionDispatch
# = Action Dispatch \Callbacks # # Action Dispatch Callbacks
# #
# Provides callbacks to be executed before and after dispatching the request. # Provides callbacks to be executed before and after dispatching the request.
class Callbacks class Callbacks

Some files were not shown because too many files have changed in this diff Show More