All Files ( 48.67% covered at 54.03 hits/line )
143 files in total.
9268 relevant lines,
4511 lines covered and
4757 lines missed.
(
48.67%
)
# frozen_string_literal: true
- 1
require "action_pack"
- 1
require "active_support"
- 1
require "active_support/rails"
- 1
require "active_support/i18n"
- 1
module AbstractController
- 1
extend ActiveSupport::Autoload
- 1
autoload :ActionNotFound, "abstract_controller/base"
- 1
autoload :Base
- 1
autoload :Caching
- 1
autoload :Callbacks
- 1
autoload :Collector
- 1
autoload :DoubleRenderError, "abstract_controller/rendering"
- 1
autoload :Helpers
- 1
autoload :Logger
- 1
autoload :Rendering
- 1
autoload :Translation
- 1
autoload :AssetPaths
- 1
autoload :UrlFor
- 1
def self.eager_load!
super
AbstractController::Caching.eager_load!
end
end
# frozen_string_literal: true
- 1
module AbstractController
- 1
module AssetPaths #:nodoc:
- 1
extend ActiveSupport::Concern
- 1
included do
- 1
config_accessor :asset_host, :assets_dir, :javascripts_dir,
:stylesheets_dir, :default_asset_host_protocol, :relative_url_root
end
end
end
# frozen_string_literal: true
- 1
require "abstract_controller/error"
- 1
require "active_support/configurable"
- 1
require "active_support/descendants_tracker"
- 1
require "active_support/core_ext/module/anonymous"
- 1
require "active_support/core_ext/module/attr_internal"
- 1
module AbstractController
# Raised when a non-existing controller action is triggered.
- 1
class ActionNotFound < StandardError
- 1
attr_reader :controller, :action
- 1
def initialize(message = nil, controller = nil, action = nil)
@controller = controller
@action = action
super(message)
end
- 1
class Correction
- 1
def initialize(error)
@error = error
end
- 1
def corrections
if @error.action
maybe_these = @error.controller.class.action_methods
maybe_these.sort_by { |n|
DidYouMean::Jaro.distance(@error.action.to_s, n)
}.reverse.first(4)
else
[]
end
end
end
# We may not have DYM, and DYM might not let us register error handlers
- 1
if defined?(DidYouMean) && DidYouMean.respond_to?(:correct_error)
DidYouMean.correct_error(self, Correction)
end
end
# AbstractController::Base is a low-level API. Nobody should be
# using it directly, and subclasses (like ActionController::Base) are
# expected to provide their own +render+ method, since rendering means
# different things depending on the context.
- 1
class Base
##
# Returns the body of the HTTP response sent by the controller.
- 1
attr_internal :response_body
##
# Returns the name of the action this controller is processing.
- 1
attr_internal :action_name
##
# Returns the formats that can be processed by the controller.
- 1
attr_internal :formats
- 1
include ActiveSupport::Configurable
- 1
extend ActiveSupport::DescendantsTracker
- 1
class << self
- 1
attr_reader :abstract
- 1
alias_method :abstract?, :abstract
# Define a controller as abstract. See internal_methods for more
# details.
- 1
def abstract!
- 6
@abstract = true
end
- 1
def inherited(klass) # :nodoc:
# Define the abstract ivar on subclasses so that we don't get
# uninitialized ivar warnings
- 314
unless klass.instance_variable_defined?(:@abstract)
- 314
klass.instance_variable_set(:@abstract, false)
end
- 314
super
end
# A list of all internal methods for a controller. This finds the first
# abstract superclass of a controller, and gets a list of all public
# instance methods on that abstract class. Public instance methods of
# a controller would normally be considered action methods, so methods
# declared on abstract classes are being removed.
# (<tt>ActionController::Metal</tt> and ActionController::Base are defined as abstract)
- 1
def internal_methods
controller = self
controller = controller.superclass until controller.abstract?
controller.public_instance_methods(true)
end
# A list of method names that should be considered actions. This
# includes all public instance methods on a controller, less
# any internal methods (see internal_methods), adding back in
# any methods that are internal, but still exist on the class
# itself.
#
# ==== Returns
# * <tt>Set</tt> - A set of all methods that should be considered actions.
- 1
def action_methods
@action_methods ||= begin
# All public instance methods of this class, including ancestors
methods = (public_instance_methods(true) -
# Except for public instance methods of Base and its ancestors
internal_methods +
# Be sure to include shadowed public instance methods of this class
public_instance_methods(false))
methods.map!(&:to_s)
methods.to_set
end
end
# action_methods are cached and there is sometimes a need to refresh
# them. ::clear_action_methods! allows you to do that, so next time
# you run action_methods, they will be recalculated.
- 1
def clear_action_methods!
- 1639
@action_methods = nil
end
# Returns the full controller name, underscored, without the ending Controller.
#
# class MyApp::MyPostsController < AbstractController::Base
#
# end
#
# MyApp::MyPostsController.controller_path # => "my_app/my_posts"
#
# ==== Returns
# * <tt>String</tt>
- 1
def controller_path
- 556
@controller_path ||= name.delete_suffix("Controller").underscore unless anonymous?
end
# Refresh the cached action_methods when a new action_method is added.
- 1
def method_added(name)
- 1639
super
- 1639
clear_action_methods!
end
end
- 1
abstract!
# Calls the action going through the entire action dispatch stack.
#
# The actual method that is called is determined by calling
# #method_for_action. If no method can handle the action, then an
# AbstractController::ActionNotFound error is raised.
#
# ==== Returns
# * <tt>self</tt>
- 1
def process(action, *args)
@_action_name = action.to_s
unless action_name = _find_action_name(@_action_name)
raise ActionNotFound.new("The action '#{action}' could not be found for #{self.class.name}", self, action)
end
@_response_body = nil
process_action(action_name, *args)
end
# Delegates to the class' ::controller_path
- 1
def controller_path
self.class.controller_path
end
# Delegates to the class' ::action_methods
- 1
def action_methods
self.class.action_methods
end
# Returns true if a method for the action is available and
# can be dispatched, false otherwise.
#
# Notice that <tt>action_methods.include?("foo")</tt> may return
# false and <tt>available_action?("foo")</tt> returns true because
# this method considers actions that are also available
# through other means, for example, implicit render ones.
#
# ==== Parameters
# * <tt>action_name</tt> - The name of an action to be tested
- 1
def available_action?(action_name)
_find_action_name(action_name)
end
# Tests if a response body is set. Used to determine if the
# +process_action+ callback needs to be terminated in
# +AbstractController::Callbacks+.
- 1
def performed?
response_body
end
# Returns true if the given controller is capable of rendering
# a path. A subclass of +AbstractController::Base+
# may return false. An Email controller for example does not
# support paths, only full URLs.
- 1
def self.supports_path?
true
end
- 1
private
# Returns true if the name can be considered an action because
# it has a method defined in the controller.
#
# ==== Parameters
# * <tt>name</tt> - The name of an action to be tested
- 1
def action_method?(name)
self.class.action_methods.include?(name)
end
# Call the action. Override this in a subclass to modify the
# behavior around processing an action. This, and not #process,
# is the intended way to override action dispatching.
#
# Notice that the first argument is the method to be dispatched
# which is *not* necessarily the same as the action name.
- 1
def process_action(method_name, *args)
send_action(method_name, *args)
end
# Actually call the method associated with the action. Override
# this method if you wish to change how action methods are called,
# not to add additional behavior around it. For example, you would
# override #send_action if you want to inject arguments into the
# method.
- 1
alias send_action send
# If the action name was not found, but a method called "action_missing"
# was found, #method_for_action will return "_handle_action_missing".
# This method calls #action_missing with the current action name.
- 1
def _handle_action_missing(*args)
action_missing(@_action_name, *args)
end
# Takes an action name and returns the name of the method that will
# handle the action.
#
# It checks if the action name is valid and returns false otherwise.
#
# See method_for_action for more information.
#
# ==== Parameters
# * <tt>action_name</tt> - An action name to find a method name for
#
# ==== Returns
# * <tt>string</tt> - The name of the method that handles the action
# * false - No valid method name could be found.
# Raise +AbstractController::ActionNotFound+.
- 1
def _find_action_name(action_name)
_valid_action_name?(action_name) && method_for_action(action_name)
end
# Takes an action name and returns the name of the method that will
# handle the action. In normal cases, this method returns the same
# name as it receives. By default, if #method_for_action receives
# a name that is not an action, it will look for an #action_missing
# method and return "_handle_action_missing" if one is found.
#
# Subclasses may override this method to add additional conditions
# that should be considered an action. For instance, an HTTP controller
# with a template matching the action name is considered to exist.
#
# If you override this method to handle additional cases, you may
# also provide a method (like +_handle_method_missing+) to handle
# the case.
#
# If none of these conditions are true, and +method_for_action+
# returns +nil+, an +AbstractController::ActionNotFound+ exception will be raised.
#
# ==== Parameters
# * <tt>action_name</tt> - An action name to find a method name for
#
# ==== Returns
# * <tt>string</tt> - The name of the method that handles the action
# * <tt>nil</tt> - No method name could be found.
- 1
def method_for_action(action_name)
if action_method?(action_name)
action_name
elsif respond_to?(:action_missing, true)
"_handle_action_missing"
end
end
# Checks if the action name is valid and returns false otherwise.
- 1
def _valid_action_name?(action_name)
!action_name.to_s.include? File::SEPARATOR
end
end
end
# frozen_string_literal: true
- 1
module AbstractController
- 1
module Caching
- 1
extend ActiveSupport::Concern
- 1
extend ActiveSupport::Autoload
- 1
eager_autoload do
- 1
autoload :Fragments
end
- 1
module ConfigMethods
- 1
def cache_store
config.cache_store
end
- 1
def cache_store=(store)
- 1
config.cache_store = ActiveSupport::Cache.lookup_store(*store)
end
- 1
private
- 1
def cache_configured?
perform_caching && cache_store
end
end
- 1
include ConfigMethods
- 1
include AbstractController::Caching::Fragments
- 1
included do
- 2
extend ConfigMethods
- 2
config_accessor :default_static_extension
- 2
self.default_static_extension ||= ".html"
- 2
config_accessor :perform_caching
- 2
self.perform_caching = true if perform_caching.nil?
- 2
config_accessor :enable_fragment_cache_logging
- 2
self.enable_fragment_cache_logging = false
- 2
class_attribute :_view_cache_dependencies, default: []
- 2
helper_method :view_cache_dependencies if respond_to?(:helper_method)
end
- 1
module ClassMethods
- 1
def view_cache_dependency(&dependency)
- 2
self._view_cache_dependencies += [dependency]
end
end
- 1
def view_cache_dependencies
self.class._view_cache_dependencies.map { |dep| instance_exec(&dep) }.compact
end
- 1
private
# Convenience accessor.
- 1
def cache(key, options = {}, &block) # :doc:
if cache_configured?
cache_store.fetch(ActiveSupport::Cache.expand_cache_key(key, :controller), options, &block)
else
yield
end
end
end
end
# frozen_string_literal: true
- 1
module AbstractController
- 1
module Caching
# Fragment caching is used for caching various blocks within
# views without caching the entire action as a whole. This is
# useful when certain elements of an action change frequently or
# depend on complicated state while other parts rarely change or
# can be shared amongst multiple parties. The caching is done using
# the +cache+ helper available in the Action View. See
# ActionView::Helpers::CacheHelper for more information.
#
# While it's strongly recommended that you use key-based cache
# expiration (see links in CacheHelper for more information),
# it is also possible to manually expire caches. For example:
#
# expire_fragment('name_of_cache')
- 1
module Fragments
- 1
extend ActiveSupport::Concern
- 1
included do
- 2
if respond_to?(:class_attribute)
- 2
class_attribute :fragment_cache_keys
else
mattr_writer :fragment_cache_keys
end
- 2
self.fragment_cache_keys = []
- 2
if respond_to?(:helper_method)
- 1
helper_method :combined_fragment_cache_key
end
end
- 1
module ClassMethods
# Allows you to specify controller-wide key prefixes for
# cache fragments. Pass either a constant +value+, or a block
# which computes a value each time a cache key is generated.
#
# For example, you may want to prefix all fragment cache keys
# with a global version identifier, so you can easily
# invalidate all caches.
#
# class ApplicationController
# fragment_cache_key "v1"
# end
#
# When it's time to invalidate all fragments, simply change
# the string constant. Or, progressively roll out the cache
# invalidation using a computed value:
#
# class ApplicationController
# fragment_cache_key do
# @account.id.odd? ? "v1" : "v2"
# end
# end
- 1
def fragment_cache_key(value = nil, &key)
- 2
self.fragment_cache_keys += [key || -> { value }]
end
end
# Given a key (as described in +expire_fragment+), returns
# a key array suitable for use in reading, writing, or expiring a
# cached fragment. All keys begin with <tt>:views</tt>,
# followed by <tt>ENV["RAILS_CACHE_ID"]</tt> or <tt>ENV["RAILS_APP_VERSION"]</tt> if set,
# followed by any controller-wide key prefix values, ending
# with the specified +key+ value.
- 1
def combined_fragment_cache_key(key)
head = self.class.fragment_cache_keys.map { |k| instance_exec(&k) }
tail = key.is_a?(Hash) ? url_for(key).split("://").last : key
cache_key = [:views, ENV["RAILS_CACHE_ID"] || ENV["RAILS_APP_VERSION"], head, tail]
cache_key.flatten!(1)
cache_key.compact!
cache_key
end
# Writes +content+ to the location signified by
# +key+ (see +expire_fragment+ for acceptable formats).
- 1
def write_fragment(key, content, options = nil)
return content unless cache_configured?
key = combined_fragment_cache_key(key)
instrument_fragment_cache :write_fragment, key do
content = content.to_str
cache_store.write(key, content, options)
end
content
end
# Reads a cached fragment from the location signified by +key+
# (see +expire_fragment+ for acceptable formats).
- 1
def read_fragment(key, options = nil)
return unless cache_configured?
key = combined_fragment_cache_key(key)
instrument_fragment_cache :read_fragment, key do
result = cache_store.read(key, options)
result.respond_to?(:html_safe) ? result.html_safe : result
end
end
# Check if a cached fragment from the location signified by
# +key+ exists (see +expire_fragment+ for acceptable formats).
- 1
def fragment_exist?(key, options = nil)
return unless cache_configured?
key = combined_fragment_cache_key(key)
instrument_fragment_cache :exist_fragment?, key do
cache_store.exist?(key, options)
end
end
# Removes fragments from the cache.
#
# +key+ can take one of three forms:
#
# * String - This would normally take the form of a path, like
# <tt>pages/45/notes</tt>.
# * Hash - Treated as an implicit call to +url_for+, like
# <tt>{ controller: 'pages', action: 'notes', id: 45}</tt>
# * Regexp - Will remove any fragment that matches, so
# <tt>%r{pages/\d*/notes}</tt> might remove all notes. Make sure you
# don't use anchors in the regex (<tt>^</tt> or <tt>$</tt>) because
# the actual filename matched looks like
# <tt>./cache/filename/path.cache</tt>. Note: Regexp expiration is
# 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).
- 1
def expire_fragment(key, options = nil)
return unless cache_configured?
key = combined_fragment_cache_key(key) unless key.is_a?(Regexp)
instrument_fragment_cache :expire_fragment, key do
if key.is_a?(Regexp)
cache_store.delete_matched(key, options)
else
cache_store.delete(key, options)
end
end
end
- 1
def instrument_fragment_cache(name, key) # :nodoc:
ActiveSupport::Notifications.instrument("#{name}.#{instrument_name}", instrument_payload(key)) { yield }
end
end
end
end
# frozen_string_literal: true
- 1
module AbstractController
# = Abstract Controller Callbacks
#
# Abstract Controller provides hooks during the life cycle of a controller action.
# Callbacks allow you to trigger logic during this cycle. Available callbacks are:
#
# * <tt>after_action</tt>
# * <tt>append_after_action</tt>
# * <tt>append_around_action</tt>
# * <tt>append_before_action</tt>
# * <tt>around_action</tt>
# * <tt>before_action</tt>
# * <tt>prepend_after_action</tt>
# * <tt>prepend_around_action</tt>
# * <tt>prepend_before_action</tt>
# * <tt>skip_after_action</tt>
# * <tt>skip_around_action</tt>
# * <tt>skip_before_action</tt>
#
# NOTE: Calling the same callback multiple times will overwrite previous callback definitions.
#
- 1
module Callbacks
- 1
extend ActiveSupport::Concern
# Uses ActiveSupport::Callbacks as the base functionality. For
# more details on the whole callback system, read the documentation
# for ActiveSupport::Callbacks.
- 1
include ActiveSupport::Callbacks
- 1
included do
- 3
define_callbacks :process_action,
terminator: ->(controller, result_lambda) { result_lambda.call; controller.performed? },
skip_after_callbacks_if_terminated: true
end
# Override <tt>AbstractController::Base#process_action</tt> to run the
# <tt>process_action</tt> callbacks around the normal behavior.
- 1
def process_action(*)
run_callbacks(:process_action) do
super
end
end
- 1
module ClassMethods
# If +:only+ or +:except+ are used, convert the options into the
# +:if+ and +:unless+ options of ActiveSupport::Callbacks.
#
# The basic idea is that <tt>:only => :index</tt> gets converted to
# <tt>:if => proc {|c| c.action_name == "index" }</tt>.
#
# Note that <tt>:only</tt> has priority over <tt>:if</tt> in case they
# are used together.
#
# only: :index, if: -> { true } # the :if option will be ignored.
#
# Note that <tt>:if</tt> has priority over <tt>:except</tt> in case they
# are used together.
#
# except: :index, if: -> { true } # the :except option will be ignored.
#
# ==== 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.
- 1
def _normalize_callback_options(options)
- 167
_normalize_callback_option(options, :only, :if)
- 167
_normalize_callback_option(options, :except, :unless)
end
- 1
def _normalize_callback_option(options, from, to) # :nodoc:
- 334
if from = options.delete(from)
- 73
_from = Array(from).map(&:to_s).to_set
- 73
from = proc { |c| _from.include? c.action_name }
- 73
options[to] = Array(options[to]).unshift(from)
end
end
# Take callback names and an optional callback proc, normalize them,
# then call the block with each callback. This allows us to abstract
# the normalization across several methods that use it.
#
# ==== Parameters
# * <tt>callbacks</tt> - An array of callbacks, with an optional
# options hash as the last parameter.
# * <tt>block</tt> - A proc that should be added to the callbacks.
#
# ==== Block Parameters
# * <tt>name</tt> - The callback to be added.
# * <tt>options</tt> - A hash of options to be used when adding the callback.
- 1
def _insert_callbacks(callbacks, block = nil)
- 167
options = callbacks.extract_options!
- 167
_normalize_callback_options(options)
- 167
callbacks.push(block) if block
- 167
callbacks.each do |callback|
- 172
yield callback, options
end
end
##
# :method: before_action
#
# :call-seq: before_action(names, block)
#
# Append a callback before actions. See _insert_callbacks for parameter details.
#
# If the callback renders or redirects, the action will not run. If there
# are additional callbacks scheduled to run after that callback, they are
# also cancelled.
##
# :method: prepend_before_action
#
# :call-seq: prepend_before_action(names, block)
#
# Prepend a callback before actions. See _insert_callbacks for parameter details.
#
# If the callback renders or redirects, the action will not run. If there
# are additional callbacks scheduled to run after that callback, they are
# also cancelled.
##
# :method: skip_before_action
#
# :call-seq: skip_before_action(names)
#
# Skip a callback before actions. See _insert_callbacks for parameter details.
##
# :method: append_before_action
#
# :call-seq: append_before_action(names, block)
#
# Append a callback before actions. See _insert_callbacks for parameter details.
#
# If the callback renders or redirects, the action will not run. If there
# are additional callbacks scheduled to run after that callback, they are
# also cancelled.
##
# :method: after_action
#
# :call-seq: after_action(names, block)
#
# Append a callback after actions. See _insert_callbacks for parameter details.
##
# :method: prepend_after_action
#
# :call-seq: prepend_after_action(names, block)
#
# Prepend a callback after actions. See _insert_callbacks for parameter details.
##
# :method: skip_after_action
#
# :call-seq: skip_after_action(names)
#
# Skip a callback after actions. See _insert_callbacks for parameter details.
##
# :method: append_after_action
#
# :call-seq: append_after_action(names, block)
#
# Append a callback after actions. See _insert_callbacks for parameter details.
##
# :method: around_action
#
# :call-seq: around_action(names, block)
#
# Append a callback around actions. See _insert_callbacks for parameter details.
##
# :method: prepend_around_action
#
# :call-seq: prepend_around_action(names, block)
#
# Prepend a callback around actions. See _insert_callbacks for parameter details.
##
# :method: skip_around_action
#
# :call-seq: skip_around_action(names)
#
# Skip a callback around actions. See _insert_callbacks for parameter details.
##
# :method: append_around_action
#
# :call-seq: append_around_action(names, block)
#
# Append a callback around actions. See _insert_callbacks for parameter details.
# set up before_action, prepend_before_action, skip_before_action, etc.
# for each of before, after, and around.
- 1
[:before, :after, :around].each do |callback|
- 3
define_method "#{callback}_action" do |*names, &blk|
- 145
_insert_callbacks(names, blk) do |name, options|
- 150
set_callback(:process_action, callback, name, options)
end
end
- 3
define_method "prepend_#{callback}_action" do |*names, &blk|
- 6
_insert_callbacks(names, blk) do |name, options|
- 6
set_callback(:process_action, callback, name, options.merge(prepend: true))
end
end
# Skip a before, after or around callback. See _insert_callbacks
# for details on the allowed parameters.
- 3
define_method "skip_#{callback}_action" do |*names|
- 16
_insert_callbacks(names) do |name, options|
- 16
skip_callback(:process_action, callback, name, options)
end
end
# *_action is the same as append_*_action
- 3
alias_method :"append_#{callback}_action", :"#{callback}_action"
end
end
end
end
# frozen_string_literal: true
- 1
require "action_dispatch/http/mime_type"
- 1
module AbstractController
- 1
module Collector
- 1
def self.generate_method_for_mime(mime)
- 34
sym = mime.is_a?(Symbol) ? mime : mime.to_sym
- 34
class_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{sym}(*args, &block)
custom(Mime[:#{sym}], *args, &block)
end
RUBY
end
- 1
Mime::SET.each do |mime|
- 34
generate_method_for_mime(mime)
end
- 1
Mime::Type.register_callback do |mime|
generate_method_for_mime(mime) unless instance_methods.include?(mime.to_sym)
end
- 1
private
- 1
def method_missing(symbol, &block)
unless mime_constant = Mime[symbol]
raise NoMethodError, "To respond to a custom format, register it as a MIME type first: " \
"https://guides.rubyonrails.org/action_controller_overview.html#restful-downloads. " \
"If you meant to respond to a variant like :tablet or :phone, not a custom format, " \
"be sure to nest your variant response within a format response: " \
"format.html { |html| html.tablet { ... } }"
end
if Mime::SET.include?(mime_constant)
AbstractController::Collector.generate_method_for_mime(mime_constant)
send(symbol, &block)
else
super
end
end
end
end
# frozen_string_literal: true
- 1
module AbstractController
- 1
class Error < StandardError #:nodoc:
end
end
# frozen_string_literal: true
- 1
require "active_support/dependencies"
- 1
module AbstractController
- 1
module Helpers
- 1
extend ActiveSupport::Concern
- 1
included do
- 2
class_attribute :_helpers, default: define_helpers_module(self)
- 2
class_attribute :_helper_methods, default: Array.new
end
- 1
class MissingHelperError < LoadError
- 1
def initialize(error, path)
@error = error
@path = "helpers/#{path}.rb"
set_backtrace error.backtrace
if /^#{path}(\.rb)?$/.match?(error.path)
super("Missing helper file helpers/%s.rb" % path)
else
raise error
end
end
end
- 1
module ClassMethods
# When a class is inherited, wrap its helper module in a new module.
# This ensures that the parent class's module can be changed
# independently of the child class's.
- 1
def inherited(klass)
- 272
helpers = _helpers
- 272
klass._helpers = define_helpers_module(klass, helpers)
- 540
klass.class_eval { default_helper_module! } unless klass.anonymous?
- 272
super
end
# Declare a controller method as a helper. For example, the following
# makes the +current_user+ and +logged_in?+ controller methods available
# to the view:
# class ApplicationController < ActionController::Base
# helper_method :current_user, :logged_in?
#
# def current_user
# @current_user ||= User.find_by(id: session[:user])
# end
#
# def logged_in?
# current_user != nil
# end
# end
#
# In a view:
# <% if logged_in? -%>Welcome, <%= current_user.name %><% end -%>
#
# ==== Parameters
# * <tt>method[, method]</tt> - A name or names of a method on the controller
# to be made available on the view.
- 1
def helper_method(*methods)
- 22
methods.flatten!
- 22
self._helper_methods += methods
- 22
location = caller_locations(1, 1).first
- 22
file, line = location.path, location.lineno
- 22
methods.each do |method|
- 22
_helpers.class_eval <<-ruby_eval, file, line
def #{method}(*args, &block) # def current_user(*args, &block)
controller.send(:'#{method}', *args, &block) # controller.send(:'current_user', *args, &block)
end # end
ruby2_keywords(:'#{method}') if respond_to?(:ruby2_keywords, true)
ruby_eval
end
end
# Includes the given modules in the template class.
#
# Modules can be specified in different ways. All of the following calls
# include +FooHelper+:
#
# # Module, recommended.
# helper FooHelper
#
# # String/symbol without the "helper" suffix, camel or snake case.
# helper "Foo"
# helper :Foo
# helper "foo"
# helper :foo
#
# The last two assume that <tt>"foo".camelize</tt> returns "Foo".
#
# When strings or symbols are passed, the method finds the actual module
# object using +String#constantize+. Therefore, if the module has not been
# yet loaded, it has to be autoloadable, which is normally the case.
#
# Namespaces are supported. The following calls include +Foo::BarHelper+:
#
# # Module, recommended.
# helper Foo::BarHelper
#
# # String/symbol without the "helper" suffix, camel or snake case.
# 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 method accepts a block too. If present, the block is evaluated in
# the context of the controller helper module. This simple call makes the
# +wadus+ method available in templates of the enclosing controller:
#
# helper do
# def wadus
# "wadus"
# end
# end
#
# Furthermore, all the above styles can be mixed together:
#
# helper FooHelper, "woo", "bar/baz" do
# def wadus
# "wadus"
# end
# end
#
- 1
def helper(*args, &block)
- 274
modules_for_helpers(args).each do |mod|
- 14
_helpers.include(mod)
end
- 11
_helpers.module_eval(&block) if block_given?
end
# Clears up all existing helpers in this class, only keeping the helper
# with the same name as this class.
- 1
def clear_helpers
- 1
inherited_helper_methods = _helper_methods
- 1
self._helpers = Module.new
- 1
self._helper_methods = Array.new
- 11
inherited_helper_methods.each { |meth| helper_method meth }
- 1
default_helper_module! unless anonymous?
end
# Given an array of values like the ones accepted by +helper+, this method
# returns an array with the corresponding modules, in the same order.
- 1
def modules_for_helpers(modules_or_helper_prefixes)
- 274
modules_or_helper_prefixes.flatten.map! do |module_or_helper_prefix|
- 277
case module_or_helper_prefix
when Module
- 1
module_or_helper_prefix
when String, Symbol
- 276
helper_prefix = module_or_helper_prefix.to_s
- 276
helper_prefix = helper_prefix.camelize unless helper_prefix.start_with?(/[A-Z]/)
- 276
"#{helper_prefix}Helper".constantize
else
raise ArgumentError, "helper must be a String, Symbol, or Module"
end
end
end
- 1
private
- 1
def define_helpers_module(klass, helpers = nil)
# In some tests inherited is called explicitly. In that case, just
# return the module from the first time it was defined
- 274
return klass.const_get(:HelperMethods) if klass.const_defined?(:HelperMethods, false)
- 274
mod = Module.new
- 274
klass.const_set(:HelperMethods, mod)
- 274
mod.include(helpers) if helpers
- 274
mod
end
- 1
def default_helper_module!
- 269
helper_prefix = name.delete_suffix("Controller")
- 269
helper(helper_prefix)
rescue NameError => e
- 263
raise unless e.missing_name?("#{helper_prefix}Helper")
end
end
end
end
# frozen_string_literal: true
- 1
require "active_support/benchmarkable"
- 1
module AbstractController
- 1
module Logger #:nodoc:
- 1
extend ActiveSupport::Concern
- 1
included do
- 2
config_accessor :logger
- 2
include ActiveSupport::Benchmarkable
end
end
end
# frozen_string_literal: true
- 1
module AbstractController
- 1
module Railties
- 1
module RoutesHelpers
- 1
def self.with(routes, include_path_helpers = true)
- 3
Module.new do
- 3
define_method(:inherited) do |klass|
- 284
super(klass)
- 751
if namespace = klass.module_parents.detect { |m| m.respond_to?(:railtie_routes_url_helpers) }
- 1
klass.include(namespace.railtie_routes_url_helpers(include_path_helpers))
else
- 283
klass.include(routes.url_helpers(include_path_helpers))
end
end
end
end
end
end
end
# frozen_string_literal: true
- 1
require "abstract_controller/error"
- 1
require "action_view"
- 1
require "action_view/view_paths"
- 1
require "set"
- 1
module AbstractController
- 1
class DoubleRenderError < Error
- 1
DEFAULT_MESSAGE = "Render and/or redirect were called multiple times in this action. Please note that you may only call render OR redirect, and at most once per action. Also note that neither redirect nor render terminate execution of the action, so if you want to exit an action after redirecting, you need to do something like \"redirect_to(...) and return\"."
- 1
def initialize(message = nil)
super(message || DEFAULT_MESSAGE)
end
end
- 1
module Rendering
- 1
extend ActiveSupport::Concern
- 1
include ActionView::ViewPaths
# Normalizes arguments, options and then delegates render_to_body and
# sticks the result in <tt>self.response_body</tt>.
- 1
def render(*args, &block)
options = _normalize_render(*args, &block)
rendered_body = render_to_body(options)
if options[:html]
_set_html_content_type
else
_set_rendered_content_type rendered_format
end
_set_vary_header
self.response_body = rendered_body
end
# Raw rendering of a template to a string.
#
# It is similar to render, except that it does not
# set the +response_body+ and it should be guaranteed
# to always return a string.
#
# If a component extends the semantics of +response_body+
# (as ActionController extends it to be anything that
# responds to the method each), this method needs to be
# overridden in order to still return a string.
- 1
def render_to_string(*args, &block)
options = _normalize_render(*args, &block)
render_to_body(options)
end
# Performs the actual template rendering.
- 1
def render_to_body(options = {})
end
# Returns Content-Type of rendered content.
- 1
def rendered_format
Mime[:text]
end
- 1
DEFAULT_PROTECTED_INSTANCE_VARIABLES = %i(@_action_name @_response_body @_formats @_prefixes)
# This method should return a hash with assigns.
# You can overwrite this configuration per controller.
- 1
def view_assigns
variables = instance_variables - _protected_ivars
variables.each_with_object({}) do |name, hash|
hash[name.slice(1, name.length)] = instance_variable_get(name)
end
end
- 1
private
# Normalize args by converting <tt>render "foo"</tt> to
# <tt>render :action => "foo"</tt> and <tt>render "foo/bar"</tt> to
# <tt>render :file => "foo/bar"</tt>.
- 1
def _normalize_args(action = nil, options = {}) # :doc:
if action.respond_to?(:permitted?)
if action.permitted?
action
else
raise ArgumentError, "render parameters are not permitted"
end
elsif action.is_a?(Hash)
action
else
options
end
end
# Normalize options.
- 1
def _normalize_options(options) # :doc:
options
end
# Process extra options.
- 1
def _process_options(options) # :doc:
options
end
# Process the rendered format.
- 1
def _process_format(format) # :nodoc:
end
- 1
def _process_variant(options)
end
- 1
def _set_html_content_type # :nodoc:
end
- 1
def _set_vary_header # :nodoc:
end
- 1
def _set_rendered_content_type(format) # :nodoc:
end
# Normalize args and options.
- 1
def _normalize_render(*args, &block) # :nodoc:
options = _normalize_args(*args, &block)
_process_variant(options)
_normalize_options(options)
options
end
- 1
def _protected_ivars
DEFAULT_PROTECTED_INSTANCE_VARIABLES
end
end
end
# frozen_string_literal: true
- 1
require "active_support/core_ext/symbol/starts_ends_with"
- 1
module AbstractController
- 1
module Translation
- 1
mattr_accessor :raise_on_missing_translations, default: false
# Delegates to <tt>I18n.translate</tt>. Also aliased as <tt>t</tt>.
#
# 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
# <tt>PeopleController#index</tt>, it will convert the call to
# <tt>I18n.translate("people.index.foo")</tt>. This makes it less repetitive
# to translate many keys within the same controller / action and gives you a
# simple framework for scoping them consistently.
- 1
def translate(key, **options)
if key.start_with?(".")
path = controller_path.tr("/", ".")
defaults = [:"#{path}#{key}"]
defaults << options[:default] if options[:default]
options[:default] = defaults.flatten
key = "#{path}.#{action_name}#{key}"
end
i18n_raise = options.fetch(:raise, self.raise_on_missing_translations)
I18n.translate(key, **options, raise: i18n_raise)
end
- 1
alias :t :translate
# Delegates to <tt>I18n.localize</tt>. Also aliased as <tt>l</tt>.
- 1
def localize(object, **options)
I18n.localize(object, **options)
end
- 1
alias :l :localize
end
end
# frozen_string_literal: true
- 1
module AbstractController
# Includes +url_for+ into the host class (e.g. an abstract controller or mailer). The class
# has to provide a +RouteSet+ by implementing the <tt>_routes</tt> methods. Otherwise, an
# exception will be raised.
#
# Note that this module is completely decoupled from HTTP - the only requirement is a valid
# <tt>_routes</tt> implementation.
- 1
module UrlFor
- 1
extend ActiveSupport::Concern
- 1
include ActionDispatch::Routing::UrlFor
- 1
def _routes
raise "In order to use #url_for, you must include routing helpers explicitly. " \
"For instance, `include Rails.application.routes.url_helpers`."
end
- 1
module ClassMethods
- 1
def _routes
nil
end
- 1
def action_methods
@action_methods ||= begin
if _routes
super - _routes.named_routes.helper_names
else
super
end
end
end
end
end
end
# frozen_string_literal: true
- 1
require "abstract_controller"
- 1
require "action_dispatch"
- 1
require "action_controller/metal/strong_parameters"
- 1
module ActionController
- 1
extend ActiveSupport::Autoload
- 1
autoload :API
- 1
autoload :Base
- 1
autoload :Metal
- 1
autoload :Renderer
- 1
autoload :FormBuilder
- 1
eager_autoload do
- 1
autoload :Caching
end
- 1
autoload_under "metal" do
- 1
eager_autoload do
- 1
autoload :Live
end
- 1
autoload :ConditionalGet
- 1
autoload :ContentSecurityPolicy
- 1
autoload :Cookies
- 1
autoload :DataStreaming
- 1
autoload :DefaultHeaders
- 1
autoload :EtagWithTemplateDigest
- 1
autoload :EtagWithFlash
- 1
autoload :FeaturePolicy
- 1
autoload :Flash
- 1
autoload :Head
- 1
autoload :Helpers
- 1
autoload :HttpAuthentication
- 1
autoload :BasicImplicitRender
- 1
autoload :ImplicitRender
- 1
autoload :Instrumentation
- 1
autoload :Logging
- 1
autoload :MimeResponds
- 1
autoload :ParamsWrapper
- 1
autoload :Redirecting
- 1
autoload :Renderers
- 1
autoload :Rendering
- 1
autoload :RequestForgeryProtection
- 1
autoload :Rescue
- 1
autoload :Streaming
- 1
autoload :StrongParameters
- 1
autoload :ParameterEncoding
- 1
autoload :Testing
- 1
autoload :UrlFor
end
- 1
autoload_under "api" do
- 1
autoload :ApiRendering
end
- 1
autoload :TestCase, "action_controller/test_case"
- 1
autoload :TemplateAssertions, "action_controller/test_case"
end
# Common Active Support usage in Action Controller
- 1
require "active_support/core_ext/module/attribute_accessors"
- 1
require "active_support/core_ext/load_error"
- 1
require "active_support/core_ext/module/attr_internal"
- 1
require "active_support/core_ext/name_error"
- 1
require "active_support/core_ext/uri"
- 1
require "active_support/inflector"
# frozen_string_literal: true
- 1
require "action_view"
- 1
require "action_controller"
- 1
require "action_controller/log_subscriber"
- 1
module ActionController
# API Controller is a lightweight version of <tt>ActionController::Base</tt>,
# created for applications that don't require all functionalities that a complete
# \Rails controller provides, allowing you to create controllers with just the
# features that you need for API only applications.
#
# An API Controller is different from a normal controller in the sense that
# by default it doesn't include a number of features that are usually required
# by browser access only: layouts and templates rendering,
# flash, assets, and so on. This makes the entire controller stack thinner,
# suitable for API applications. It doesn't mean you won't have such
# features if you need them: they're all available for you to include in
# your application, they're just not part of the default API controller stack.
#
# Normally, +ApplicationController+ is the only controller that inherits from
# <tt>ActionController::API</tt>. All other controllers in turn inherit from
# +ApplicationController+.
#
# A sample controller could look like this:
#
# class PostsController < ApplicationController
# def index
# posts = Post.all
# render json: posts
# end
# end
#
# Request, response, and parameters objects all work the exact same way as
# <tt>ActionController::Base</tt>.
#
# == Renders
#
# The default API Controller stack includes all renderers, which means you
# can use <tt>render :json</tt> and brothers freely in your controllers. Keep
# in mind that templates are not going to be rendered, so you need to ensure
# your controller is calling either <tt>render</tt> or <tt>redirect_to</tt> in
# all actions, otherwise it will return 204 No Content.
#
# def show
# post = Post.find(params[:id])
# render json: post
# end
#
# == Redirects
#
# 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
# <tt>ActionController::Base</tt>. For example:
#
# def create
# redirect_to root_url and return if not_authorized?
# # do stuff here
# end
#
# == Adding New Behavior
#
# In some scenarios you may want to add back some functionality provided by
# <tt>ActionController::Base</tt> that is not present by default in
# <tt>ActionController::API</tt>, for instance <tt>MimeResponds</tt>. This
# module gives you the <tt>respond_to</tt> method. Adding it is quite simple,
# you just need to include the module in a specific controller or in
# +ApplicationController+ in case you want it available in your entire
# application:
#
# class ApplicationController < ActionController::API
# include ActionController::MimeResponds
# end
#
# class PostsController < ApplicationController
# def index
# posts = Post.all
#
# respond_to do |format|
# format.json { render json: posts }
# format.xml { render xml: posts }
# end
# end
# end
#
# Make sure to check the modules included in <tt>ActionController::Base</tt>
# if you want to use any other functionality that is not provided
# by <tt>ActionController::API</tt> out of the box.
- 1
class API < Metal
- 1
abstract!
# Shortcut helper that returns all the ActionController::API modules except
# the ones passed as arguments:
#
# class MyAPIBaseController < ActionController::Metal
# ActionController::API.without_modules(:UrlFor).each do |left|
# include left
# end
# end
#
# This gives better control over what you want to exclude and makes it easier
# to create an API controller class, instead of listing the modules required
# manually.
- 1
def self.without_modules(*modules)
modules = modules.map do |m|
m.is_a?(Symbol) ? ActionController.const_get(m) : m
end
MODULES - modules
end
- 1
MODULES = [
AbstractController::Rendering,
UrlFor,
Redirecting,
ApiRendering,
Renderers::All,
ConditionalGet,
BasicImplicitRender,
StrongParameters,
DataStreaming,
DefaultHeaders,
Logging,
# Before callbacks should also be executed as early as possible, so
# also include them at the bottom.
AbstractController::Callbacks,
# Append rescue at the bottom to wrap as much as possible.
Rescue,
# Add instrumentations hooks at the bottom, to ensure they instrument
# all the methods properly.
Instrumentation,
# Params wrapper should come before instrumentation so they are
# properly showed in logs
ParamsWrapper
]
- 1
MODULES.each do |mod|
- 15
include mod
end
- 1
ActiveSupport.run_load_hooks(:action_controller_api, self)
- 1
ActiveSupport.run_load_hooks(:action_controller, self)
end
end
# frozen_string_literal: true
- 1
module ActionController
- 1
module ApiRendering
- 1
extend ActiveSupport::Concern
- 1
included do
- 1
include Rendering
end
- 1
def render_to_body(options = {})
_process_options(options)
super
end
end
end
# frozen_string_literal: true
- 1
require "action_view"
- 1
require "action_controller/log_subscriber"
- 1
require "action_controller/metal/params_wrapper"
- 1
module ActionController
# Action Controllers are the core of a web request in \Rails. They are made up of one or more actions that are executed
# on request and then either it renders a 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 <tt>ActionController::Base</tt>. 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.
#
# A sample controller could look like this:
#
# class PostsController < ApplicationController
# def index
# @posts = Post.all
# end
#
# def create
# @post = Post.create params[:post]
# redirect_to posts_path
# end
# end
#
# Actions, by default, render a template in the <tt>app/views</tt> directory corresponding to the name of the controller and action
# 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
# new post), it initiates a redirect instead. This redirect works by returning an external
# <tt>302 Moved</tt> 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
#
# == Parameters
#
# All request parameters, whether they come from a query string in the URL or form data submitted through a POST request are
# 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:
#
# "Hello #{session[:person]}"
#
# For removing objects from the session, you can either assign a single key to +nil+:
#
# # removes :person from session
# session[:person] = nil
#
# or you can remove the entire session with +reset_session+.
#
# Sessions are stored by default in a browser cookie that's cryptographically signed, but unencrypted.
# This prevents the user from tampering with the session but also allows them to see its contents.
#
# Do not put secret information in cookie-based sessions!
#
# == Responses
#
# Each action results in a response, which holds the headers and document to be 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
#
# Action Controller sends content to the user by using one of five rendering methods. The most versatile and common is the rendering
# of a template. 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
# @post = Post.find(params[:id])
# end
#
# Which are then automatically available to the view:
#
# 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
# will use the manual rendering methods:
#
# def search
# @results = Search.find(params[:query])
# case @results.count
# when 0 then render action: "no_results"
# when 1 then render action: "show"
# when 2..10 then render action: "show_many"
# end
# end
#
# Read more about writing ERB and Builder templates in ActionView::Base.
#
# == 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
# database, we might like to 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 <tt>show</tt> action that we'll assume has already been created. The code might look like this:
#
# def create
# @entry = Entry.new(params[:entry])
# if @entry.save
# # The entry was saved correctly, redirect to show
# redirect_to action: 'show', id: @entry.id
# else
# # things didn't go so well, do something else
# 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.
# 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),
# 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.
#
# == 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:
#
# def do_something
# redirect_to action: "elsewhere"
# render action: "overthere" # raises DoubleRenderError
# end
#
# If you need to redirect on the condition of something, then be sure to add "and return" to halt execution.
#
# def do_something
# redirect_to(action: "elsewhere") and return if monkeys.nil?
# render action: "overthere" # won't be called if monkeys is nil
# end
#
- 1
class Base < Metal
- 1
abstract!
# We document the request and response methods here because albeit they are
# implemented in ActionController::Metal, the type of the returned objects
# is unknown at that level.
##
# :method: request
#
# Returns an ActionDispatch::Request instance that represents the
# current request.
##
# :method: response
#
# Returns an ActionDispatch::Response that represents the current
# response.
# Shortcut helper that returns all the modules included in
# ActionController::Base except the ones passed as arguments:
#
# class MyBaseController < ActionController::Metal
# ActionController::Base.without_modules(:ParamsWrapper, :Streaming).each do |left|
# include left
# end
# end
#
# This gives better control over what you want to exclude and makes it
# easier to create a bare controller class, instead of listing the modules
# required manually.
- 1
def self.without_modules(*modules)
modules = modules.map do |m|
m.is_a?(Symbol) ? ActionController.const_get(m) : m
end
MODULES - modules
end
- 1
MODULES = [
AbstractController::Rendering,
AbstractController::Translation,
AbstractController::AssetPaths,
Helpers,
UrlFor,
Redirecting,
ActionView::Layouts,
Rendering,
Renderers::All,
ConditionalGet,
EtagWithTemplateDigest,
EtagWithFlash,
Caching,
MimeResponds,
ImplicitRender,
StrongParameters,
ParameterEncoding,
Cookies,
Flash,
FormBuilder,
RequestForgeryProtection,
ContentSecurityPolicy,
FeaturePolicy,
Streaming,
DataStreaming,
HttpAuthentication::Basic::ControllerMethods,
HttpAuthentication::Digest::ControllerMethods,
HttpAuthentication::Token::ControllerMethods,
DefaultHeaders,
Logging,
# Before callbacks should also be executed as early as possible, so
# also include them at the bottom.
AbstractController::Callbacks,
# Append rescue at the bottom to wrap as much as possible.
Rescue,
# Add instrumentations hooks at the bottom, to ensure they instrument
# all the methods properly.
Instrumentation,
# Params wrapper should come before instrumentation so they are
# properly showed in logs
ParamsWrapper
]
- 1
MODULES.each do |mod|
- 34
include mod
end
- 1
setup_renderer!
# Define some internal variables that should not be propagated to the view.
- 1
PROTECTED_IVARS = AbstractController::Rendering::DEFAULT_PROTECTED_INSTANCE_VARIABLES + %i(
@_params @_response @_request @_config @_url_options @_action_has_layout @_view_context_class
@_view_renderer @_lookup_context @_routes @_view_runtime @_db_runtime @_helper_proxy
)
- 1
def _protected_ivars
PROTECTED_IVARS
end
- 1
private :_protected_ivars
- 1
ActiveSupport.run_load_hooks(:action_controller_base, self)
- 1
ActiveSupport.run_load_hooks(:action_controller, self)
end
end
# frozen_string_literal: true
- 1
module ActionController
# \Caching is a cheap way of speeding up slow applications by keeping the result of
# calculations, renderings, and database calls around for subsequent requests.
#
# You can read more about each approach by clicking the modules below.
#
# Note: To turn off all caching provided by Action Controller, set
# config.action_controller.perform_caching = false
#
# == \Caching stores
#
# All the caching stores from ActiveSupport::Cache are available to be used as backends
# for Action Controller caching.
#
# Configuration examples (FileStore is the default):
#
# config.action_controller.cache_store = :memory_store
# 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, Memcached::Rails.new('localhost:11211')
# config.action_controller.cache_store = MyOwnStore.new('parameter')
- 1
module Caching
- 1
extend ActiveSupport::Concern
- 1
included do
- 2
include AbstractController::Caching
end
- 1
private
- 1
def instrument_payload(key)
{
controller: controller_name,
action: action_name,
key: key
}
end
- 1
def instrument_name
"action_controller"
end
end
end
# frozen_string_literal: true
- 1
module ActionController
# Override the default form builder for all views rendered by this
# controller and any of its descendants. Accepts a subclass of
# +ActionView::Helpers::FormBuilder+.
#
# For example, given a form builder:
#
# class AdminFormBuilder < ActionView::Helpers::FormBuilder
# def special_field(name)
# end
# end
#
# The controller specifies a form builder as its default:
#
# class AdminAreaController < ApplicationController
# default_form_builder AdminFormBuilder
# end
#
# Then in the view any form using +form_for+ will be an instance of the
# specified form builder:
#
# <%= form_for(@instance) do |builder| %>
# <%= builder.special_field(:name) %>
# <% end %>
- 1
module FormBuilder
- 1
extend ActiveSupport::Concern
- 1
included do
- 1
class_attribute :_default_form_builder, instance_accessor: false
end
- 1
module ClassMethods
# Set the form builder to be used as the default for all forms
# in the views rendered by this controller and its subclasses.
#
# ==== Parameters
# * <tt>builder</tt> - Default form builder, an instance of +ActionView::Helpers::FormBuilder+
- 1
def default_form_builder(builder)
- 1
self._default_form_builder = builder
end
end
# Default form builder for the controller
- 1
def default_form_builder
self.class._default_form_builder
end
end
end
# frozen_string_literal: true
- 1
module ActionController
- 1
class LogSubscriber < ActiveSupport::LogSubscriber
- 1
INTERNAL_PARAMS = %w(controller action format _method only_path)
- 1
def start_processing(event)
return unless logger.info?
payload = event.payload
params = payload[:params].except(*INTERNAL_PARAMS)
format = payload[:format]
format = format.to_s.upcase if format.is_a?(Symbol)
format = "*/*" if format.nil?
info "Processing by #{payload[:controller]}##{payload[:action]} as #{format}"
info " Parameters: #{params.inspect}" unless params.empty?
end
- 1
def process_action(event)
info do
payload = event.payload
additions = ActionController::Base.log_process_action(payload)
status = payload[:status]
if status.nil? && (exception_class_name = payload[:exception].first)
status = ActionDispatch::ExceptionWrapper.status_code_for_exception(exception_class_name)
end
additions << "Allocations: #{event.allocations}"
message = +"Completed #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]} in #{event.duration.round}ms"
message << " (#{additions.join(" | ")})"
message << "\n\n" if defined?(Rails.env) && Rails.env.development?
message
end
end
- 1
def halted_callback(event)
info { "Filter chain halted as #{event.payload[:filter].inspect} rendered or redirected" }
end
- 1
def send_file(event)
info { "Sent file #{event.payload[:path]} (#{event.duration.round(1)}ms)" }
end
- 1
def redirect_to(event)
info { "Redirected to #{event.payload[:location]}" }
end
- 1
def send_data(event)
info { "Sent data #{event.payload[:filename]} (#{event.duration.round(1)}ms)" }
end
- 1
def unpermitted_parameters(event)
debug do
unpermitted_keys = event.payload[:keys]
color("Unpermitted parameter#{'s' if unpermitted_keys.size > 1}: #{unpermitted_keys.map { |e| ":#{e}" }.join(", ")}", RED)
end
end
%w(write_fragment read_fragment exist_fragment?
- 1
expire_fragment expire_page write_page).each do |method|
- 6
class_eval <<-METHOD, __FILE__, __LINE__ + 1
def #{method}(event)
return unless logger.info? && ActionController::Base.enable_fragment_cache_logging
key = ActiveSupport::Cache.expand_cache_key(event.payload[:key] || event.payload[:path])
human_name = #{method.to_s.humanize.inspect}
info("\#{human_name} \#{key} (\#{event.duration.round(1)}ms)")
end
METHOD
end
- 1
def logger
ActionController::Base.logger
end
end
end
- 1
ActionController::LogSubscriber.attach_to :action_controller
# frozen_string_literal: true
- 1
require "active_support/core_ext/array/extract_options"
- 1
require "action_dispatch/middleware/stack"
- 1
require "action_dispatch/http/request"
- 1
require "action_dispatch/http/response"
- 1
module ActionController
# Extend ActionDispatch middleware stack to make it aware of options
# allowing the following syntax in controllers:
#
# class PostsController < ApplicationController
# use AuthenticationMiddleware, except: [:index, :show]
# end
#
- 1
class MiddlewareStack < ActionDispatch::MiddlewareStack #:nodoc:
- 1
class Middleware < ActionDispatch::MiddlewareStack::Middleware #:nodoc:
- 1
def initialize(klass, args, actions, strategy, block)
- 7
@actions = actions
- 7
@strategy = strategy
- 7
super(klass, args, block)
end
- 1
def valid?(action)
@strategy.call @actions, action
end
end
- 1
def build(action, app = nil, &block)
action = action.to_s
middlewares.reverse.inject(app || block) do |a, middleware|
middleware.valid?(action) ? middleware.build(a) : a
end
end
- 1
private
- 1
INCLUDE = ->(list, action) { list.include? action }
- 1
EXCLUDE = ->(list, action) { !list.include? action }
- 1
NULL = ->(list, action) { true }
- 1
def build_middleware(klass, args, block)
- 7
options = args.extract_options!
- 7
only = Array(options.delete(:only)).map(&:to_s)
- 7
except = Array(options.delete(:except)).map(&:to_s)
- 7
args << options unless options.empty?
- 7
strategy = NULL
- 7
list = nil
- 7
if only.any?
- 1
strategy = INCLUDE
- 1
list = only
- 6
elsif except.any?
- 1
strategy = EXCLUDE
- 1
list = except
end
- 7
Middleware.new(klass, args, list, strategy, block)
end
end
# <tt>ActionController::Metal</tt> is the simplest possible controller, providing a
# valid Rack interface without the additional niceties provided by
# <tt>ActionController::Base</tt>.
#
# A sample metal controller might look like this:
#
# class HelloController < ActionController::Metal
# def index
# self.response_body = "Hello World!"
# end
# end
#
# And then to route requests to your metal controller, you would add
# something like this to <tt>config/routes.rb</tt>:
#
# get 'hello', to: HelloController.action(:index)
#
# The +action+ method returns a valid Rack application for the \Rails
# router to dispatch to.
#
# == Rendering Helpers
#
# <tt>ActionController::Metal</tt> by default provides no utilities for rendering
# views, partials, or other responses aside from explicitly calling of
# <tt>response_body=</tt>, <tt>content_type=</tt>, and <tt>status=</tt>. To
# add the render helpers you're used to having in a normal controller, you
# can do the following:
#
# class HelloController < ActionController::Metal
# include AbstractController::Rendering
# include ActionView::Layouts
# append_view_path "#{Rails.root}/app/views"
#
# def index
# render "hello/index"
# end
# end
#
# == Redirection Helpers
#
# To add redirection helpers to your metal controller, do the following:
#
# class HelloController < ActionController::Metal
# include ActionController::Redirecting
# include Rails.application.routes.url_helpers
#
# def index
# redirect_to root_url
# end
# end
#
# == Other Helpers
#
# You can refer to the modules included in <tt>ActionController::Base</tt> to see
# other features you can bring into your metal controller.
#
- 1
class Metal < AbstractController::Base
- 1
abstract!
# Returns the last part of the controller's name, underscored, without the ending
# <tt>Controller</tt>. For instance, PostsController returns <tt>posts</tt>.
# Namespaces are left out, so Admin::PostsController returns <tt>posts</tt> as well.
#
# ==== Returns
# * <tt>string</tt>
- 1
def self.controller_name
@controller_name ||= name.demodulize.delete_suffix("Controller").underscore
end
- 1
def self.make_response!(request)
ActionDispatch::Response.new.tap do |res|
res.request = request
end
end
- 1
def self.binary_params_for?(action) # :nodoc:
false
end
# Delegates to the class' <tt>controller_name</tt>.
- 1
def controller_name
self.class.controller_name
end
- 1
attr_internal :response, :request
- 1
delegate :session, to: "@_request"
- 1
delegate :headers, :status=, :location=, :content_type=,
:status, :location, :content_type, :media_type, to: "@_response"
- 1
def initialize
@_request = nil
@_response = nil
@_routes = nil
super
end
- 1
def params
@_params ||= request.parameters
end
- 1
def params=(val)
@_params = val
end
- 1
alias :response_code :status # :nodoc:
# Basic url_for that can be overridden for more robust functionality.
- 1
def url_for(string)
string
end
- 1
def response_body=(body)
body = [body] unless body.nil? || body.respond_to?(:each)
response.reset_body!
return unless body
response.body = body
super
end
# Tests if render or redirect has already happened.
- 1
def performed?
response_body || response.committed?
end
- 1
def dispatch(name, request, response) #:nodoc:
set_request!(request)
set_response!(response)
process(name)
request.commit_flash
to_a
end
- 1
def set_response!(response) # :nodoc:
@_response = response
end
- 1
def set_request!(request) #:nodoc:
@_request = request
@_request.controller_instance = self
end
- 1
def to_a #:nodoc:
response.to_a
end
- 1
def reset_session
@_request.reset_session
end
- 1
class_attribute :middleware_stack, default: ActionController::MiddlewareStack.new
- 1
def self.inherited(base) # :nodoc:
- 301
base.middleware_stack = middleware_stack.dup
- 301
super
end
- 1
class << self
# Pushes the given Rack middleware and its arguments to the bottom of the
# middleware stack.
- 1
def use(*args, &block)
- 5
middleware_stack.use(*args, &block)
end
- 1
ruby2_keywords(:use) if respond_to?(:ruby2_keywords, true)
end
# Alias for +middleware_stack+.
- 1
def self.middleware
- 2
middleware_stack
end
# Returns a Rack endpoint for the given action name.
- 1
def self.action(name)
- 1
app = lambda { |env|
req = ActionDispatch::Request.new(env)
res = make_response! req
new.dispatch(name, req, res)
}
- 1
if middleware_stack.any?
middleware_stack.build(name, app)
else
- 1
app
end
end
# Direct dispatch to the controller. Instantiates the controller, then
# executes the action named +name+.
- 1
def self.dispatch(name, req, res)
if middleware_stack.any?
middleware_stack.build(name) { |env| new.dispatch(name, req, res) }.call req.env
else
new.dispatch(name, req, res)
end
end
end
end
# frozen_string_literal: true
- 1
module ActionController
- 1
module BasicImplicitRender # :nodoc:
- 1
def send_action(method, *args)
super.tap { default_render unless performed? }
end
- 1
def default_render
head :no_content
end
end
end
# frozen_string_literal: true
- 1
require "active_support/core_ext/object/try"
- 1
require "active_support/core_ext/integer/time"
- 1
module ActionController
- 1
module ConditionalGet
- 1
extend ActiveSupport::Concern
- 1
include Head
- 1
included do
- 2
class_attribute :etaggers, default: []
end
- 1
module ClassMethods
# Allows you to consider additional controller-wide information when generating an ETag.
# For example, if you serve pages tailored depending on who's logged in at the moment, you
# may want to add the current user id to be part of the ETag to prevent unauthorized displaying
# of cached pages.
#
# class InvoicesController < ApplicationController
# etag { current_user&.id }
#
# def show
# # Etag will differ even for the same invoice when it's viewed by a different current_user
# @invoice = Invoice.find(params[:id])
# fresh_when etag: @invoice
# end
# end
- 1
def etag(&etagger)
- 7
self.etaggers += [etagger]
end
end
# Sets the +etag+, +last_modified+, or both on the response and renders a
# <tt>304 Not Modified</tt> response if the request is already fresh.
#
# === Parameters:
#
# * <tt>:etag</tt> Sets a "weak" ETag validator on the response. See the
# +:weak_etag+ option.
# * <tt>:weak_etag</tt> Sets a "weak" ETag validator on the response.
# Requests that set If-None-Match header may return a 304 Not Modified
# response if it matches the ETag exactly. A weak ETag indicates semantic
# equivalence, not byte-for-byte equality, so they're good for caching
# HTML pages in browser caches. They can't be used for responses that
# must be byte-identical, like serving Range requests within a PDF file.
# * <tt>:strong_etag</tt> Sets a "strong" ETag validator on the response.
# Requests that set If-None-Match header may return a 304 Not Modified
# response if it matches the ETag exactly. A strong ETag implies exact
# equality: the response must match byte for byte. This is necessary for
# doing Range requests within a large video or PDF file, for example, or
# for compatibility with some CDNs that don't support weak ETags.
# * <tt>:last_modified</tt> Sets a "weak" last-update validator on the
# response. Subsequent requests that set If-Modified-Since may return a
# 304 Not Modified response if last_modified <= If-Modified-Since.
# * <tt>:public</tt> By default the Cache-Control header is private, set this to
# +true+ if you want your application to be cacheable by other devices (proxy caches).
# * <tt>:template</tt> By default, the template digest for the current
# controller/action is included in ETags. If the action renders a
# different template, you can include its digest instead. If the action
# doesn't render a template at all, you can pass <tt>template: false</tt>
# to skip any attempt to check for a template digest.
#
# === Example:
#
# def show
# @article = Article.find(params[:id])
# fresh_when(etag: @article, last_modified: @article.updated_at, public: true)
# end
#
# This will render the show template if the request isn't sending a matching ETag or
# If-Modified-Since header and just a <tt>304 Not Modified</tt> response if there's a match.
#
# You can also just pass a record. In this case +last_modified+ will be set
# by calling +updated_at+ and +etag+ by passing the object itself.
#
# def show
# @article = Article.find(params[:id])
# fresh_when(@article)
# end
#
# You can also pass an object that responds to +maximum+, such as a
# collection of active records. In this case +last_modified+ will be set by
# calling <tt>maximum(:updated_at)</tt> on the collection (the timestamp of the
# most recently updated record) and the +etag+ by passing the object itself.
#
# def index
# @articles = Article.all
# fresh_when(@articles)
# end
#
# When passing a record or a collection, you can still set the public header:
#
# def show
# @article = Article.find(params[:id])
# fresh_when(@article, public: true)
# end
#
# When rendering a different template than the default controller/action
# style, you can indicate which digest to include in the ETag:
#
# before_action { fresh_when @article, template: 'widgets/show' }
#
- 1
def fresh_when(object = nil, etag: nil, weak_etag: nil, strong_etag: nil, last_modified: nil, public: false, template: nil)
weak_etag ||= etag || object unless strong_etag
last_modified ||= object.try(:updated_at) || object.try(:maximum, :updated_at)
if strong_etag
response.strong_etag = combine_etags strong_etag,
last_modified: last_modified, public: public, template: template
elsif weak_etag || template
response.weak_etag = combine_etags weak_etag,
last_modified: last_modified, public: public, template: template
end
response.last_modified = last_modified if last_modified
response.cache_control[:public] = true if public
head :not_modified if request.fresh?(response)
end
# Sets the +etag+ and/or +last_modified+ on the response and checks it against
# the client request. If the request doesn't match the options provided, the
# request is considered stale and should be generated from scratch. Otherwise,
# it's fresh and we don't need to generate anything and a reply of <tt>304 Not Modified</tt> is sent.
#
# === Parameters:
#
# * <tt>:etag</tt> Sets a "weak" ETag validator on the response. See the
# +:weak_etag+ option.
# * <tt>:weak_etag</tt> Sets a "weak" ETag validator on the response.
# Requests that set If-None-Match header may return a 304 Not Modified
# response if it matches the ETag exactly. A weak ETag indicates semantic
# equivalence, not byte-for-byte equality, so they're good for caching
# HTML pages in browser caches. They can't be used for responses that
# must be byte-identical, like serving Range requests within a PDF file.
# * <tt>:strong_etag</tt> Sets a "strong" ETag validator on the response.
# Requests that set If-None-Match header may return a 304 Not Modified
# response if it matches the ETag exactly. A strong ETag implies exact
# equality: the response must match byte for byte. This is necessary for
# doing Range requests within a large video or PDF file, for example, or
# for compatibility with some CDNs that don't support weak ETags.
# * <tt>:last_modified</tt> Sets a "weak" last-update validator on the
# response. Subsequent requests that set If-Modified-Since may return a
# 304 Not Modified response if last_modified <= If-Modified-Since.
# * <tt>:public</tt> By default the Cache-Control header is private, set this to
# +true+ if you want your application to be cacheable by other devices (proxy caches).
# * <tt>:template</tt> By default, the template digest for the current
# controller/action is included in ETags. If the action renders a
# different template, you can include its digest instead. If the action
# doesn't render a template at all, you can pass <tt>template: false</tt>
# to skip any attempt to check for a template digest.
#
# === Example:
#
# def show
# @article = Article.find(params[:id])
#
# if stale?(etag: @article, last_modified: @article.updated_at)
# @statistics = @article.really_expensive_call
# respond_to do |format|
# # all the supported formats
# end
# end
# end
#
# You can also just pass a record. In this case +last_modified+ will be set
# by calling +updated_at+ and +etag+ by passing the object itself.
#
# def show
# @article = Article.find(params[:id])
#
# if stale?(@article)
# @statistics = @article.really_expensive_call
# respond_to do |format|
# # all the supported formats
# end
# end
# end
#
# You can also pass an object that responds to +maximum+, such as a
# collection of active records. In this case +last_modified+ will be set by
# calling +maximum(:updated_at)+ on the collection (the timestamp of the
# most recently updated record) and the +etag+ by passing the object itself.
#
# def index
# @articles = Article.all
#
# if stale?(@articles)
# @statistics = @articles.really_expensive_call
# respond_to do |format|
# # all the supported formats
# end
# end
# end
#
# When passing a record or a collection, you can still set the public header:
#
# def show
# @article = Article.find(params[:id])
#
# if stale?(@article, public: true)
# @statistics = @article.really_expensive_call
# respond_to do |format|
# # all the supported formats
# end
# end
# end
#
# When rendering a different template than the default controller/action
# style, you can indicate which digest to include in the ETag:
#
# def show
# super if stale? @article, template: 'widgets/show'
# end
#
- 1
def stale?(object = nil, **freshness_kwargs)
fresh_when(object, **freshness_kwargs)
!request.fresh?(response)
end
# Sets an HTTP 1.1 Cache-Control header. Defaults to issuing a +private+
# instruction, so that intermediate caches must not cache the response.
#
# expires_in 20.minutes
# expires_in 3.hours, public: true
# expires_in 3.hours, public: true, must_revalidate: true
#
# This method will overwrite an existing Cache-Control header.
# See https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html for more possibilities.
#
# HTTP Cache-Control Extensions for Stale Content. See https://tools.ietf.org/html/rfc5861
# It helps to cache an asset and serve it while is being revalidated and/or returning with an error.
#
# expires_in 3.hours, public: true, stale_while_revalidate: 60.seconds
# expires_in 3.hours, public: true, stale_while_revalidate: 60.seconds, stale_if_error: 5.minutes
#
# HTTP Cache-Control Extensions other values: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
# Any additional key-value pairs are concatenated onto the `Cache-Control` header in the response:
#
# expires_in 3.hours, public: true, "s-maxage": 3.hours, "no-transform": true
#
# The method will also ensure an HTTP Date header for client compatibility.
- 1
def expires_in(seconds, options = {})
response.cache_control.merge!(
max_age: seconds,
public: options.delete(:public),
must_revalidate: options.delete(:must_revalidate),
stale_while_revalidate: options.delete(:stale_while_revalidate),
stale_if_error: options.delete(:stale_if_error),
)
options.delete(:private)
response.cache_control[:extras] = options.map { |k, v| "#{k}=#{v}" }
response.date = Time.now unless response.date?
end
# Sets an HTTP 1.1 Cache-Control header of <tt>no-cache</tt>. This means the
# resource will be marked as stale, so clients must always revalidate.
# Intermediate/browser caches may still store the asset.
- 1
def expires_now
response.cache_control.replace(no_cache: true)
end
# Cache or yield the block. The cache is supposed to never expire.
#
# You can use this method when you have an HTTP response that never changes,
# and the browser and proxies should cache it indefinitely.
#
# * +public+: By default, HTTP responses are private, cached only on the
# user's web browser. To allow proxies to cache the response, set +true+ to
# indicate that they can serve the cached response to all users.
- 1
def http_cache_forever(public: false)
expires_in 100.years, public: public
yield if stale?(etag: request.fullpath,
last_modified: Time.new(2011, 1, 1).utc,
public: public)
end
- 1
private
- 1
def combine_etags(validator, options)
[validator, *etaggers.map { |etagger| instance_exec(options, &etagger) }].compact
end
end
end
# frozen_string_literal: true
- 1
module ActionController #:nodoc:
- 1
module ContentSecurityPolicy
# TODO: Documentation
- 1
extend ActiveSupport::Concern
- 1
include AbstractController::Helpers
- 1
include AbstractController::Callbacks
- 1
included do
- 1
helper_method :content_security_policy?
- 1
helper_method :content_security_policy_nonce
end
- 1
module ClassMethods
- 1
def content_security_policy(enabled = true, **options, &block)
- 8
before_action(options) do
if block_given?
policy = current_content_security_policy
yield policy
request.content_security_policy = policy
end
unless enabled
request.content_security_policy = nil
end
end
end
- 1
def content_security_policy_report_only(report_only = true, **options)
- 1
before_action(options) do
request.content_security_policy_report_only = report_only
end
end
end
- 1
private
- 1
def content_security_policy?
request.content_security_policy
end
- 1
def content_security_policy_nonce
request.content_security_policy_nonce
end
- 1
def current_content_security_policy
request.content_security_policy&.clone || ActionDispatch::ContentSecurityPolicy.new
end
end
end
# frozen_string_literal: true
- 1
module ActionController #:nodoc:
- 1
module Cookies
- 1
extend ActiveSupport::Concern
- 1
included do
- 2
helper_method :cookies if defined?(helper_method)
end
- 1
private
- 1
def cookies
request.cookie_jar
end
end
end
# frozen_string_literal: true
- 1
require "action_controller/metal/exceptions"
- 1
require "action_dispatch/http/content_disposition"
- 1
module ActionController #:nodoc:
# Methods for sending arbitrary data and for streaming files to the browser,
# instead of rendering.
- 1
module DataStreaming
- 1
extend ActiveSupport::Concern
- 1
include ActionController::Rendering
- 1
DEFAULT_SEND_FILE_TYPE = "application/octet-stream" #:nodoc:
- 1
DEFAULT_SEND_FILE_DISPOSITION = "attachment" #:nodoc:
- 1
private
# Sends the file. This uses a server-appropriate method (such as X-Sendfile)
# via the Rack::Sendfile middleware. The header to use is set via
# +config.action_dispatch.x_sendfile_header+.
# Your server can also configure this for you by setting the X-Sendfile-Type header.
#
# Be careful to sanitize the path parameter if it is coming from a web
# page. <tt>send_file(params[:path])</tt> allows a malicious user to
# download any file on your server.
#
# Options:
# * <tt>:filename</tt> - suggests a filename for the browser to use.
# Defaults to <tt>File.basename(path)</tt>.
# * <tt>:type</tt> - specifies an HTTP content type.
# You can specify either a string or a symbol for a registered type with <tt>Mime::Type.register</tt>, for example :json.
# If omitted, the type will be inferred from the file extension specified in <tt>:filename</tt>.
# If no content type is registered for the extension, the default type 'application/octet-stream' will be used.
# * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded.
# Valid values are 'inline' and 'attachment' (default).
# * <tt>:status</tt> - specifies the status code to send with the response. Defaults to 200.
# * <tt>:url_based_filename</tt> - set to +true+ if you want the browser to guess the filename from
# the URL, which is necessary for i18n filenames on certain browsers
# (setting <tt>:filename</tt> overrides this option).
#
# The default Content-Type and Content-Disposition headers are
# set to download arbitrary binary files in as many browsers as
# possible. IE versions 4, 5, 5.5, and 6 are all known to have
# a variety of quirks (especially when downloading over SSL).
#
# Simple download:
#
# send_file '/path/to.zip'
#
# Show a JPEG in the browser:
#
# send_file '/path/to.jpeg', type: 'image/jpeg', disposition: 'inline'
#
# Show a 404 page in the browser:
#
# send_file '/path/to/404.html', type: 'text/html; charset=utf-8', disposition: 'inline', status: 404
#
# Read about the other Content-* HTTP headers if you'd like to
# provide the user with more information (such as Content-Description) in
# https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11.
#
# Also be aware that the document may be cached by proxies and browsers.
# The Pragma and Cache-Control headers declare how the file may be cached
# by intermediaries. They default to require clients to validate with
# the server before releasing cached responses. See
# https://www.mnot.net/cache_docs/ for an overview of web caching and
# https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9
# for the Cache-Control header spec.
- 1
def send_file(path, options = {}) #:doc:
raise MissingFile, "Cannot read file #{path}" unless File.file?(path) && File.readable?(path)
options[:filename] ||= File.basename(path) unless options[:url_based_filename]
send_file_headers! options
self.status = options[:status] || 200
self.content_type = options[:content_type] if options.key?(:content_type)
response.send_file path
end
# Sends the given binary data to the browser. This method is similar to
# <tt>render plain: data</tt>, but also allows you to specify whether
# the browser should display the response as a file attachment (i.e. in a
# download dialog) or as inline data. You may also set the content type,
# the file name, and other things.
#
# Options:
# * <tt>:filename</tt> - suggests a filename for the browser to use.
# * <tt>:type</tt> - specifies an HTTP content type. Defaults to 'application/octet-stream'.
# You can specify either a string or a symbol for a registered type with <tt>Mime::Type.register</tt>, for example :json.
# If omitted, type will be inferred from the file extension specified in <tt>:filename</tt>.
# If no content type is registered for the extension, the default type 'application/octet-stream' will be used.
# * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded.
# Valid values are 'inline' and 'attachment' (default).
# * <tt>:status</tt> - specifies the status code to send with the response. Defaults to 200.
#
# Generic data download:
#
# send_data buffer
#
# Download a dynamically-generated tarball:
#
# send_data generate_tgz('dir'), filename: 'dir.tgz'
#
# Display an image Active Record in the browser:
#
# send_data image.data, type: image.content_type, disposition: 'inline'
#
# See +send_file+ for more information on HTTP Content-* headers and caching.
- 1
def send_data(data, options = {}) #:doc:
send_file_headers! options
render options.slice(:status, :content_type).merge(body: data)
end
- 1
def send_file_headers!(options)
type_provided = options.has_key?(:type)
content_type = options.fetch(:type, DEFAULT_SEND_FILE_TYPE)
self.content_type = content_type
response.sending_file = true
raise ArgumentError, ":type option required" if content_type.nil?
if content_type.is_a?(Symbol)
extension = Mime[content_type]
raise ArgumentError, "Unknown MIME type #{options[:type]}" unless extension
self.content_type = extension
else
if !type_provided && options[:filename]
# If type wasn't provided, try guessing from file extension.
content_type = Mime::Type.lookup_by_extension(File.extname(options[:filename]).downcase.delete(".")) || content_type
end
self.content_type = content_type
end
disposition = options.fetch(:disposition, DEFAULT_SEND_FILE_DISPOSITION)
if disposition
headers["Content-Disposition"] = ActionDispatch::Http::ContentDisposition.format(disposition: disposition, filename: options[:filename])
end
headers["Content-Transfer-Encoding"] = "binary"
# Fix a problem with IE 6.0 on opening downloaded files:
# If Cache-Control: no-cache is set (which Rails does by default),
# IE removes the file it just downloaded from its cache immediately
# after it displays the "open/save" dialog, which means that if you
# hit "open" the file isn't there anymore when the application that
# is called for handling the download is run, so let's workaround that
response.cache_control[:public] ||= false
end
end
end
# frozen_string_literal: true
- 1
module ActionController
# Allows configuring default headers that will be automatically merged into
# each response.
- 1
module DefaultHeaders
- 1
extend ActiveSupport::Concern
- 1
module ClassMethods
- 1
def make_response!(request)
ActionDispatch::Response.create.tap do |res|
res.request = request
end
end
end
end
end
# frozen_string_literal: true
- 1
module ActionController
# When you're using the flash, it's generally used as a conditional on the view.
# This means the content of the view depends on the flash. Which in turn means
# that the ETag for a response should be computed with the content of the flash
# in mind. This does that by including the content of the flash as a component
# in the ETag that's generated for a response.
- 1
module EtagWithFlash
- 1
extend ActiveSupport::Concern
- 1
include ActionController::ConditionalGet
- 1
included do
- 1
etag { flash unless flash.empty? }
end
end
end
# frozen_string_literal: true
- 1
module ActionController
# When our views change, they should bubble up into HTTP cache freshness
# and bust browser caches. So the template digest for the current action
# is automatically included in the ETag.
#
# Enabled by default for apps that use Action View. Disable by setting
#
# config.action_controller.etag_with_template_digest = false
#
# Override the template to digest by passing +:template+ to +fresh_when+
# and +stale?+ calls. For example:
#
# # We're going to render widgets/show, not posts/show
# fresh_when @post, template: 'widgets/show'
#
# # We're not going to render a template, so omit it from the ETag.
# fresh_when @post, template: false
#
- 1
module EtagWithTemplateDigest
- 1
extend ActiveSupport::Concern
- 1
include ActionController::ConditionalGet
- 1
included do
- 1
class_attribute :etag_with_template_digest, default: true
- 1
etag do |options|
determine_template_etag(options) if etag_with_template_digest
end
end
- 1
private
- 1
def determine_template_etag(options)
if template = pick_template_for_etag(options)
lookup_and_digest_template(template)
end
end
# Pick the template digest to include in the ETag. If the +:template+ option
# is present, use the named template. If +:template+ is +nil+ or absent, use
# the default controller/action template. If +:template+ is false, omit the
# template digest from the ETag.
- 1
def pick_template_for_etag(options)
unless options[:template] == false
options[:template] || "#{controller_path}/#{action_name}"
end
end
- 1
def lookup_and_digest_template(template)
ActionView::Digestor.digest name: template, format: nil, finder: lookup_context
end
end
end
# frozen_string_literal: true
- 1
module ActionController
- 1
class ActionControllerError < StandardError #:nodoc:
end
- 1
class BadRequest < ActionControllerError #:nodoc:
- 1
def initialize(msg = nil)
super(msg)
set_backtrace $!.backtrace if $!
end
end
- 1
class RenderError < ActionControllerError #:nodoc:
end
- 1
class RoutingError < ActionControllerError #:nodoc:
- 1
attr_reader :failures
- 1
def initialize(message, failures = [])
super(message)
@failures = failures
end
end
- 1
class UrlGenerationError < ActionControllerError #:nodoc:
- 1
attr_reader :routes, :route_name, :method_name
- 1
def initialize(message, routes = nil, route_name = nil, method_name = nil)
@routes = routes
@route_name = route_name
@method_name = method_name
super(message)
end
- 1
class Correction
- 1
def initialize(error)
@error = error
end
- 1
def corrections
if @error.method_name
maybe_these = @error.routes.named_routes.helper_names.grep(/#{@error.route_name}/)
maybe_these -= [@error.method_name.to_s] # remove exact match
maybe_these.sort_by { |n|
DidYouMean::Jaro.distance(@error.route_name, n)
}.reverse.first(4)
else
[]
end
end
end
# We may not have DYM, and DYM might not let us register error handlers
- 1
if defined?(DidYouMean) && DidYouMean.respond_to?(:correct_error)
DidYouMean.correct_error(self, Correction)
end
end
- 1
class MethodNotAllowed < ActionControllerError #:nodoc:
- 1
def initialize(*allowed_methods)
super("Only #{allowed_methods.to_sentence} requests are allowed.")
end
end
- 1
class NotImplemented < MethodNotAllowed #:nodoc:
end
- 1
class MissingFile < ActionControllerError #:nodoc:
end
- 1
class SessionOverflowError < ActionControllerError #:nodoc:
- 1
DEFAULT_MESSAGE = "Your session data is larger than the data column in which it is to be stored. You must increase the size of your data column if you intend to store large data."
- 1
def initialize(message = nil)
super(message || DEFAULT_MESSAGE)
end
end
- 1
class UnknownHttpMethod < ActionControllerError #:nodoc:
end
- 1
class UnknownFormat < ActionControllerError #:nodoc:
end
# Raised when a nested respond_to is triggered and the content types of each
# are incompatible. For example:
#
# respond_to do |outer_type|
# outer_type.js do
# respond_to do |inner_type|
# inner_type.html { render body: "HTML" }
# end
# end
# end
- 1
class RespondToMismatchError < ActionControllerError
- 1
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
def initialize(message = nil)
super(message || DEFAULT_MESSAGE)
end
end
- 1
class MissingExactTemplate < UnknownFormat #:nodoc:
end
end
# frozen_string_literal: true
- 1
module ActionController #:nodoc:
# HTTP Feature Policy is a web standard for defining a mechanism to
# allow and deny the use of browser features in its own context, and
# in content within any <iframe> elements in the document.
#
# Full details of HTTP Feature Policy specification and guidelines can
# be found at MDN:
#
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Feature-Policy
#
# Examples of usage:
#
# # Global policy
# Rails.application.config.feature_policy do |f|
# f.camera :none
# f.gyroscope :none
# f.microphone :none
# f.usb :none
# f.fullscreen :self
# f.payment :self, "https://secure.example.com"
# end
#
# # Controller level policy
# class PagesController < ApplicationController
# feature_policy do |p|
# p.geolocation "https://example.com"
# end
# end
- 1
module FeaturePolicy
- 1
extend ActiveSupport::Concern
- 1
module ClassMethods
- 1
def feature_policy(**options, &block)
- 3
before_action(options) do
if block_given?
policy = request.feature_policy.clone
yield policy
request.feature_policy = policy
end
end
end
end
end
end
# frozen_string_literal: true
- 1
module ActionController #:nodoc:
- 1
module Flash
- 1
extend ActiveSupport::Concern
- 1
included do
- 1
class_attribute :_flash_types, instance_accessor: false, default: []
- 1
delegate :flash, to: :request
- 1
add_flash_types(:alert, :notice)
end
- 1
module ClassMethods
# Creates new flash types. You can pass as many types as you want to create
# flash types other than the default <tt>alert</tt> and <tt>notice</tt> in
# your controllers and views. For instance:
#
# # in application_controller.rb
# class ApplicationController < ActionController::Base
# add_flash_types :warning
# end
#
# # in your controller
# redirect_to user_path(@user), warning: "Incomplete profile"
#
# # in your view
# <%= warning %>
#
# This method will automatically define a new method for each of the given
# names, and it will be available in your views.
- 1
def add_flash_types(*types)
- 2
types.each do |type|
- 3
next if _flash_types.include?(type)
- 3
define_method(type) do
request.flash[type]
end
- 3
helper_method(type) if respond_to?(:helper_method)
- 3
self._flash_types += [type]
end
end
end
- 1
private
- 1
def redirect_to(options = {}, response_options_and_flash = {}) #:doc:
self.class._flash_types.each do |flash_type|
if type = response_options_and_flash.delete(flash_type)
flash[flash_type] = type
end
end
if other_flashes = response_options_and_flash.delete(:flash)
flash.update(other_flashes)
end
super(options, response_options_and_flash)
end
end
end
# frozen_string_literal: true
- 1
module ActionController
- 1
module Head
# Returns a response that has no content (merely headers). The options
# argument is interpreted to be a hash of header names and values.
# This allows you to easily return a response that consists only of
# significant headers:
#
# head :created, location: person_path(@person)
#
# head :created, location: @person
#
# It can also be used to return exceptional conditions:
#
# return head(:method_not_allowed) unless request.post?
# return head(:bad_request) unless valid_request?
# render
#
# See Rack::Utils::SYMBOL_TO_STATUS_CODE for a full list of valid +status+ symbols.
- 1
def head(status, options = {})
if status.is_a?(Hash)
raise ArgumentError, "#{status.inspect} is not a valid value for `status`."
end
status ||= :ok
location = options.delete(:location)
content_type = options.delete(:content_type)
options.each do |key, value|
headers[key.to_s.split(/[-_]/).each { |v| v[0] = v[0].upcase }.join("-")] = value.to_s
end
self.status = status
self.location = url_for(location) if location
if include_content?(response_code)
unless self.media_type
self.content_type = content_type || (Mime[formats.first] if formats) || Mime[:html]
end
response.charset = false
end
self.response_body = ""
true
end
- 1
private
- 1
def include_content?(status)
case status
when 100..199
false
when 204, 205, 304
false
else
true
end
end
end
end
# frozen_string_literal: true
- 1
module ActionController
# The \Rails framework provides a large number of helpers for working with assets, dates, forms,
# numbers and model objects, to name a few. These helpers are available to all templates
# by default.
#
# In addition to using the standard template helpers provided, creating custom helpers to
# extract complicated logic or reusable functionality is strongly encouraged. By default, each controller
# will include all helpers. These helpers are only accessible on the controller through <tt>#helpers</tt>
#
# In previous versions of \Rails the controller will include a helper which
# matches the name of the controller, e.g., <tt>MyController</tt> will automatically
# include <tt>MyHelper</tt>. You can revert to the old behavior with the following:
#
# # config/application.rb
# class Application < Rails::Application
# 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=" ")
# time.blank? ? blank_message : time.to_s(format)
# end
# end
#
# FormattedTimeHelper can now be included in a controller, using the +helper+ class method:
#
# class EventsController < ActionController::Base
# helper FormattedTimeHelper
# def index
# @events = Event.all
# end
# end
#
# Then, in any view rendered by <tt>EventsController</tt>, the <tt>format_time</tt> method can be called:
#
# <% @events.each do |event| -%>
# <p>
# <%= format_time(event.time, :short, "N/A") %> | <%= event.name %>
# </p>
# <% end -%>
#
# Finally, assuming we have two event instances, one which has a time and one which does not,
# the output might look like this:
#
# 23 Aug 11:30 | Carolina Railhawks Soccer Match
# N/A | Carolina Railhawks Training Workshop
#
- 1
module Helpers
- 1
extend ActiveSupport::Concern
- 2
class << self; attr_accessor :helpers_path; end
- 1
include AbstractController::Helpers
- 1
included do
- 2
class_attribute :helpers_path, default: []
- 2
class_attribute :include_all_helpers, default: true
end
- 1
module ClassMethods
# Declares helper accessors for controller attributes. For example, the
# following adds new +name+ and <tt>name=</tt> instance methods to a
# controller and makes them available to the view:
# attr_accessor :name
# helper_attr :name
#
# ==== Parameters
# * <tt>attrs</tt> - Names of attributes to be converted into helpers.
- 1
def helper_attr(*attrs)
attrs.flatten.each { |attr| helper_method(attr, "#{attr}=") }
end
# Provides a proxy to access helper methods from outside the view.
#
# Note that the proxy is rendered under a different view context.
# This may cause incorrect behaviour with capture methods. Consider
# using {helper}[rdoc-ref:AbstractController::Helpers::ClassMethods#helper]
# instead when using +capture+.
- 1
def helpers
@helper_proxy ||= begin
proxy = ActionView::Base.empty
proxy.config = config.inheritable_copy
proxy.extend(_helpers)
end
end
# Overwrite modules_for_helpers to accept :all as argument, which loads
# all helpers in helpers_path.
#
# ==== Parameters
# * <tt>args</tt> - A list of helpers
#
# ==== Returns
# * <tt>array</tt> - A normalized list of modules for the list of helpers provided.
- 1
def modules_for_helpers(args)
- 274
args += all_application_helpers if args.delete(:all)
- 274
super(args)
end
# Returns a list of helper names in a given path.
#
# ActionController::Base.all_helpers_from_path 'app/helpers'
# # => ["application", "chart", "rubygems"]
- 1
def all_helpers_from_path(path)
- 2
helpers = Array(path).flat_map do |_path|
- 10
names = Dir["#{_path}/**/*_helper.rb"].map { |file| file[_path.to_s.size + 1..-"_helper.rb".size - 1] }
- 3
names.sort!
end
- 2
helpers.uniq!
- 2
helpers
end
- 1
private
# Extract helper names from files in <tt>app/helpers/**/*_helper.rb</tt>
- 1
def all_application_helpers
- 2
all_helpers_from_path(helpers_path)
end
end
# Provides a proxy to access helper methods from outside the view.
- 1
def helpers
@_helper_proxy ||= view_context
end
end
end
# frozen_string_literal: true
- 1
require "base64"
- 1
require "active_support/security_utils"
- 1
module ActionController
# Makes it dead easy to do HTTP Basic, Digest and Token authentication.
- 1
module HttpAuthentication
# Makes it dead easy to do HTTP \Basic authentication.
#
# === Simple \Basic example
#
# class PostsController < ApplicationController
# http_basic_authenticate_with name: "dhh", password: "secret", except: :index
#
# def index
# render plain: "Everyone can see me!"
# end
#
# def edit
# render plain: "I'm only accessible if you know the password"
# end
# end
#
# === Advanced \Basic example
#
# Here is a more advanced \Basic example where only Atom feeds and the XML API is protected by HTTP authentication,
# the regular HTML interface is protected by a session approach:
#
# class ApplicationController < ActionController::Base
# before_action :set_account, :authenticate
#
# private
# def set_account
# @account = Account.find_by(url_name: request.subdomains.first)
# end
#
# def authenticate
# case request.format
# when Mime[:xml], Mime[:atom]
# if user = authenticate_with_http_basic { |u, p| @account.users.authenticate(u, p) }
# @current_user = user
# else
# request_http_basic_authentication
# end
# else
# if session_authenticated?
# @current_user = @account.users.find(session[:authenticated][:user_id])
# else
# redirect_to(login_url) and return false
# end
# end
# end
# end
#
# In your integration tests, you can do something like this:
#
# def test_access_granted_from_xml
# authorization = ActionController::HttpAuthentication::Basic.encode_credentials(users(:dhh).name, users(:dhh).password)
#
# get "/notes/1.xml", headers: { 'HTTP_AUTHORIZATION' => authorization }
#
# assert_equal 200, status
# end
- 1
module Basic
- 1
extend self
- 1
module ControllerMethods
- 1
extend ActiveSupport::Concern
- 1
module ClassMethods
- 1
def http_basic_authenticate_with(name:, password:, realm: nil, **options)
- 1
before_action(options) { http_basic_authenticate_or_request_with name: name, password: password, realm: realm }
end
end
- 1
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|
ActiveSupport::SecurityUtils.secure_compare(given_name, name) &
ActiveSupport::SecurityUtils.secure_compare(given_password, password)
end
end
- 1
def authenticate_or_request_with_http_basic(realm = nil, message = nil, &login_procedure)
authenticate_with_http_basic(&login_procedure) || request_http_basic_authentication(realm || "Application", message)
end
- 1
def authenticate_with_http_basic(&login_procedure)
HttpAuthentication::Basic.authenticate(request, &login_procedure)
end
- 1
def request_http_basic_authentication(realm = "Application", message = nil)
HttpAuthentication::Basic.authentication_request(self, realm, message)
end
end
- 1
def authenticate(request, &login_procedure)
if has_basic_credentials?(request)
login_procedure.call(*user_name_and_password(request))
end
end
- 1
def has_basic_credentials?(request)
request.authorization.present? && (auth_scheme(request).downcase == "basic")
end
- 1
def user_name_and_password(request)
decode_credentials(request).split(":", 2)
end
- 1
def decode_credentials(request)
::Base64.decode64(auth_param(request) || "")
end
- 1
def auth_scheme(request)
request.authorization.to_s.split(" ", 2).first
end
- 1
def auth_param(request)
request.authorization.to_s.split(" ", 2).second
end
- 1
def encode_credentials(user_name, password)
"Basic #{::Base64.strict_encode64("#{user_name}:#{password}")}"
end
- 1
def authentication_request(controller, realm, message)
message ||= "HTTP Basic: Access denied.\n"
controller.headers["WWW-Authenticate"] = %(Basic realm="#{realm.tr('"', "")}")
controller.status = 401
controller.response_body = message
end
end
# Makes it dead easy to do HTTP \Digest authentication.
#
# === Simple \Digest example
#
# require "digest/md5"
# class PostsController < ApplicationController
# REALM = "SuperSecret"
# USERS = {"dhh" => "secret", #plain text password
# "dap" => Digest::MD5.hexdigest(["dap",REALM,"secret"].join(":"))} #ha1 digest password
#
# before_action :authenticate, except: [:index]
#
# def index
# render plain: "Everyone can see me!"
# end
#
# def edit
# render plain: "I'm only accessible if you know the password"
# end
#
# private
# def authenticate
# authenticate_or_request_with_http_digest(REALM) do |username|
# USERS[username]
# end
# end
# end
#
# === Notes
#
# The +authenticate_or_request_with_http_digest+ block must return the user's password
# or the ha1 digest hash so the framework can appropriately hash to check the user's
# credentials. Returning +nil+ will cause authentication to fail.
#
# Storing the ha1 hash: MD5(username:realm:password), is better than storing a plain password. If
# the password file or database is compromised, the attacker would be able to use the ha1 hash to
# authenticate as the user at this +realm+, but would not have the user's password to try using at
# other sites.
#
# In rare instances, web servers or front proxies strip authorization headers before
# they reach your application. You can debug this situation by logging all environment
# variables, and check for HTTP_AUTHORIZATION, amongst others.
- 1
module Digest
- 1
extend self
- 1
module ControllerMethods
- 1
def authenticate_or_request_with_http_digest(realm = "Application", message = nil, &password_procedure)
authenticate_with_http_digest(realm, &password_procedure) || request_http_digest_authentication(realm, message)
end
# Authenticate with HTTP Digest, returns true or false
- 1
def authenticate_with_http_digest(realm = "Application", &password_procedure)
HttpAuthentication::Digest.authenticate(request, realm, &password_procedure)
end
# Render output including the HTTP Digest authentication header
- 1
def request_http_digest_authentication(realm = "Application", message = nil)
HttpAuthentication::Digest.authentication_request(self, realm, message)
end
end
# Returns false on a valid response, true otherwise
- 1
def authenticate(request, realm, &password_procedure)
request.authorization && validate_digest_response(request, realm, &password_procedure)
end
# Returns false unless the request credentials response value matches the expected value.
# First try the password as a ha1 digest password. If this fails, then try it as a plain
# text password.
- 1
def validate_digest_response(request, realm, &password_procedure)
secret_key = secret_token(request)
credentials = decode_credentials_header(request)
valid_nonce = validate_nonce(secret_key, request, credentials[:nonce])
if valid_nonce && realm == credentials[:realm] && opaque(secret_key) == credentials[:opaque]
password = password_procedure.call(credentials[:username])
return false unless password
method = request.get_header("rack.methodoverride.original_method") || request.get_header("REQUEST_METHOD")
uri = credentials[:uri]
[true, false].any? do |trailing_question_mark|
[true, false].any? do |password_is_ha1|
_uri = trailing_question_mark ? uri + "?" : uri
expected = expected_response(method, _uri, credentials, password, password_is_ha1)
expected == credentials[:response]
end
end
end
end
# Returns the expected response for a request of +http_method+ to +uri+ with the decoded +credentials+ and the expected +password+
# Optional parameter +password_is_ha1+ is set to +true+ by default, since best practice is to store ha1 digest instead
# of a plain-text password.
- 1
def expected_response(http_method, uri, credentials, password, password_is_ha1 = true)
ha1 = password_is_ha1 ? password : ha1(credentials, password)
ha2 = ::Digest::MD5.hexdigest([http_method.to_s.upcase, uri].join(":"))
::Digest::MD5.hexdigest([ha1, credentials[:nonce], credentials[:nc], credentials[:cnonce], credentials[:qop], ha2].join(":"))
end
- 1
def ha1(credentials, password)
::Digest::MD5.hexdigest([credentials[:username], credentials[:realm], password].join(":"))
end
- 1
def encode_credentials(http_method, credentials, password, password_is_ha1)
credentials[:response] = expected_response(http_method, credentials[:uri], credentials, password, password_is_ha1)
"Digest " + credentials.sort_by { |x| x[0].to_s }.map { |v| "#{v[0]}='#{v[1]}'" }.join(", ")
end
- 1
def decode_credentials_header(request)
decode_credentials(request.authorization)
end
- 1
def decode_credentials(header)
ActiveSupport::HashWithIndifferentAccess[header.to_s.gsub(/^Digest\s+/, "").split(",").map do |pair|
key, value = pair.split("=", 2)
[key.strip, value.to_s.gsub(/^"|"$/, "").delete("'")]
end]
end
- 1
def authentication_header(controller, realm)
secret_key = secret_token(controller.request)
nonce = self.nonce(secret_key)
opaque = opaque(secret_key)
controller.headers["WWW-Authenticate"] = %(Digest realm="#{realm}", qop="auth", algorithm=MD5, nonce="#{nonce}", opaque="#{opaque}")
end
- 1
def authentication_request(controller, realm, message = nil)
message ||= "HTTP Digest: Access denied.\n"
authentication_header(controller, realm)
controller.status = 401
controller.response_body = message
end
- 1
def secret_token(request)
key_generator = request.key_generator
http_auth_salt = request.http_auth_salt
key_generator.generate_key(http_auth_salt)
end
# Uses an MD5 digest based on time to generate a value to be used only once.
#
# A server-specified data string which should be uniquely generated each time a 401 response is made.
# It is recommended that this string be base64 or hexadecimal data.
# Specifically, since the string is passed in the header lines as a quoted string, the double-quote character is not allowed.
#
# The contents of the nonce are implementation dependent.
# The quality of the implementation depends on a good choice.
# A nonce might, for example, be constructed as the base 64 encoding of
#
# time-stamp H(time-stamp ":" ETag ":" private-key)
#
# where time-stamp is a server-generated time or other non-repeating value,
# ETag is the value of the HTTP ETag header associated with the requested entity,
# and private-key is data known only to the server.
# With a nonce of this form a server would recalculate the hash portion after receiving the client authentication header and
# reject the request if it did not match the nonce from that header or
# if the time-stamp value is not recent enough. In this way the server can limit the time of the nonce's validity.
# The inclusion of the ETag prevents a replay request for an updated version of the resource.
# (Note: including the IP address of the client in the nonce would appear to offer the server the ability
# to limit the reuse of the nonce to the same client that originally got it.
# However, that would break proxy farms, where requests from a single user often go through different proxies in the farm.
# Also, IP address spoofing is not that hard.)
#
# An implementation might choose not to accept a previously used nonce or a previously used digest, in order to
# protect against a replay attack. Or, an implementation might choose to use one-time nonces or digests for
# POST, PUT, or PATCH requests and a time-stamp for GET requests. For more details on the issues involved see Section 4
# of this document.
#
# The nonce is opaque to the client. Composed of Time, and hash of Time with secret
# key from the Rails session secret generated upon creation of project. Ensures
# the time cannot be modified by client.
- 1
def nonce(secret_key, time = Time.now)
t = time.to_i
hashed = [t, secret_key]
digest = ::Digest::MD5.hexdigest(hashed.join(":"))
::Base64.strict_encode64("#{t}:#{digest}")
end
# Might want a shorter timeout depending on whether the request
# is a PATCH, PUT, or POST, and if the client is a browser or web service.
# Can be much shorter if the Stale directive is implemented. This would
# allow a user to use new nonce without prompting the user again for their
# username and password.
- 1
def validate_nonce(secret_key, request, value, seconds_to_timeout = 5 * 60)
return false if value.nil?
t = ::Base64.decode64(value).split(":").first.to_i
nonce(secret_key, t) == value && (t - Time.now.to_i).abs <= seconds_to_timeout
end
# Opaque based on digest of secret key
- 1
def opaque(secret_key)
::Digest::MD5.hexdigest(secret_key)
end
end
# Makes it dead easy to do HTTP Token authentication.
#
# Simple Token example:
#
# class PostsController < ApplicationController
# TOKEN = "secret"
#
# before_action :authenticate, except: [ :index ]
#
# def index
# render plain: "Everyone can see me!"
# end
#
# def edit
# render plain: "I'm only accessible if you know the password"
# end
#
# private
# def authenticate
# authenticate_or_request_with_http_token do |token, options|
# # Compare the tokens in a time-constant manner, to mitigate
# # timing attacks.
# ActiveSupport::SecurityUtils.secure_compare(token, TOKEN)
# end
# end
# end
#
#
# Here is a more advanced Token example where only Atom feeds and the XML API is protected by HTTP token authentication,
# the regular HTML interface is protected by a session approach:
#
# class ApplicationController < ActionController::Base
# before_action :set_account, :authenticate
#
# private
# def set_account
# @account = Account.find_by(url_name: request.subdomains.first)
# end
#
# def authenticate
# case request.format
# when Mime[:xml], Mime[:atom]
# if user = authenticate_with_http_token { |t, o| @account.users.authenticate(t, o) }
# @current_user = user
# else
# request_http_token_authentication
# end
# else
# if session_authenticated?
# @current_user = @account.users.find(session[:authenticated][:user_id])
# else
# redirect_to(login_url) and return false
# end
# end
# end
# end
#
#
# In your integration tests, you can do something like this:
#
# def test_access_granted_from_xml
# authorization = ActionController::HttpAuthentication::Token.encode_credentials(users(:dhh).token)
#
# get "/notes/1.xml", headers: { 'HTTP_AUTHORIZATION' => authorization }
#
# assert_equal 200, status
# end
#
#
# On shared hosts, Apache sometimes doesn't pass authentication headers to
# FCGI instances. If your environment matches this description and you cannot
# authenticate, try this rule in your Apache setup:
#
# RewriteRule ^(.*)$ dispatch.fcgi [E=X-HTTP_AUTHORIZATION:%{HTTP:Authorization},QSA,L]
- 1
module Token
- 1
TOKEN_KEY = "token="
- 1
TOKEN_REGEX = /^(Token|Bearer)\s+/
- 1
AUTHN_PAIR_DELIMITERS = /(?:,|;|\t+)/
- 1
extend self
- 1
module ControllerMethods
- 1
def authenticate_or_request_with_http_token(realm = "Application", message = nil, &login_procedure)
authenticate_with_http_token(&login_procedure) || request_http_token_authentication(realm, message)
end
- 1
def authenticate_with_http_token(&login_procedure)
Token.authenticate(self, &login_procedure)
end
- 1
def request_http_token_authentication(realm = "Application", message = nil)
Token.authentication_request(self, realm, message)
end
end
# If token Authorization header is present, call the login
# procedure with the present token and options.
#
# [controller]
# ActionController::Base instance for the current request.
#
# [login_procedure]
# Proc to call if a token is present. The Proc should take two arguments:
#
# authenticate(controller) { |token, options| ... }
#
# Returns the return value of <tt>login_procedure</tt> if a
# token is found. Returns <tt>nil</tt> if no token is found.
- 1
def authenticate(controller, &login_procedure)
token, options = token_and_options(controller.request)
unless token.blank?
login_procedure.call(token, options)
end
end
# Parses the token and options out of the token Authorization header.
# The value for the Authorization header is expected to have the prefix
# <tt>"Token"</tt> or <tt>"Bearer"</tt>. If the header looks like this:
# Authorization: Token token="abc", nonce="def"
# Then the returned token is <tt>"abc"</tt>, and the options are
# <tt>{nonce: "def"}</tt>
#
# request - ActionDispatch::Request instance with the current headers.
#
# Returns an +Array+ of <tt>[String, Hash]</tt> if a token is present.
# Returns +nil+ if no token is found.
- 1
def token_and_options(request)
authorization_request = request.authorization.to_s
if authorization_request[TOKEN_REGEX]
params = token_params_from authorization_request
[params.shift[1], Hash[params].with_indifferent_access]
end
end
- 1
def token_params_from(auth)
rewrite_param_values params_array_from raw_params auth
end
# Takes raw_params and turns it into an array of parameters
- 1
def params_array_from(raw_params)
raw_params.map { |param| param.split %r/=(.+)?/ }
end
# This removes the <tt>"</tt> characters wrapping the value.
- 1
def rewrite_param_values(array_params)
array_params.each { |param| (param[1] || +"").gsub! %r/^"|"$/, "" }
end
# This method takes an authorization body and splits up the key-value
# pairs by the standardized <tt>:</tt>, <tt>;</tt>, or <tt>\t</tt>
# delimiters defined in +AUTHN_PAIR_DELIMITERS+.
- 1
def raw_params(auth)
_raw_params = auth.sub(TOKEN_REGEX, "").split(/\s*#{AUTHN_PAIR_DELIMITERS}\s*/)
if !_raw_params.first.start_with?(TOKEN_KEY)
_raw_params[0] = "#{TOKEN_KEY}#{_raw_params.first}"
end
_raw_params
end
# Encodes the given token and options into an Authorization header value.
#
# token - String token.
# options - optional Hash of the options.
#
# Returns String.
- 1
def encode_credentials(token, options = {})
values = ["#{TOKEN_KEY}#{token.to_s.inspect}"] + options.map do |key, value|
"#{key}=#{value.to_s.inspect}"
end
"Token #{values * ", "}"
end
# Sets a WWW-Authenticate header to let the client know a token is desired.
#
# controller - ActionController::Base instance for the outgoing response.
# realm - String realm to use in the header.
#
# Returns nothing.
- 1
def authentication_request(controller, realm, message = nil)
message ||= "HTTP Token: Access denied.\n"
controller.headers["WWW-Authenticate"] = %(Token realm="#{realm.tr('"', "")}")
controller.__send__ :render, plain: message, status: :unauthorized
end
end
end
end
# frozen_string_literal: true
- 1
module ActionController
# Handles implicit rendering for a controller action that does not
# explicitly respond with +render+, +respond_to+, +redirect+, or +head+.
#
# For API controllers, the implicit response is always <tt>204 No Content</tt>.
#
# For all other controllers, we use these heuristics to decide whether to
# render a template, raise an error for a missing template, or respond with
# <tt>204 No Content</tt>:
#
# First, if we DO find a template, it's rendered. Template lookup accounts
# for the action name, locales, format, variant, template handlers, and more
# (see +render+ for details).
#
# Second, if we DON'T find a template but the controller action does have
# templates for other formats, variants, etc., then we trust that you meant
# to provide a template for this response, too, and we raise
# <tt>ActionController::UnknownFormat</tt> with an explanation.
#
# Third, if we DON'T find a template AND the request is a page load in a web
# browser (technically, a non-XHR GET request for an HTML response) where
# you reasonably expect to have rendered a template, then we raise
# <tt>ActionController::MissingExactTemplate</tt> with an explanation.
#
# Finally, if we DON'T find a template AND the request isn't a browser page
# load, then we implicitly respond with <tt>204 No Content</tt>.
- 1
module ImplicitRender
# :stopdoc:
- 1
include BasicImplicitRender
- 1
def default_render
if template_exists?(action_name.to_s, _prefixes, variants: request.variant)
render
elsif any_templates?(action_name.to_s, _prefixes)
message = "#{self.class.name}\##{action_name} is missing a template " \
"for this request format and variant.\n" \
"\nrequest.formats: #{request.formats.map(&:to_s).inspect}" \
"\nrequest.variant: #{request.variant.inspect}"
raise ActionController::UnknownFormat, message
elsif interactive_browser_request?
message = "#{self.class.name}\##{action_name} is missing a template for request formats: #{request.formats.map(&:to_s).join(',')}"
raise ActionController::MissingExactTemplate, message
else
logger.info "No template found for #{self.class.name}\##{action_name}, rendering head :no_content" if logger
super
end
end
- 1
def method_for_action(action_name)
super || if template_exists?(action_name.to_s, _prefixes)
"default_render"
end
end
- 1
private
- 1
def interactive_browser_request?
request.get? && request.format == Mime[:html] && !request.xhr?
end
end
end
# frozen_string_literal: true
- 1
require "benchmark"
- 1
require "abstract_controller/logger"
- 1
module ActionController
# Adds instrumentation to several ends in ActionController::Base. It also provides
# some hooks related with process_action. This allows an ORM like Active Record
# and/or DataMapper to plug in ActionController and show related information.
#
# Check ActiveRecord::Railties::ControllerRuntime for an example.
- 1
module Instrumentation
- 1
extend ActiveSupport::Concern
- 1
include AbstractController::Logger
- 1
attr_internal :view_runtime
- 1
def process_action(*)
raw_payload = {
controller: self.class.name,
action: action_name,
request: request,
params: request.filtered_parameters,
headers: request.headers,
format: request.format.ref,
method: request.request_method,
path: request.fullpath
}
ActiveSupport::Notifications.instrument("start_processing.action_controller", raw_payload)
ActiveSupport::Notifications.instrument("process_action.action_controller", raw_payload) do |payload|
result = super
payload[:response] = response
payload[:status] = response.status
result
ensure
append_info_to_payload(payload)
end
end
- 1
def render(*)
render_output = nil
self.view_runtime = cleanup_view_runtime do
Benchmark.ms { render_output = super }
end
render_output
end
- 1
def send_file(path, options = {})
ActiveSupport::Notifications.instrument("send_file.action_controller",
options.merge(path: path)) do
super
end
end
- 1
def send_data(data, options = {})
ActiveSupport::Notifications.instrument("send_data.action_controller", options) do
super
end
end
- 1
def redirect_to(*)
ActiveSupport::Notifications.instrument("redirect_to.action_controller", request: request) do |payload|
result = super
payload[:status] = response.status
payload[:location] = response.filtered_location
result
end
end
- 1
private
# A hook invoked every time a before callback is halted.
- 1
def halted_callback_hook(filter, _)
ActiveSupport::Notifications.instrument("halted_callback.action_controller", filter: filter)
end
# A hook which allows you to clean up any time, wrongly taken into account in
# views, like database querying time.
#
# def cleanup_view_runtime
# super - time_taken_in_something_expensive
# end
- 1
def cleanup_view_runtime # :doc:
yield
end
# Every time after an action is processed, this method is invoked
# with the payload, so you can add more information.
- 1
def append_info_to_payload(payload) # :doc:
payload[:view_runtime] = view_runtime
end
- 1
module ClassMethods
# A hook which allows other frameworks to log what happened during
# controller process action. This method should return an array
# with the messages to be added.
- 1
def log_process_action(payload) #:nodoc:
messages, view_runtime = [], payload[:view_runtime]
messages << ("Views: %.1fms" % view_runtime.to_f) if view_runtime
messages
end
end
end
end
# frozen_string_literal: true
- 1
require "action_dispatch/http/response"
- 1
require "delegate"
- 1
require "active_support/json"
- 1
module ActionController
# Mix this module into your controller, and all actions in that controller
# will be able to stream data to the client as it's written.
#
# class MyController < ActionController::Base
# include ActionController::Live
#
# def stream
# response.headers['Content-Type'] = 'text/event-stream'
# 100.times {
# response.stream.write "hello world\n"
# sleep 1
# }
# ensure
# response.stream.close
# end
# end
#
# There are a few caveats with this module. You *cannot* write headers after the
# response has been committed (Response#committed? will return truthy).
# Calling +write+ or +close+ on the response stream will cause the response
# object to be committed. Make sure all headers are set before calling write
# or close on your stream.
#
# You *must* call close on your stream when you're finished, otherwise the
# socket may be left open forever.
#
# The final caveat is that your actions are executed in a separate thread than
# the main thread. Make sure your actions are thread safe, and this shouldn't
# be a problem (don't share state across threads, etc).
- 1
module Live
- 1
extend ActiveSupport::Concern
- 1
module ClassMethods
- 1
def make_response!(request)
if request.get_header("HTTP_VERSION") == "HTTP/1.0"
super
else
Live::Response.new.tap do |res|
res.request = request
end
end
end
end
# This class provides the ability to write an SSE (Server Sent Event)
# to an IO stream. The class is initialized with a stream and can be used
# to either write a JSON string or an object which can be converted to JSON.
#
# Writing an object will convert it into standard SSE format with whatever
# options you have configured. You may choose to set the following options:
#
# 1) Event. If specified, an event with this name will be dispatched on
# the browser.
# 2) Retry. The reconnection time in milliseconds used when attempting
# to send the event.
# 3) Id. If the connection dies while sending an SSE to the browser, then
# the server will receive a +Last-Event-ID+ header with value equal to +id+.
#
# After setting an option in the constructor of the SSE object, all future
# SSEs sent across the stream will use those options unless overridden.
#
# Example Usage:
#
# class MyController < ActionController::Base
# include ActionController::Live
#
# def index
# response.headers['Content-Type'] = 'text/event-stream'
# sse = SSE.new(response.stream, retry: 300, event: "event-name")
# sse.write({ name: 'John'})
# sse.write({ name: 'John'}, id: 10)
# sse.write({ name: 'John'}, id: 10, event: "other-event")
# sse.write({ name: 'John'}, id: 10, event: "other-event", retry: 500)
# ensure
# sse.close
# end
# end
#
# Note: SSEs are not currently supported by IE. However, they are supported
# by Chrome, Firefox, Opera, and Safari.
- 1
class SSE
- 1
PERMITTED_OPTIONS = %w( retry event id )
- 1
def initialize(stream, options = {})
@stream = stream
@options = options
end
- 1
def close
@stream.close
end
- 1
def write(object, options = {})
case object
when String
perform_write(object, options)
else
perform_write(ActiveSupport::JSON.encode(object), options)
end
end
- 1
private
- 1
def perform_write(json, options)
current_options = @options.merge(options).stringify_keys
PERMITTED_OPTIONS.each do |option_name|
if (option_value = current_options[option_name])
@stream.write "#{option_name}: #{option_value}\n"
end
end
message = json.gsub("\n", "\ndata: ")
@stream.write "data: #{message}\n\n"
end
end
- 1
class ClientDisconnected < RuntimeError
end
- 1
class Buffer < ActionDispatch::Response::Buffer #:nodoc:
- 1
include MonitorMixin
# Ignore that the client has disconnected.
#
# If this value is `true`, calling `write` after the client
# disconnects will result in the written content being silently
# discarded. If this value is `false` (the default), a
# ClientDisconnected exception will be raised.
- 1
attr_accessor :ignore_disconnect
- 1
def initialize(response)
super(response, SizedQueue.new(10))
@error_callback = lambda { true }
@cv = new_cond
@aborted = false
@ignore_disconnect = false
end
- 1
def write(string)
unless @response.committed?
@response.headers["Cache-Control"] ||= "no-cache"
@response.delete_header "Content-Length"
end
super
unless connected?
@buf.clear
unless @ignore_disconnect
# Raise ClientDisconnected, which is a RuntimeError (not an
# IOError), because that's more appropriate for something beyond
# the developer's control.
raise ClientDisconnected, "client disconnected"
end
end
end
# Write a 'close' event to the buffer; the producer/writing thread
# uses this to notify us that it's finished supplying content.
#
# See also #abort.
- 1
def close
synchronize do
super
@buf.push nil
@cv.broadcast
end
end
# Inform the producer/writing thread that the client has
# disconnected; the reading thread is no longer interested in
# anything that's being written.
#
# See also #close.
- 1
def abort
synchronize do
@aborted = true
@buf.clear
end
end
# Is the client still connected and waiting for content?
#
# The result of calling `write` when this is `false` is determined
# by `ignore_disconnect`.
- 1
def connected?
!@aborted
end
- 1
def on_error(&block)
@error_callback = block
end
- 1
def call_on_error
@error_callback.call
end
- 1
private
- 1
def each_chunk(&block)
loop do
str = nil
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
str = @buf.pop
end
break unless str
yield str
end
end
end
- 1
class Response < ActionDispatch::Response #:nodoc: all
- 1
private
- 1
def before_committed
super
jar = request.cookie_jar
# The response can be committed multiple times
jar.write self unless committed?
end
- 1
def build_buffer(response, body)
buf = Live::Buffer.new response
body.each { |part| buf.write part }
buf
end
end
- 1
def process(name)
t1 = Thread.current
locals = t1.keys.map { |key| [key, t1[key]] }
error = nil
# This processes the action in a child thread. It lets us return the
# response code and headers back up the Rack stack, and still process
# the body in parallel with sending data to the client.
new_controller_thread {
ActiveSupport::Dependencies.interlock.running do
t2 = Thread.current
# Since we're processing the view in a different thread, copy the
# thread locals from the main thread to the child thread. :'(
locals.each { |k, v| t2[k] = v }
begin
super(name)
rescue => e
if @_response.committed?
begin
@_response.stream.write(ActionView::Base.streaming_completion_on_exception) if request.format == :html
@_response.stream.call_on_error
rescue => exception
log_error(exception)
ensure
log_error(e)
@_response.stream.close
end
else
error = e
end
ensure
@_response.commit!
end
end
}
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
@_response.await_commit
end
raise error if error
end
- 1
def response_body=(body)
super
response.close if response
end
- 1
private
# Spawn a new thread to serve up the controller in. This is to get
# around the fact that Rack isn't based around IOs and we need to use
# a thread to stream data from the response bodies. Nobody should call
# this method except in Rails internals. Seriously!
- 1
def new_controller_thread # :nodoc:
Thread.new {
t2 = Thread.current
t2.abort_on_exception = true
yield
}
end
- 1
def log_error(exception)
logger = ActionController::Base.logger
return unless logger
logger.fatal do
message = +"\n#{exception.class} (#{exception.message}):\n"
message << exception.annotated_source_code.to_s if exception.respond_to?(:annotated_source_code)
message << " " << exception.backtrace.join("\n ")
"#{message}\n\n"
end
end
end
end
# frozen_string_literal: true
- 1
module ActionController
- 1
module Logging
- 1
extend ActiveSupport::Concern
- 1
module ClassMethods
# Set a different log level per request.
#
# # Use the debug log level if a particular cookie is set.
# class ApplicationController < ActionController::Base
# log_at :debug, if: -> { cookies[:debug] }
# end
#
- 1
def log_at(level, **options)
- 2
around_action ->(_, action) { logger.log_at(level, &action) }, **options
end
end
end
end
# frozen_string_literal: true
- 1
require "abstract_controller/collector"
- 1
module ActionController #:nodoc:
- 1
module MimeResponds
# Without web-service support, an action which collects the data for displaying a list of people
# might look something like this:
#
# def index
# @people = Person.all
# end
#
# That action implicitly responds to all formats, but formats can also be explicitly enumerated:
#
# def index
# @people = Person.all
# respond_to :html, :js
# end
#
# Here's the same action, with web-service support baked in:
#
# def index
# @people = Person.all
#
# respond_to do |format|
# format.html
# format.js
# format.xml { render xml: @people }
# end
# end
#
# What that says is, "if the client wants HTML or JS in response to this action, just respond as we
# would have before, but if the client wants XML, return them the list of people in XML format."
# (Rails determines the desired response format from the HTTP Accept header submitted by the client.)
#
# Supposing you have an action that adds a new person, optionally creating their company
# (by name) if it does not already exist, without web-services, it might look like this:
#
# def create
# @company = Company.find_or_create_by(name: params[:company][:name])
# @person = @company.people.create(params[:person])
#
# redirect_to(person_list_url)
# end
#
# Here's the same action, with web-service support baked in:
#
# def create
# company = params[:person].delete(:company)
# @company = Company.find_or_create_by(name: company[:name])
# @person = @company.people.create(params[:person])
#
# respond_to do |format|
# format.html { redirect_to(person_list_url) }
# format.js
# format.xml { render xml: @person.to_xml(include: @company) }
# end
# end
#
# If the client wants HTML, we just redirect them back to the person list. If they want JavaScript,
# then it is an Ajax request and we render the JavaScript template associated with this action.
# Lastly, if the client wants XML, we render the created person as XML, but with a twist: we also
# include the person's company in the rendered XML, so you get something like this:
#
# <person>
# <id>...</id>
# ...
# <company>
# <id>...</id>
# <name>...</name>
# ...
# </company>
# </person>
#
# Note, however, the extra bit at the top of that action:
#
# company = params[:person].delete(:company)
# @company = Company.find_or_create_by(name: company[:name])
#
# This is because the incoming XML document (if a web-service request is in process) can only contain a
# single root-node. So, we have to rearrange things so that the request looks like this (url-encoded):
#
# person[name]=...&person[company][name]=...&...
#
# And, like this (xml-encoded):
#
# <person>
# <name>...</name>
# <company>
# <name>...</name>
# </company>
# </person>
#
# In other words, we make the request so that it operates on a single entity's person. Then, in the action,
# we extract the company data from the request, find or create the company, and then create the new person
# with the remaining data.
#
# Note that you can define your own XML parameter parser which would allow you to describe multiple entities
# in a single request (i.e., by wrapping them all in a single root node), but if you just go with the flow
# and accept Rails' defaults, life will be much easier.
#
# If you need to use a MIME type which isn't supported by default, you can register your own handlers in
# +config/initializers/mime_types.rb+ as follows.
#
# Mime::Type.register "image/jpg", :jpg
#
# +respond_to+ also allows you to specify a common block for different formats by using +any+:
#
# def index
# @people = Person.all
#
# respond_to do |format|
# format.html
# format.any(:xml, :json) { render request.format.to_sym => @people }
# end
# end
#
# In the example above, if the format is xml, it will render:
#
# render xml: @people
#
# Or if the format is json:
#
# render json: @people
#
# +any+ can also be used with no arguments, in which case it will be used for any format requested by
# the user:
#
# respond_to do |format|
# format.html
# format.any { redirect_to support_path }
# end
#
# Formats can have different variants.
#
# The request variant is a specialization of the request format, like <tt>:tablet</tt>,
# <tt>:phone</tt>, or <tt>:desktop</tt>.
#
# We often want to render different html/json/xml templates for phones,
# tablets, and desktop browsers. Variants make it easy.
#
# You can set the variant in a +before_action+:
#
# request.variant = :tablet if /iPad/.match?(request.user_agent)
#
# Respond to variants in the action just like you respond to formats:
#
# respond_to do |format|
# format.html do |variant|
# variant.tablet # renders app/views/projects/show.html+tablet.erb
# variant.phone { extra_setup; render ... }
# variant.none { special_setup } # executed only if there is no variant set
# end
# end
#
# Provide separate templates for each format and variant:
#
# app/views/projects/show.html.erb
# app/views/projects/show.html+tablet.erb
# app/views/projects/show.html+phone.erb
#
# When you're not sharing any code within the format, you can simplify defining variants
# using the inline syntax:
#
# respond_to do |format|
# format.js { render "trash" }
# format.html.phone { redirect_to progress_path }
# format.html.none { render "trash" }
# end
#
# Variants also support common +any+/+all+ block that formats have.
#
# It works for both inline:
#
# respond_to do |format|
# format.html.any { render html: "any" }
# format.html.phone { render html: "phone" }
# end
#
# and block syntax:
#
# respond_to do |format|
# format.html do |variant|
# variant.any(:tablet, :phablet){ render html: "any" }
# variant.phone { render html: "phone" }
# end
# end
#
# You can also set an array of variants:
#
# request.variant = [:tablet, :phone]
#
# This will work similarly to formats and MIME types negotiation. If there
# is no +:tablet+ variant declared, the +:phone+ variant will be used:
#
# respond_to do |format|
# format.html.none
# format.html.phone # this gets rendered
# end
- 1
def respond_to(*mimes)
raise ArgumentError, "respond_to takes either types or a block, never both" if mimes.any? && block_given?
collector = Collector.new(mimes, request.variant)
yield collector if block_given?
if format = collector.negotiate_format(request)
if media_type && media_type != format
raise ActionController::RespondToMismatchError
end
_process_format(format)
_set_rendered_content_type(format) unless collector.any_response?
response = collector.response
response.call if response
else
raise ActionController::UnknownFormat
end
end
# A container for responses available from the current controller for
# requests for different mime-types sent to a particular action.
#
# The public controller methods +respond_to+ may be called with a block
# that is used to define responses to different mime-types, e.g.
# for +respond_to+ :
#
# respond_to do |format|
# format.html
# format.xml { render xml: @people }
# end
#
# In this usage, the argument passed to the block (+format+ above) is an
# instance of the ActionController::MimeResponds::Collector class. This
# object serves as a container in which available responses can be stored by
# calling any of the dynamically generated, mime-type-specific methods such
# as +html+, +xml+ etc on the Collector. Each response is represented by a
# corresponding block if present.
#
# A subsequent call to #negotiate_format(request) will enable the Collector
# to determine which specific mime-type it should respond with for the current
# request, with this response then being accessible by calling #response.
- 1
class Collector
- 1
include AbstractController::Collector
- 1
attr_accessor :format
- 1
def initialize(mimes, variant = nil)
@responses = {}
@variant = variant
mimes.each { |mime| @responses[Mime[mime]] = nil }
end
- 1
def any(*args, &block)
if args.any?
args.each { |type| send(type, &block) }
else
custom(Mime::ALL, &block)
end
end
- 1
alias :all :any
- 1
def custom(mime_type, &block)
mime_type = Mime::Type.lookup(mime_type.to_s) unless mime_type.is_a?(Mime::Type)
@responses[mime_type] ||= if block_given?
block
else
VariantCollector.new(@variant)
end
end
- 1
def any_response?
!@responses.fetch(format, false) && @responses[Mime::ALL]
end
- 1
def response
response = @responses.fetch(format, @responses[Mime::ALL])
if response.is_a?(VariantCollector) # `format.html.phone` - variant inline syntax
response.variant
elsif response.nil? || response.arity == 0 # `format.html` - just a format, call its block
response
else # `format.html{ |variant| variant.phone }` - variant block syntax
variant_collector = VariantCollector.new(@variant)
response.call(variant_collector) # call format block with variants collector
variant_collector.variant
end
end
- 1
def negotiate_format(request)
@format = request.negotiate_mime(@responses.keys)
end
- 1
class VariantCollector #:nodoc:
- 1
def initialize(variant = nil)
@variant = variant
@variants = {}
end
- 1
def any(*args, &block)
if block_given?
if args.any? && args.none? { |a| a == @variant }
args.each { |v| @variants[v] = block }
else
@variants[:any] = block
end
end
end
- 1
alias :all :any
- 1
def method_missing(name, *args, &block)
@variants[name] = block if block_given?
end
- 1
def variant
if @variant.empty?
@variants[:none] || @variants[:any]
else
@variants[variant_key]
end
end
- 1
private
- 1
def variant_key
@variant.find { |variant| @variants.key?(variant) } || :any
end
end
end
end
end
# frozen_string_literal: true
- 1
module ActionController
# Specify binary encoding for parameters for a given action.
- 1
module ParameterEncoding
- 1
extend ActiveSupport::Concern
- 1
module ClassMethods
- 1
def inherited(klass) # :nodoc:
- 271
super
- 271
klass.setup_param_encode
end
- 1
def setup_param_encode # :nodoc:
- 271
@_parameter_encodings = {}
end
- 1
def binary_params_for?(action) # :nodoc:
@_parameter_encodings[action.to_s]
end
# Specify that a given action's parameters should all be encoded as
# ASCII-8BIT (it "skips" the encoding default of UTF-8).
#
# For example, a controller would use it like this:
#
# class RepositoryController < ActionController::Base
# skip_parameter_encoding :show
#
# def show
# @repo = Repository.find_by_filesystem_path params[:file_path]
#
# # `repo_name` is guaranteed to be UTF-8, but was ASCII-8BIT, so
# # tag it as such
# @repo_name = params[:repo_name].force_encoding 'UTF-8'
# end
#
# def index
# @repositories = Repository.all
# end
# end
#
# The show action in the above controller would have all parameter values
# encoded as ASCII-8BIT. This is useful in the case where an application
# must handle data but encoding of the data is unknown, like file system data.
- 1
def skip_parameter_encoding(action)
- 2
@_parameter_encodings[action.to_s] = true
end
end
end
end
# frozen_string_literal: true
- 1
require "active_support/core_ext/hash/slice"
- 1
require "active_support/core_ext/hash/except"
- 1
require "active_support/core_ext/module/anonymous"
- 1
require "action_dispatch/http/mime_type"
- 1
module ActionController
# Wraps the parameters hash into a nested hash. This will allow clients to
# submit requests without having to specify any root elements.
#
# This functionality is enabled in +config/initializers/wrap_parameters.rb+
# and can be customized.
#
# You could also turn it on per controller by setting the format array to
# a non-empty array:
#
# class UsersController < ApplicationController
# wrap_parameters format: [:json, :xml, :url_encoded_form, :multipart_form]
# end
#
# If you enable +ParamsWrapper+ for +:json+ format, instead of having to
# send JSON parameters like this:
#
# {"user": {"name": "Konata"}}
#
# You can send parameters like this:
#
# {"name": "Konata"}
#
# And it will be wrapped into a nested hash with the key name matching the
# controller's name. For example, if you're posting to +UsersController+,
# your new +params+ hash will look like this:
#
# {"name" => "Konata", "user" => {"name" => "Konata"}}
#
# You can also specify the key in which the parameters should be wrapped to,
# and also the list of attributes it should wrap by using either +:include+ or
# +:exclude+ options like this:
#
# class UsersController < ApplicationController
# wrap_parameters :person, include: [:username, :password]
# end
#
# On Active Record models with no +:include+ or +:exclude+ option set,
# it will only wrap the parameters returned by the class method
# <tt>attribute_names</tt>.
#
# If you're going to pass the parameters to an +ActiveModel+ object (such as
# <tt>User.new(params[:user])</tt>), you might consider passing the model class to
# the method instead. The +ParamsWrapper+ will actually try to determine the
# list of attribute names from the model and only wrap those attributes:
#
# class UsersController < ApplicationController
# wrap_parameters Person
# end
#
# You still could pass +:include+ and +:exclude+ to set the list of attributes
# you want to wrap.
#
# By default, if you don't specify the key in which the parameters would be
# wrapped to, +ParamsWrapper+ will actually try to determine if there's
# a model related to it or not. This controller, for example:
#
# class Admin::UsersController < ApplicationController
# end
#
# will try to check if <tt>Admin::User</tt> or +User+ model exists, and use it to
# determine the wrapper key respectively. If both models don't exist,
# it will then fallback to use +user+ as the key.
- 1
module ParamsWrapper
- 1
extend ActiveSupport::Concern
- 1
EXCLUDE_PARAMETERS = %w(authenticity_token _method utf8)
- 1
require "mutex_m"
- 1
class Options < Struct.new(:name, :format, :include, :exclude, :klass, :model) # :nodoc:
- 1
include Mutex_m
- 1
def self.from_hash(hash)
- 5
name = hash[:name]
- 5
format = Array(hash[:format])
- 5
include = hash[:include] && Array(hash[:include]).collect(&:to_s)
- 5
exclude = hash[:exclude] && Array(hash[:exclude]).collect(&:to_s)
- 5
new name, format, include, exclude, nil, nil
end
- 1
def initialize(name, format, include, exclude, klass, model) # :nodoc:
- 5
super
- 5
@include_set = include
- 5
@name_set = name
end
- 1
def model
super || self.model = _default_wrap_model
end
- 1
def include
return super if @include_set
m = model
synchronize do
return super if @include_set
@include_set = true
unless super || exclude
if m.respond_to?(:attribute_names) && m.attribute_names.any?
if m.respond_to?(:stored_attributes) && !m.stored_attributes.empty?
self.include = m.attribute_names + m.stored_attributes.values.flatten.map(&:to_s)
else
self.include = m.attribute_names
end
if m.respond_to?(:nested_attributes_options) && m.nested_attributes_options.keys.any?
self.include += m.nested_attributes_options.keys.map do |key|
(+key.to_s).concat("_attributes")
end
end
self.include
end
end
end
end
- 1
def name
return super if @name_set
m = model
synchronize do
return super if @name_set
@name_set = true
unless super || klass.anonymous?
self.name = m ? m.to_s.demodulize.underscore :
klass.controller_name.singularize
end
end
end
- 1
private
# Determine the wrapper model from the controller's name. By convention,
# this could be done by trying to find the defined model that has the
# same singular name as the controller. For example, +UsersController+
# will try to find if the +User+ model exists.
#
# This method also does namespace lookup. Foo::Bar::UsersController will
# try to find Foo::Bar::User, Foo::User and finally User.
- 1
def _default_wrap_model
return nil if klass.anonymous?
model_name = klass.name.delete_suffix("Controller").classify
begin
if model_klass = model_name.safe_constantize
model_klass
else
namespaces = model_name.split("::")
namespaces.delete_at(-2)
break if namespaces.last == model_name
model_name = namespaces.join("::")
end
end until model_klass
model_klass
end
end
- 1
included do
- 2
class_attribute :_wrapper_options, default: Options.from_hash(format: [])
end
- 1
module ClassMethods
- 1
def _set_wrapper_options(options)
self._wrapper_options = Options.from_hash(options)
end
# Sets the name of the wrapper key, or the model which +ParamsWrapper+
# would use to determine the attribute names from.
#
# ==== Examples
# wrap_parameters format: :xml
# # enables the parameter wrapper for XML format
#
# wrap_parameters :person
# # wraps parameters into +params[:person]+ hash
#
# wrap_parameters Person
# # wraps parameters by determining the wrapper key from Person class
# # (+person+, in this case) and the list of attribute names
#
# wrap_parameters include: [:username, :title]
# # wraps only +:username+ and +:title+ attributes from parameters.
#
# wrap_parameters false
# # disables parameters wrapping for this controller altogether.
#
# ==== Options
# * <tt>:format</tt> - The list of formats in which the parameters wrapper
# will be enabled.
# * <tt>:include</tt> - The list of attribute names which parameters wrapper
# will wrap into a nested hash.
# * <tt>:exclude</tt> - The list of attribute names which parameters wrapper
# will exclude from a nested hash.
- 1
def wrap_parameters(name_or_model_or_options, options = {})
- 3
model = nil
- 3
case name_or_model_or_options
when Hash
- 1
options = name_or_model_or_options
when false
options = options.merge(format: [])
when Symbol, String
- 2
options = options.merge(name: name_or_model_or_options)
else
model = name_or_model_or_options
end
- 3
opts = Options.from_hash _wrapper_options.to_h.slice(:format).merge(options)
- 3
opts.model = model
- 3
opts.klass = self
- 3
self._wrapper_options = opts
end
# Sets the default wrapper key or model which will be used to determine
# wrapper key and attribute names. Called automatically when the
# module is inherited.
- 1
def inherited(klass)
- 282
if klass._wrapper_options.format.any?
params = klass._wrapper_options.dup
params.klass = klass
klass._wrapper_options = params
end
- 282
super
end
end
# Performs parameters wrapping upon the request. Called automatically
# by the metal call stack.
- 1
def process_action(*)
_perform_parameter_wrapping if _wrapper_enabled?
super
end
- 1
private
# Returns the wrapper key which will be used to store wrapped parameters.
- 1
def _wrapper_key
_wrapper_options.name
end
# Returns the list of enabled formats.
- 1
def _wrapper_formats
_wrapper_options.format
end
# Returns the list of parameters which will be selected for wrapped.
- 1
def _wrap_parameters(parameters)
{ _wrapper_key => _extract_parameters(parameters) }
end
- 1
def _extract_parameters(parameters)
if include_only = _wrapper_options.include
parameters.slice(*include_only)
else
exclude = _wrapper_options.exclude || []
parameters.except(*(exclude + EXCLUDE_PARAMETERS))
end
end
# Checks if we should perform parameters wrapping.
- 1
def _wrapper_enabled?
return false unless request.has_content_type?
ref = request.content_mime_type.ref
_wrapper_formats.include?(ref) && _wrapper_key && !request.parameters.key?(_wrapper_key)
end
- 1
def _perform_parameter_wrapping
wrapped_hash = _wrap_parameters request.request_parameters
wrapped_keys = request.request_parameters.keys
wrapped_filtered_hash = _wrap_parameters request.filtered_parameters.slice(*wrapped_keys)
# This will make the wrapped hash accessible from controller and view.
request.parameters.merge! wrapped_hash
request.request_parameters.merge! wrapped_hash
# This will display the wrapped hash in the log file.
request.filtered_parameters.merge! wrapped_filtered_hash
rescue ActionDispatch::Http::Parameters::ParseError
# swallow parse error exception
end
end
end
# frozen_string_literal: true
- 1
module ActionController
- 1
module Redirecting
- 1
extend ActiveSupport::Concern
- 1
include AbstractController::Logger
- 1
include ActionController::UrlFor
# Redirects the browser to the target specified in +options+. This parameter can be any one of:
#
# * <tt>Hash</tt> - The URL will be generated by calling url_for with the +options+.
# * <tt>Record</tt> - The URL will be generated by calling url_for with the +options+, which will reference a named URL for that record.
# * <tt>String</tt> starting with <tt>protocol://</tt> (like <tt>http://</tt>) or a protocol relative reference (like <tt>//</tt>) - Is passed straight through as the target for redirection.
# * <tt>String</tt> not containing a protocol - The current protocol and host is prepended to the string.
# * <tt>Proc</tt> - A block that will be executed in the controller's context. Should return any option accepted by +redirect_to+.
#
# === Examples:
#
# redirect_to action: "show", id: 5
# redirect_to @post
# redirect_to "http://www.rubyonrails.org"
# redirect_to "/images/screenshot.jpg"
# redirect_to posts_url
# redirect_to proc { edit_post_url(@post) }
#
# The redirection happens as a <tt>302 Found</tt> header unless otherwise specified using the <tt>:status</tt> option:
#
# redirect_to post_url(@post), status: :found
# redirect_to action: 'atom', status: :moved_permanently
# redirect_to post_url(@post), status: 301
# redirect_to action: 'atom', status: 302
#
# The status code can either be a standard {HTTP Status code}[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
# request then some browsers will follow the redirect using the original request
# method. This may lead to undesirable behavior such as a double DELETE. To work
# around this you can return a <tt>303 See Other</tt> status code which will be
# followed using a GET request.
#
# redirect_to posts_url, status: :see_other
# redirect_to action: 'index', status: 303
#
# It is also possible to assign a flash message as part of the redirection. There are two special accessors for the commonly used flash names
# +alert+ and +notice+ as well as a general purpose +flash+ bucket.
#
# redirect_to post_url(@post), alert: "Watch it, mister!"
# redirect_to post_url(@post), status: :found, notice: "Pay attention to the road"
# redirect_to post_url(@post), status: 301, flash: { updated_post_id: @post.id }
# redirect_to({ action: 'atom' }, alert: "Something serious happened")
#
# Statements after +redirect_to+ in our controller get executed, so +redirect_to+ doesn't stop the execution of the function.
# To terminate the execution of the function immediately after the +redirect_to+, use return.
# redirect_to post_url(@post) and return
- 1
def redirect_to(options = {}, response_options = {})
raise ActionControllerError.new("Cannot redirect to nil!") unless options
raise AbstractController::DoubleRenderError if response_body
self.status = _extract_redirect_to_status(options, response_options)
self.location = _compute_redirect_to_location(request, options)
self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(response.location)}\">redirected</a>.</body></html>"
end
# Redirects the browser to the page that issued the request (the referrer)
# if possible, otherwise redirects to the provided default fallback
# location.
#
# The referrer information is pulled from the HTTP +Referer+ (sic) header on
# the request. This is an optional header and its presence on the request is
# subject to browser security settings and user preferences. If the request
# is missing this header, the <tt>fallback_location</tt> will be used.
#
# redirect_back fallback_location: { action: "show", id: 5 }
# redirect_back fallback_location: @post
# redirect_back fallback_location: "http://www.rubyonrails.org"
# redirect_back fallback_location: "/images/screenshot.jpg"
# redirect_back fallback_location: posts_url
# redirect_back fallback_location: proc { edit_post_url(@post) }
# redirect_back fallback_location: '/', allow_other_host: false
#
# ==== Options
# * <tt>:fallback_location</tt> - The default fallback location that will be used on missing +Referer+ header.
# * <tt>:allow_other_host</tt> - 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.
- 1
def redirect_back(fallback_location:, allow_other_host: true, **args)
referer = request.headers["Referer"]
redirect_to_referer = referer && (allow_other_host || _url_host_allowed?(referer))
redirect_to redirect_to_referer ? referer : fallback_location, **args
end
- 1
def _compute_redirect_to_location(request, options) #:nodoc:
case options
# The scheme name consist of a letter followed by any combination of
# letters, digits, and the plus ("+"), period ("."), or hyphen ("-")
# characters; and is terminated by a colon (":").
# See https://tools.ietf.org/html/rfc3986#section-3.1
# The protocol relative scheme starts with a double slash "//".
when /\A([a-z][a-z\d\-+\.]*:|\/\/).*/i
options
when String
request.protocol + request.host_with_port + options
when Proc
_compute_redirect_to_location request, instance_eval(&options)
else
url_for(options)
end.delete("\0\r\n")
end
- 1
module_function :_compute_redirect_to_location
- 1
public :_compute_redirect_to_location
- 1
private
- 1
def _extract_redirect_to_status(options, response_options)
if options.is_a?(Hash) && options.key?(:status)
Rack::Utils.status_code(options.delete(:status))
elsif response_options.key?(:status)
Rack::Utils.status_code(response_options[:status])
else
302
end
end
- 1
def _url_host_allowed?(url)
URI(url.to_s).host == request.host
rescue ArgumentError, URI::Error
false
end
end
end
# frozen_string_literal: true
- 1
require "set"
- 1
module ActionController
# See <tt>Renderers.add</tt>
- 1
def self.add_renderer(key, &block)
Renderers.add(key, &block)
end
# See <tt>Renderers.remove</tt>
- 1
def self.remove_renderer(key)
Renderers.remove(key)
end
# See <tt>Responder#api_behavior</tt>
- 1
class MissingRenderer < LoadError
- 1
def initialize(format)
super "No renderer defined for format: #{format}"
end
end
- 1
module Renderers
- 1
extend ActiveSupport::Concern
# A Set containing renderer names that correspond to available renderer procs.
# Default values are <tt>:json</tt>, <tt>:js</tt>, <tt>:xml</tt>.
- 1
RENDERERS = Set.new
- 1
included do
- 3
class_attribute :_renderers, default: Set.new.freeze
end
# Used in <tt>ActionController::Base</tt>
# and <tt>ActionController::API</tt> to include all
# renderers by default.
- 1
module All
- 1
extend ActiveSupport::Concern
- 1
include Renderers
- 1
included do
- 2
self._renderers = RENDERERS
end
end
# Adds a new renderer to call within controller actions.
# A renderer is invoked by passing its name as an option to
# <tt>AbstractController::Rendering#render</tt>. To create a renderer
# pass it a name and a block. The block takes two arguments, the first
# is the value paired with its key and the second is the remaining
# hash of options passed to +render+.
#
# Create a csv renderer:
#
# ActionController::Renderers.add :csv do |obj, options|
# filename = options[:filename] || 'data'
# str = obj.respond_to?(:to_csv) ? obj.to_csv : obj.to_s
# send_data str, type: Mime[:csv],
# disposition: "attachment; filename=#{filename}.csv"
# end
#
# Note that we used Mime[:csv] for the csv mime type as it comes with Rails.
# For a custom renderer, you'll need to register a mime type with
# <tt>Mime::Type.register</tt>.
#
# To use the csv renderer in a controller action:
#
# def show
# @csvable = Csvable.find(params[:id])
# respond_to do |format|
# format.html
# format.csv { render csv: @csvable, filename: @csvable.name }
# end
# end
- 1
def self.add(key, &block)
- 3
define_method(_render_with_renderer_method_name(key), &block)
- 3
RENDERERS << key.to_sym
end
# This method is the opposite of add method.
#
# To remove a csv renderer:
#
# ActionController::Renderers.remove(:csv)
- 1
def self.remove(key)
RENDERERS.delete(key.to_sym)
method_name = _render_with_renderer_method_name(key)
remove_possible_method(method_name)
end
- 1
def self._render_with_renderer_method_name(key)
- 3
"_render_with_renderer_#{key}"
end
- 1
module ClassMethods
# Adds, by name, a renderer or renderers to the +_renderers+ available
# to call within controller actions.
#
# It is useful when rendering from an <tt>ActionController::Metal</tt> controller or
# otherwise to add an available renderer proc to a specific controller.
#
# Both <tt>ActionController::Base</tt> and <tt>ActionController::API</tt>
# include <tt>ActionController::Renderers::All</tt>, making all renderers
# available in the controller. See <tt>Renderers::RENDERERS</tt> and <tt>Renderers.add</tt>.
#
# Since <tt>ActionController::Metal</tt> controllers cannot render, the controller
# must include <tt>AbstractController::Rendering</tt>, <tt>ActionController::Rendering</tt>,
# and <tt>ActionController::Renderers</tt>, and have at least one renderer.
#
# Rather than including <tt>ActionController::Renderers::All</tt> and including all renderers,
# you may specify which renderers to include by passing the renderer name or names to
# +use_renderers+. For example, a controller that includes only the <tt>:json</tt> renderer
# (+_render_with_renderer_json+) might look like:
#
# class MetalRenderingController < ActionController::Metal
# include AbstractController::Rendering
# include ActionController::Rendering
# include ActionController::Renderers
#
# use_renderers :json
#
# def show
# render json: record
# end
# end
#
# You must specify a +use_renderer+, else the +controller.renderer+ and
# +controller._renderers+ will be <tt>nil</tt>, and the action will fail.
- 1
def use_renderers(*args)
- 1
renderers = _renderers + args
- 1
self._renderers = renderers.freeze
end
- 1
alias use_renderer use_renderers
end
# Called by +render+ in <tt>AbstractController::Rendering</tt>
# which sets the return value as the +response_body+.
#
# If no renderer is found, +super+ returns control to
# <tt>ActionView::Rendering.render_to_body</tt>, if present.
- 1
def render_to_body(options)
_render_to_body_with_renderer(options) || super
end
- 1
def _render_to_body_with_renderer(options)
_renderers.each do |name|
if options.key?(name)
_process_options(options)
method_name = Renderers._render_with_renderer_method_name(name)
return send(method_name, options.delete(name), options)
end
end
nil
end
- 1
add :json do |json, options|
json = json.to_json(options) unless json.kind_of?(String)
if options[:callback].present?
if media_type.nil? || media_type == Mime[:json]
self.content_type = Mime[:js]
end
"/**/#{options[:callback]}(#{json})"
else
self.content_type = Mime[:json] if media_type.nil?
json
end
end
- 1
add :js do |js, options|
self.content_type = Mime[:js] if media_type.nil?
js.respond_to?(:to_js) ? js.to_js(options) : js
end
- 1
add :xml do |xml, options|
self.content_type = Mime[:xml] if media_type.nil?
xml.respond_to?(:to_xml) ? xml.to_xml(options) : xml
end
end
end
# frozen_string_literal: true
- 1
module ActionController
- 1
module Rendering
- 1
extend ActiveSupport::Concern
- 1
RENDER_FORMATS_IN_PRIORITY = [:body, :plain, :html]
- 1
module ClassMethods
# Documentation at ActionController::Renderer#render
- 1
delegate :render, to: :renderer
# Returns a renderer instance (inherited from ActionController::Renderer)
# for the controller.
- 1
attr_reader :renderer
- 1
def setup_renderer! # :nodoc:
- 284
@renderer = Renderer.for(self)
end
- 1
def inherited(klass)
- 283
klass.setup_renderer!
- 283
super
end
end
# Before processing, set the request formats in current controller formats.
- 1
def process_action(*) #:nodoc:
self.formats = request.formats.map(&:ref).compact
super
end
# Check for double render errors and set the content_type after rendering.
- 1
def render(*args) #:nodoc:
raise ::AbstractController::DoubleRenderError if response_body
super
end
# Overwrite render_to_string because body can now be set to a Rack body.
- 1
def render_to_string(*)
result = super
if result.respond_to?(:each)
string = +""
result.each { |r| string << r }
string
else
result
end
end
- 1
def render_to_body(options = {})
super || _render_in_priorities(options) || " "
end
- 1
private
- 1
def _process_variant(options)
if defined?(request) && !request.nil? && request.variant.present?
options[:variant] = request.variant
end
end
- 1
def _render_in_priorities(options)
RENDER_FORMATS_IN_PRIORITY.each do |format|
return options[format] if options.key?(format)
end
nil
end
- 1
def _set_html_content_type
self.content_type = Mime[:html].to_s
end
- 1
def _set_rendered_content_type(format)
if format && !response.media_type
self.content_type = format.to_s
end
end
- 1
def _set_vary_header
if self.headers["Vary"].blank? && request.should_apply_vary_header?
self.headers["Vary"] = "Accept"
end
end
# Normalize arguments by catching blocks and setting them on :update.
- 1
def _normalize_args(action = nil, options = {}, &blk)
options = super
options[:update] = blk if block_given?
options
end
# Normalize both text and status options.
- 1
def _normalize_options(options)
_normalize_text(options)
if options[:html]
options[:html] = ERB::Util.html_escape(options[:html])
end
if options[:status]
options[:status] = Rack::Utils.status_code(options[:status])
end
super
end
- 1
def _normalize_text(options)
RENDER_FORMATS_IN_PRIORITY.each do |format|
if options.key?(format) && options[format].respond_to?(:to_text)
options[format] = options[format].to_text
end
end
end
# Process controller specific options, as status, content-type and location.
- 1
def _process_options(options)
status, content_type, location = options.values_at(:status, :content_type, :location)
self.status = status if status
self.content_type = content_type if content_type
headers["Location"] = url_for(location) if location
super
end
end
end
# frozen_string_literal: true
- 1
require "rack/session/abstract/id"
- 1
require "action_controller/metal/exceptions"
- 1
require "active_support/security_utils"
- 1
module ActionController #:nodoc:
- 1
class InvalidAuthenticityToken < ActionControllerError #:nodoc:
end
- 1
class InvalidCrossOriginRequest < ActionControllerError #:nodoc:
end
# Controller actions are protected from Cross-Site Request Forgery (CSRF) attacks
# by including a token in the rendered HTML for your application. This token is
# stored as a random string in the session, to which an attacker does not have
# access. When a request reaches your application, \Rails verifies the received
# token with the token in the session. All requests are checked except GET requests
# as these should be idempotent. Keep in mind that all session-oriented requests
# are CSRF protected by default, including JavaScript and HTML requests.
#
# Since HTML and JavaScript requests are typically made from the browser, we
# need to ensure to verify request authenticity for the web browser. We can
# use session-oriented authentication for these types of requests, by using
# the <tt>protect_from_forgery</tt> method in our controllers.
#
# GET requests are not protected since they don't have side effects like writing
# to the database and don't leak sensitive information. JavaScript requests are
# an exception: a third-party site can use a <script> tag to reference a JavaScript
# URL on your site. When your JavaScript response loads on their site, it executes.
# With carefully crafted JavaScript on their end, sensitive data in your JavaScript
# response may be extracted. To prevent this, only XmlHttpRequest (known as XHR or
# Ajax) requests are allowed to make requests for JavaScript responses.
#
# Subclasses of <tt>ActionController::Base</tt> are protected by default with the
# <tt>:exception</tt> strategy, which raises an
# <tt>ActionController::InvalidAuthenticityToken</tt> error on unverified requests.
#
# 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.
# One way to achieve this is to use the <tt>:null_session</tt> strategy instead,
# which allows unverified requests to be handled, but with an empty session:
#
# class ApplicationController < ActionController::Base
# protect_from_forgery with: :null_session
# end
#
# Note that API only applications don't include this module or a session 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
# value of this token must be added to every layout that renders forms by including
# <tt>csrf_meta_tags</tt> in the HTML +head+.
#
# Learn more about CSRF attacks and securing your application in the
# {Ruby on Rails Security Guide}[https://guides.rubyonrails.org/security.html].
- 1
module RequestForgeryProtection
- 1
extend ActiveSupport::Concern
- 1
include AbstractController::Helpers
- 1
include AbstractController::Callbacks
- 1
included do
# Sets the token parameter name for RequestForgery. Calling +protect_from_forgery+
# sets it to <tt>:authenticity_token</tt> by default.
- 1
config_accessor :request_forgery_protection_token
- 1
self.request_forgery_protection_token ||= :authenticity_token
# Holds the class which implements the request forgery protection.
- 1
config_accessor :forgery_protection_strategy
- 1
self.forgery_protection_strategy = nil
# Controls whether request forgery protection is turned on or not. Turned off by default only in test mode.
- 1
config_accessor :allow_forgery_protection
- 1
self.allow_forgery_protection = true if allow_forgery_protection.nil?
# Controls whether a CSRF failure logs a warning. On by default.
- 1
config_accessor :log_warning_on_csrf_failure
- 1
self.log_warning_on_csrf_failure = true
# Controls whether the Origin header is checked in addition to the CSRF token.
- 1
config_accessor :forgery_protection_origin_check
- 1
self.forgery_protection_origin_check = false
# Controls whether form-action/method specific CSRF tokens are used.
- 1
config_accessor :per_form_csrf_tokens
- 1
self.per_form_csrf_tokens = false
# Controls whether forgery protection is enabled by default.
- 1
config_accessor :default_protect_from_forgery
- 1
self.default_protect_from_forgery = false
# Controls whether URL-safe CSRF tokens are generated.
- 1
config_accessor :urlsafe_csrf_tokens, instance_writer: false
- 1
self.urlsafe_csrf_tokens = false
- 1
helper_method :form_authenticity_token
- 1
helper_method :protect_against_forgery?
end
- 1
module ClassMethods
# Turn on request forgery protection. Bear in mind that GET and HEAD requests are not checked.
#
# class ApplicationController < ActionController::Base
# protect_from_forgery
# end
#
# class FooController < ApplicationController
# protect_from_forgery except: :index
# end
#
# You can disable forgery protection on controller by skipping the verification before_action:
#
# skip_before_action :verify_authenticity_token
#
# Valid Options:
#
# * <tt>:only/:except</tt> - Only apply forgery protection to a subset of actions. For example <tt>only: [ :create, :create_all ]</tt>.
# * <tt>:if/:unless</tt> - Turn off the forgery protection entirely depending on the passed Proc or method reference.
# * <tt>:prepend</tt> - By default, the verification of the authentication token will be added at the position of the
# protect_from_forgery call in your application. This means any callbacks added before are run first. This is useful
# when you want your forgery protection to depend on other callbacks, like authentication methods (Oauth vs Cookie auth).
#
# If you need to add verification to the beginning of the callback chain, use <tt>prepend: true</tt>.
# * <tt>:with</tt> - Set the method to handle unverified request.
#
# Valid unverified request handling methods are:
# * <tt>:exception</tt> - Raises ActionController::InvalidAuthenticityToken exception.
# * <tt>:reset_session</tt> - Resets the session.
# * <tt>:null_session</tt> - Provides an empty session during request but doesn't reset it completely. Used as default if <tt>:with</tt> option is not specified.
- 1
def protect_from_forgery(options = {})
- 12
options = options.reverse_merge(prepend: false)
- 12
self.forgery_protection_strategy = protection_method_class(options[:with] || :null_session)
- 12
self.request_forgery_protection_token ||= :authenticity_token
- 12
before_action :verify_authenticity_token, options
- 12
append_after_action :verify_same_origin_request
end
# Turn off request forgery protection. This is a wrapper for:
#
# skip_before_action :verify_authenticity_token
#
# See +skip_before_action+ for allowed options.
- 1
def skip_forgery_protection(options = {})
- 1
skip_before_action :verify_authenticity_token, options
end
- 1
private
- 1
def protection_method_class(name)
- 12
ActionController::RequestForgeryProtection::ProtectionMethods.const_get(name.to_s.classify)
rescue NameError
raise ArgumentError, "Invalid request forgery protection method, use :null_session, :exception, or :reset_session"
end
end
- 1
module ProtectionMethods
- 1
class NullSession
- 1
def initialize(controller)
@controller = controller
end
# This is the method that defines the application behavior when a request is found to be unverified.
- 1
def handle_unverified_request
request = @controller.request
request.session = NullSessionHash.new(request)
request.flash = nil
request.session_options = { skip: true }
request.cookie_jar = NullCookieJar.build(request, {})
end
- 1
private
- 1
class NullSessionHash < Rack::Session::Abstract::SessionHash #:nodoc:
- 1
def initialize(req)
super(nil, req)
@data = {}
@loaded = true
end
# no-op
- 1
def destroy; end
- 1
def exists?
true
end
end
- 1
class NullCookieJar < ActionDispatch::Cookies::CookieJar #:nodoc:
- 1
def write(*)
# nothing
end
end
end
- 1
class ResetSession
- 1
def initialize(controller)
@controller = controller
end
- 1
def handle_unverified_request
@controller.reset_session
end
end
- 1
class Exception
- 1
def initialize(controller)
@controller = controller
end
- 1
def handle_unverified_request
raise ActionController::InvalidAuthenticityToken
end
end
end
- 1
private
# The actual before_action that is used to verify the CSRF token.
# Don't override this directly. Provide your own forgery protection
# strategy instead. If you override, you'll disable same-origin
# <tt><script></tt> verification.
#
# Lean on the protect_from_forgery declaration to mark which actions are
# due for same-origin request verification. If protect_from_forgery is
# enabled on an action, this before_action flags its after_action to
# verify that JavaScript responses are for XHR requests, ensuring they
# follow the browser's same-origin policy.
- 1
def verify_authenticity_token # :doc:
mark_for_same_origin_verification!
if !verified_request?
if logger && log_warning_on_csrf_failure
if valid_request_origin?
logger.warn "Can't verify CSRF token authenticity."
else
logger.warn "HTTP Origin header (#{request.origin}) didn't match request.base_url (#{request.base_url})"
end
end
handle_unverified_request
end
end
- 1
def handle_unverified_request # :doc:
forgery_protection_strategy.new(self).handle_unverified_request
end
#:nodoc:
- 1
CROSS_ORIGIN_JAVASCRIPT_WARNING = "Security warning: an embedded " \
"<script> tag on another site requested protected JavaScript. " \
"If you know what you're doing, go ahead and disable forgery " \
"protection on this action to permit cross-origin JavaScript embedding."
- 1
private_constant :CROSS_ORIGIN_JAVASCRIPT_WARNING
# :startdoc:
# If +verify_authenticity_token+ was run (indicating that we have
# forgery protection enabled for this request) then also verify that
# we aren't serving an unauthorized cross-origin response.
- 1
def verify_same_origin_request # :doc:
if marked_for_same_origin_verification? && non_xhr_javascript_response?
if logger && log_warning_on_csrf_failure
logger.warn CROSS_ORIGIN_JAVASCRIPT_WARNING
end
raise ActionController::InvalidCrossOriginRequest, CROSS_ORIGIN_JAVASCRIPT_WARNING
end
end
# GET requests are checked for cross-origin JavaScript after rendering.
- 1
def mark_for_same_origin_verification! # :doc:
@marked_for_same_origin_verification = request.get?
end
# If the +verify_authenticity_token+ before_action ran, verify that
# JavaScript responses are only served to same-origin GET requests.
- 1
def marked_for_same_origin_verification? # :doc:
@marked_for_same_origin_verification ||= false
end
# Check for cross-origin JavaScript responses.
- 1
def non_xhr_javascript_response? # :doc:
%r(\A(?:text|application)/javascript).match?(media_type) && !request.xhr?
end
- 1
AUTHENTICITY_TOKEN_LENGTH = 32
# Returns true or false if a request is verified. Checks:
#
# * Is it a GET or HEAD request? GETs should be safe and idempotent
# * Does the form_authenticity_token match the given token value from the params?
# * Does the X-CSRF-Token header match the form_authenticity_token?
- 1
def verified_request? # :doc:
!protect_against_forgery? || request.get? || request.head? ||
(valid_request_origin? && any_authenticity_token_valid?)
end
# Checks if any of the authenticity tokens from the request are valid.
- 1
def any_authenticity_token_valid? # :doc:
request_authenticity_tokens.any? do |token|
valid_authenticity_token?(session, token)
end
end
# Possible authenticity tokens sent in the request.
- 1
def request_authenticity_tokens # :doc:
[form_authenticity_param, request.x_csrf_token]
end
# Sets the token value for the current session.
- 1
def form_authenticity_token(form_options: {})
masked_authenticity_token(session, form_options: form_options)
end
# Creates a masked version of the authenticity token that varies
# on each request. The masking is used to mitigate SSL attacks
# like BREACH.
- 1
def masked_authenticity_token(session, form_options: {}) # :doc:
action, method = form_options.values_at(:action, :method)
raw_token = if per_form_csrf_tokens && action && method
action_path = normalize_action_path(action)
per_form_csrf_token(session, action_path, method)
else
global_csrf_token(session)
end
mask_token(raw_token)
end
# Checks the client's masked token to see if it matches the
# session token. Essentially the inverse of
# +masked_authenticity_token+.
- 1
def valid_authenticity_token?(session, encoded_masked_token) # :doc:
if encoded_masked_token.nil? || encoded_masked_token.empty? || !encoded_masked_token.is_a?(String)
return false
end
begin
masked_token = decode_csrf_token(encoded_masked_token)
rescue ArgumentError # encoded_masked_token is invalid Base64
return false
end
# See if it's actually a masked token or not. In order to
# deploy this code, we should be able to handle any unmasked
# tokens that we've issued without error.
if masked_token.length == AUTHENTICITY_TOKEN_LENGTH
# This is actually an unmasked token. This is expected if
# you have just upgraded to masked tokens, but should stop
# happening shortly after installing this gem.
compare_with_real_token masked_token, session
elsif masked_token.length == AUTHENTICITY_TOKEN_LENGTH * 2
csrf_token = unmask_token(masked_token)
compare_with_global_token(csrf_token, session) ||
compare_with_real_token(csrf_token, session) ||
valid_per_form_csrf_token?(csrf_token, session)
else
false # Token is malformed.
end
end
- 1
def unmask_token(masked_token) # :doc:
# Split the token into the one-time pad and the encrypted
# value and decrypt it.
one_time_pad = masked_token[0...AUTHENTICITY_TOKEN_LENGTH]
encrypted_csrf_token = masked_token[AUTHENTICITY_TOKEN_LENGTH..-1]
xor_byte_strings(one_time_pad, encrypted_csrf_token)
end
- 1
def mask_token(raw_token) # :doc:
one_time_pad = SecureRandom.random_bytes(AUTHENTICITY_TOKEN_LENGTH)
encrypted_csrf_token = xor_byte_strings(one_time_pad, raw_token)
masked_token = one_time_pad + encrypted_csrf_token
encode_csrf_token(masked_token)
end
- 1
def compare_with_real_token(token, session) # :doc:
ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, real_csrf_token(session))
end
- 1
def compare_with_global_token(token, session) # :doc:
ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, global_csrf_token(session))
end
- 1
def valid_per_form_csrf_token?(token, session) # :doc:
if per_form_csrf_tokens
correct_token = per_form_csrf_token(
session,
request.path.chomp("/"),
request.request_method
)
ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, correct_token)
else
false
end
end
- 1
def real_csrf_token(session) # :doc:
session[:_csrf_token] ||= generate_csrf_token
decode_csrf_token(session[:_csrf_token])
end
- 1
def per_form_csrf_token(session, action_path, method) # :doc:
csrf_token_hmac(session, [action_path, method.downcase].join("#"))
end
- 1
GLOBAL_CSRF_TOKEN_IDENTIFIER = "!real_csrf_token"
- 1
private_constant :GLOBAL_CSRF_TOKEN_IDENTIFIER
- 1
def global_csrf_token(session) # :doc:
csrf_token_hmac(session, GLOBAL_CSRF_TOKEN_IDENTIFIER)
end
- 1
def csrf_token_hmac(session, identifier) # :doc:
OpenSSL::HMAC.digest(
OpenSSL::Digest::SHA256.new,
real_csrf_token(session),
identifier
)
end
- 1
def xor_byte_strings(s1, s2) # :doc:
s2 = s2.dup
size = s1.bytesize
i = 0
while i < size
s2.setbyte(i, s1.getbyte(i) ^ s2.getbyte(i))
i += 1
end
s2
end
# The form's authenticity parameter. Override to provide your own.
- 1
def form_authenticity_param # :doc:
params[request_forgery_protection_token]
end
# Checks if the controller allows forgery protection.
- 1
def protect_against_forgery? # :doc:
allow_forgery_protection
end
- 1
NULL_ORIGIN_MESSAGE = <<~MSG
The browser returned a 'null' origin for a request with origin-based forgery protection turned on. This usually
means you have the 'no-referrer' Referrer-Policy header enabled, or that the request came from a site that
refused to give its origin. This makes it impossible for Rails to verify the source of the requests. Likely the
best solution is to change your referrer policy to something less strict like same-origin or strict-origin.
If you cannot change the referrer policy, you can disable origin checking with the
Rails.application.config.action_controller.forgery_protection_origin_check setting.
MSG
# Checks if the request originated from the same origin by looking at the
# Origin header.
- 1
def valid_request_origin? # :doc:
if forgery_protection_origin_check
# We accept blank origin headers because some user agents don't send it.
raise InvalidAuthenticityToken, NULL_ORIGIN_MESSAGE if request.origin == "null"
request.origin.nil? || request.origin == request.base_url
else
true
end
end
- 1
def normalize_action_path(action_path) # :doc:
uri = URI.parse(action_path)
uri.path.chomp("/")
end
- 1
def generate_csrf_token # :nodoc:
if urlsafe_csrf_tokens
SecureRandom.urlsafe_base64(AUTHENTICITY_TOKEN_LENGTH, padding: false)
else
SecureRandom.base64(AUTHENTICITY_TOKEN_LENGTH)
end
end
- 1
def encode_csrf_token(csrf_token) # :nodoc:
if urlsafe_csrf_tokens
Base64.urlsafe_encode64(csrf_token, padding: false)
else
Base64.strict_encode64(csrf_token)
end
end
- 1
def decode_csrf_token(encoded_csrf_token) # :nodoc:
if urlsafe_csrf_tokens
Base64.urlsafe_decode64(encoded_csrf_token)
else
begin
Base64.strict_decode64(encoded_csrf_token)
rescue ArgumentError
Base64.urlsafe_decode64(encoded_csrf_token)
end
end
end
end
end
# frozen_string_literal: true
- 1
module ActionController #:nodoc:
# This module is responsible for providing +rescue_from+ helpers
# to controllers and configuring when detailed exceptions must be
# shown.
- 1
module Rescue
- 1
extend ActiveSupport::Concern
- 1
include ActiveSupport::Rescuable
# Override this method if you want to customize when detailed
# exceptions must be shown. This method is only called when
# +consider_all_requests_local+ is +false+. By default, it returns
# +false+, but someone may set it to <tt>request.local?</tt> so local
# requests in production still show the detailed exception pages.
- 1
def show_detailed_exceptions?
false
end
- 1
private
- 1
def process_action(*)
super
rescue Exception => exception
request.env["action_dispatch.show_detailed_exceptions"] ||= show_detailed_exceptions?
rescue_with_handler(exception) || raise
end
end
end
# frozen_string_literal: true
- 1
require "rack/chunked"
- 1
module ActionController #:nodoc:
# Allows views to be streamed back to the client as they are rendered.
#
# By default, Rails renders views by first rendering the template
# and then the layout. The response is sent to the client after the whole
# template is rendered, all queries are made, and the layout is processed.
#
# Streaming inverts the rendering flow by rendering the layout first and
# streaming each part of the layout as they are processed. This allows the
# header of the HTML (which is usually in the layout) to be streamed back
# to client very quickly, allowing JavaScripts and stylesheets to be loaded
# earlier than usual.
#
# This approach was introduced in Rails 3.1 and is still improving. Several
# Rack middlewares may not work and you need to be careful when streaming.
# Those points are going to be addressed soon.
#
# In order to use streaming, you will need to use a Ruby version that
# supports fibers (fibers are supported since version 1.9.2 of the main
# Ruby implementation).
#
# Streaming can be added to a given template easily, all you need to do is
# to pass the :stream option.
#
# class PostsController
# def index
# @posts = Post.all
# render stream: true
# end
# end
#
# == When to use streaming
#
# Streaming may be considered to be overkill for lightweight actions like
# +new+ or +edit+. The real benefit of streaming is on expensive actions
# that, for example, do a lot of queries on the database.
#
# In such actions, you want to delay queries execution as much as you can.
# For example, imagine the following +dashboard+ action:
#
# def dashboard
# @posts = Post.all
# @pages = Page.all
# @articles = Article.all
# end
#
# Most of the queries here are happening in the controller. In order to benefit
# from streaming you would want to rewrite it as:
#
# def dashboard
# # Allow lazy execution of the queries
# @posts = Post.all
# @pages = Page.all
# @articles = Article.all
# render stream: true
# end
#
# Notice that :stream only works with templates. Rendering :json
# or :xml with :stream won't work.
#
# == Communication between layout and template
#
# When streaming, rendering happens top-down instead of inside-out.
# Rails starts with the layout, and the template is rendered later,
# when its +yield+ is reached.
#
# This means that, if your application currently relies on instance
# variables set in the template to be used in the layout, they won't
# work once you move to streaming. The proper way to communicate
# between layout and template, regardless of whether you use streaming
# or not, is by using +content_for+, +provide+ and +yield+.
#
# Take a simple example where the layout expects the template to tell
# which title to use:
#
# <html>
# <head><title><%= yield :title %></title></head>
# <body><%= yield %></body>
# </html>
#
# You would use +content_for+ in your template to specify the title:
#
# <%= content_for :title, "Main" %>
# Hello
#
# And the final result would be:
#
# <html>
# <head><title>Main</title></head>
# <body>Hello</body>
# </html>
#
# However, if +content_for+ is called several times, the final result
# would have all calls concatenated. For instance, if we have the following
# template:
#
# <%= content_for :title, "Main" %>
# Hello
# <%= content_for :title, " page" %>
#
# The final result would be:
#
# <html>
# <head><title>Main page</title></head>
# <body>Hello</body>
# </html>
#
# This means that, if you have <code>yield :title</code> in your layout
# and you want to use streaming, you would have to render the whole template
# (and eventually trigger all queries) before streaming the title and all
# assets, which kills the purpose of streaming. For this purpose, you can use
# a helper called +provide+ that does the same as +content_for+ but tells the
# layout to stop searching for other entries and continue rendering.
#
# For instance, the template above using +provide+ would be:
#
# <%= provide :title, "Main" %>
# Hello
# <%= content_for :title, " page" %>
#
# Giving:
#
# <html>
# <head><title>Main</title></head>
# <body>Hello</body>
# </html>
#
# That said, when streaming, you need to properly check your templates
# and choose when to use +provide+ and +content_for+.
#
# == Headers, cookies, session and flash
#
# When streaming, the HTTP headers are sent to the client right before
# it renders the first line. This means that, modifying headers, cookies,
# session or flash after the template starts rendering will not propagate
# to the client.
#
# == Middlewares
#
# Middlewares that need to manipulate the body won't work with streaming.
# You should disable those middlewares whenever streaming in development
# or production. For instance, <tt>Rack::Bug</tt> won't work when streaming as it
# needs to inject contents in the HTML body.
#
# Also <tt>Rack::Cache</tt> won't work with streaming as it does not support
# streaming bodies yet. Whenever streaming Cache-Control is automatically
# set to "no-cache".
#
# == Errors
#
# When it comes to streaming, exceptions get a bit more complicated. This
# happens because part of the template was already rendered and streamed to
# the client, making it impossible to render a whole exception page.
#
# Currently, when an exception happens in development or production, Rails
# will automatically stream to the client:
#
# "><script>window.location = "/500.html"</script></html>
#
# The first two characters (">) are required in case the exception happens
# while rendering attributes for a given tag. You can check the real cause
# for the exception in your logger.
#
# == Web server support
#
# Not all web servers support streaming out-of-the-box. You need to check
# the instructions for each of them.
#
# ==== Unicorn
#
# Unicorn supports streaming but it needs to be configured. For this, you
# need to create a config file as follow:
#
# # unicorn.config.rb
# listen 3000, tcp_nopush: false
#
# And use it on initialization:
#
# unicorn_rails --config-file unicorn.config.rb
#
# You may also want to configure other parameters like <tt>:tcp_nodelay</tt>.
# Please check its documentation for more information: https://bogomips.org/unicorn/Unicorn/Configurator.html#method-i-listen
#
# If you are using Unicorn with NGINX, you may need to tweak NGINX.
# Streaming should work out of the box on Rainbows.
#
# ==== Passenger
#
# To be described.
#
- 1
module Streaming
- 1
extend ActiveSupport::Concern
- 1
private
# Set proper cache control and transfer encoding when streaming
- 1
def _process_options(options)
super
if options[:stream]
if request.version == "HTTP/1.0"
options.delete(:stream)
else
headers["Cache-Control"] ||= "no-cache"
headers["Transfer-Encoding"] = "chunked"
headers.delete("Content-Length")
end
end
end
# Call render_body if we are streaming instead of usual +render+.
- 1
def _render_template(options)
if options.delete(:stream)
Rack::Chunked::Body.new view_renderer.render_body(view_context, options)
else
super
end
end
end
end
# frozen_string_literal: true
- 1
require "active_support/core_ext/hash/indifferent_access"
- 1
require "active_support/core_ext/array/wrap"
- 1
require "active_support/core_ext/string/filters"
- 1
require "active_support/core_ext/object/to_query"
- 1
require "action_dispatch/http/upload"
- 1
require "rack/test"
- 1
require "stringio"
- 1
require "set"
- 1
require "yaml"
- 1
module ActionController
# Raised when a required parameter is missing.
#
# params = ActionController::Parameters.new(a: {})
# params.fetch(:b)
# # => ActionController::ParameterMissing: param is missing or the value is empty: b
# params.require(:a)
# # => ActionController::ParameterMissing: param is missing or the value is empty: a
- 1
class ParameterMissing < KeyError
- 1
attr_reader :param, :keys # :nodoc:
- 1
def initialize(param, keys = nil) # :nodoc:
@param = param
@keys = keys
super("param is missing or the value is empty: #{param}")
end
- 1
class Correction
- 1
def initialize(error)
@error = error
end
- 1
def corrections
if @error.param && @error.keys
maybe_these = @error.keys
maybe_these.sort_by { |n|
DidYouMean::Jaro.distance(@error.param.to_s, n)
}.reverse.first(4)
else
[]
end
end
end
# We may not have DYM, and DYM might not let us register error handlers
- 1
if defined?(DidYouMean) && DidYouMean.respond_to?(:correct_error)
DidYouMean.correct_error(self, Correction)
end
end
# Raised when a supplied parameter is not expected and
# ActionController::Parameters.action_on_unpermitted_parameters
# is set to <tt>:raise</tt>.
#
# params = ActionController::Parameters.new(a: "123", b: "456")
# params.permit(:c)
# # => ActionController::UnpermittedParameters: found unpermitted parameters: :a, :b
- 1
class UnpermittedParameters < IndexError
- 1
attr_reader :params # :nodoc:
- 1
def initialize(params) # :nodoc:
@params = params
super("found unpermitted parameter#{'s' if params.size > 1 }: #{params.map { |e| ":#{e}" }.join(", ")}")
end
end
# Raised when a Parameters instance is not marked as permitted and
# an operation to transform it to hash is called.
#
# params = ActionController::Parameters.new(a: "123", b: "456")
# params.to_h
# # => ActionController::UnfilteredParameters: unable to convert unpermitted parameters to hash
- 1
class UnfilteredParameters < ArgumentError
- 1
def initialize # :nodoc:
super("unable to convert unpermitted parameters to hash")
end
end
# == Action Controller \Parameters
#
# Allows you to choose which attributes should be permitted for mass updating
# and thus prevent accidentally exposing that which shouldn't be exposed.
# Provides two methods for this purpose: #require and #permit. The former is
# used to mark parameters as required. The latter is used to set the parameter
# as permitted and limit which attributes should be allowed for mass updating.
#
# params = ActionController::Parameters.new({
# person: {
# name: "Francesco",
# age: 22,
# role: "admin"
# }
# })
#
# permitted = params.require(:person).permit(:name, :age)
# permitted # => <ActionController::Parameters {"name"=>"Francesco", "age"=>22} permitted: true>
# permitted.permitted? # => true
#
# Person.first.update!(permitted)
# # => #<Person id: 1, name: "Francesco", age: 22, role: "user">
#
# It provides two options that controls the top-level behavior of new instances:
#
# * +permit_all_parameters+ - If it's +true+, all the parameters will be
# permitted by default. The default is +false+.
# * +action_on_unpermitted_parameters+ - Allow to control the behavior when parameters
# that are not explicitly permitted are found. The values can be +false+ to just filter them
# out, <tt>:log</tt> to additionally write a message on the logger, or <tt>:raise</tt> to raise
# ActionController::UnpermittedParameters exception. The default value is <tt>:log</tt>
# in test and development environments, +false+ otherwise.
#
# Examples:
#
# params = ActionController::Parameters.new
# params.permitted? # => false
#
# ActionController::Parameters.permit_all_parameters = true
#
# params = ActionController::Parameters.new
# params.permitted? # => true
#
# params = ActionController::Parameters.new(a: "123", b: "456")
# params.permit(:c)
# # => <ActionController::Parameters {} permitted: true>
#
# ActionController::Parameters.action_on_unpermitted_parameters = :raise
#
# params = ActionController::Parameters.new(a: "123", b: "456")
# params.permit(:c)
# # => ActionController::UnpermittedParameters: found unpermitted keys: a, b
#
# Please note that these options *are not thread-safe*. In a multi-threaded
# environment they should only be set once at boot-time and never mutated at
# runtime.
#
# You can fetch values of <tt>ActionController::Parameters</tt> using either
# <tt>:key</tt> or <tt>"key"</tt>.
#
# params = ActionController::Parameters.new(key: "value")
# params[:key] # => "value"
# params["key"] # => "value"
- 1
class Parameters
- 1
cattr_accessor :permit_all_parameters, instance_accessor: false, default: false
- 1
cattr_accessor :action_on_unpermitted_parameters, instance_accessor: false
##
# :method: as_json
#
# :call-seq:
# as_json(options=nil)
#
# Returns a hash that can be used as the JSON representation for the parameters.
##
# :method: each_key
#
# :call-seq:
# each_key()
#
# Calls block once for each key in the parameters, passing the key.
# If no block is given, an enumerator is returned instead.
##
# :method: empty?
#
# :call-seq:
# empty?()
#
# Returns true if the parameters have no key/value pairs.
##
# :method: has_key?
#
# :call-seq:
# has_key?(key)
#
# Returns true if the given key is present in the parameters.
##
# :method: has_value?
#
# :call-seq:
# has_value?(value)
#
# Returns true if the given value is present for some key in the parameters.
##
# :method: include?
#
# :call-seq:
# include?(key)
#
# Returns true if the given key is present in the parameters.
##
# :method: key?
#
# :call-seq:
# key?(key)
#
# Returns true if the given key is present in the parameters.
##
# :method: member?
#
# :call-seq:
# member?(key)
#
# Returns true if the given key is present in the parameters.
##
# :method: keys
#
# :call-seq:
# keys()
#
# Returns a new array of the keys of the parameters.
##
# :method: to_s
#
# :call-seq:
# to_s()
#
# Returns the content of the parameters as a string.
##
# :method: value?
#
# :call-seq:
# value?(value)
#
# Returns true if the given value is present for some key in the parameters.
##
# :method: values
#
# :call-seq:
# values()
#
# Returns a new array of the values of the parameters.
- 1
delegate :keys, :key?, :has_key?, :member?, :values, :has_value?, :value?, :empty?, :include?,
:as_json, :to_s, :each_key, to: :@parameters
# By default, never raise an UnpermittedParameters exception if these
# params are present. The default includes both 'controller' and 'action'
# because they are added by Rails and should be of no concern. One way
# to change these is to specify `always_permitted_parameters` in your
# config. For instance:
#
# config.action_controller.always_permitted_parameters = %w( controller action format )
- 1
cattr_accessor :always_permitted_parameters, default: %w( controller action )
- 1
class << self
- 1
def nested_attribute?(key, value) # :nodoc:
/\A-?\d+\z/.match?(key) && (value.is_a?(Hash) || value.is_a?(Parameters))
end
end
# Returns a new instance of <tt>ActionController::Parameters</tt>.
# Also, sets the +permitted+ attribute to the default value of
# <tt>ActionController::Parameters.permit_all_parameters</tt>.
#
# class Person < ActiveRecord::Base
# end
#
# params = ActionController::Parameters.new(name: "Francesco")
# params.permitted? # => false
# Person.new(params) # => ActiveModel::ForbiddenAttributesError
#
# ActionController::Parameters.permit_all_parameters = true
#
# params = ActionController::Parameters.new(name: "Francesco")
# params.permitted? # => true
# Person.new(params) # => #<Person id: nil, name: "Francesco">
- 1
def initialize(parameters = {})
@parameters = parameters.with_indifferent_access
@permitted = self.class.permit_all_parameters
end
# Returns true if another +Parameters+ object contains the same content and
# permitted flag.
- 1
def ==(other)
if other.respond_to?(:permitted?)
permitted? == other.permitted? && parameters == other.parameters
else
@parameters == other
end
end
- 1
alias eql? ==
- 1
def hash
[@parameters.hash, @permitted].hash
end
# Returns a safe <tt>ActiveSupport::HashWithIndifferentAccess</tt>
# representation of the parameters with all unpermitted keys removed.
#
# params = ActionController::Parameters.new({
# name: "Senjougahara Hitagi",
# oddity: "Heavy stone crab"
# })
# params.to_h
# # => ActionController::UnfilteredParameters: unable to convert unpermitted parameters to hash
#
# safe_params = params.permit(:name)
# safe_params.to_h # => {"name"=>"Senjougahara Hitagi"}
- 1
def to_h
if permitted?
convert_parameters_to_hashes(@parameters, :to_h)
else
raise UnfilteredParameters
end
end
# Returns a safe <tt>Hash</tt> representation of the parameters
# with all unpermitted keys removed.
#
# params = ActionController::Parameters.new({
# name: "Senjougahara Hitagi",
# oddity: "Heavy stone crab"
# })
# params.to_hash
# # => ActionController::UnfilteredParameters: unable to convert unpermitted parameters to hash
#
# safe_params = params.permit(:name)
# safe_params.to_hash # => {"name"=>"Senjougahara Hitagi"}
- 1
def to_hash
to_h.to_hash
end
# Returns a string representation of the receiver suitable for use as a URL
# query string:
#
# params = ActionController::Parameters.new({
# name: "David",
# nationality: "Danish"
# })
# params.to_query
# # => ActionController::UnfilteredParameters: unable to convert unpermitted parameters to hash
#
# safe_params = params.permit(:name, :nationality)
# safe_params.to_query
# # => "name=David&nationality=Danish"
#
# An optional namespace can be passed to enclose key names:
#
# params = ActionController::Parameters.new({
# name: "David",
# nationality: "Danish"
# })
# safe_params = params.permit(:name, :nationality)
# safe_params.to_query("user")
# # => "user%5Bname%5D=David&user%5Bnationality%5D=Danish"
#
# The string pairs "key=value" that conform the query string
# are sorted lexicographically in ascending order.
#
# This method is also aliased as +to_param+.
- 1
def to_query(*args)
to_h.to_query(*args)
end
- 1
alias_method :to_param, :to_query
# Returns an unsafe, unfiltered
# <tt>ActiveSupport::HashWithIndifferentAccess</tt> representation of the
# parameters.
#
# params = ActionController::Parameters.new({
# name: "Senjougahara Hitagi",
# oddity: "Heavy stone crab"
# })
# params.to_unsafe_h
# # => {"name"=>"Senjougahara Hitagi", "oddity" => "Heavy stone crab"}
- 1
def to_unsafe_h
convert_parameters_to_hashes(@parameters, :to_unsafe_h)
end
- 1
alias_method :to_unsafe_hash, :to_unsafe_h
# Convert all hashes in values into parameters, then yield each pair in
# the same way as <tt>Hash#each_pair</tt>.
- 1
def each_pair(&block)
return to_enum(__callee__) unless block_given?
@parameters.each_pair do |key, value|
yield [key, convert_hashes_to_parameters(key, value)]
end
self
end
- 1
alias_method :each, :each_pair
# Convert all hashes in values into parameters, then yield each value in
# the same way as <tt>Hash#each_value</tt>.
- 1
def each_value(&block)
return to_enum(:each_value) unless block_given?
@parameters.each_pair do |key, value|
yield convert_hashes_to_parameters(key, value)
end
self
end
# Attribute that keeps track of converted arrays, if any, to avoid double
# looping in the common use case permit + mass-assignment. Defined in a
# method to instantiate it only if needed.
#
# Testing membership still loops, but it's going to be faster than our own
# loop that converts values. Also, we are not going to build a new array
# object per fetch.
- 1
def converted_arrays
@converted_arrays ||= Set.new
end
# Returns +true+ if the parameter is permitted, +false+ otherwise.
#
# params = ActionController::Parameters.new
# params.permitted? # => false
# params.permit!
# params.permitted? # => true
- 1
def permitted?
@permitted
end
# Sets the +permitted+ attribute to +true+. This can be used to pass
# mass assignment. Returns +self+.
#
# class Person < ActiveRecord::Base
# end
#
# params = ActionController::Parameters.new(name: "Francesco")
# params.permitted? # => false
# Person.new(params) # => ActiveModel::ForbiddenAttributesError
# params.permit!
# params.permitted? # => true
# Person.new(params) # => #<Person id: nil, name: "Francesco">
- 1
def permit!
each_pair do |key, value|
Array.wrap(value).flatten.each do |v|
v.permit! if v.respond_to? :permit!
end
end
@permitted = true
self
end
# This method accepts both a single key and an array of keys.
#
# When passed a single key, if it exists and its associated value is
# either present or the singleton +false+, returns said value:
#
# ActionController::Parameters.new(person: { name: "Francesco" }).require(:person)
# # => <ActionController::Parameters {"name"=>"Francesco"} permitted: false>
#
# Otherwise raises <tt>ActionController::ParameterMissing</tt>:
#
# ActionController::Parameters.new.require(:person)
# # ActionController::ParameterMissing: param is missing or the value is empty: person
#
# ActionController::Parameters.new(person: nil).require(:person)
# # ActionController::ParameterMissing: param is missing or the value is empty: person
#
# ActionController::Parameters.new(person: "\t").require(:person)
# # ActionController::ParameterMissing: param is missing or the value is empty: person
#
# ActionController::Parameters.new(person: {}).require(:person)
# # ActionController::ParameterMissing: param is missing or the value is empty: person
#
# When given an array of keys, the method tries to require each one of them
# in order. If it succeeds, an array with the respective return values is
# returned:
#
# params = ActionController::Parameters.new(user: { ... }, profile: { ... })
# user_params, profile_params = params.require([:user, :profile])
#
# Otherwise, the method re-raises the first exception found:
#
# params = ActionController::Parameters.new(user: {}, profile: {})
# user_params, profile_params = params.require([:user, :profile])
# # ActionController::ParameterMissing: param is missing or the value is empty: user
#
# Technically this method can be used to fetch terminal values:
#
# # CAREFUL
# params = ActionController::Parameters.new(person: { name: "Finn" })
# name = params.require(:person).require(:name) # CAREFUL
#
# but take into account that at some point those ones have to be permitted:
#
# def person_params
# params.require(:person).permit(:name).tap do |person_params|
# person_params.require(:name) # SAFER
# end
# end
#
# for example.
- 1
def require(key)
return key.map { |k| require(k) } if key.is_a?(Array)
value = self[key]
if value.present? || value == false
value
else
raise ParameterMissing.new(key, @parameters.keys)
end
end
# Alias of #require.
- 1
alias :required :require
# Returns a new <tt>ActionController::Parameters</tt> instance that
# includes only the given +filters+ and sets the +permitted+ attribute
# for the object to +true+. This is useful for limiting which attributes
# should be allowed for mass updating.
#
# params = ActionController::Parameters.new(user: { name: "Francesco", age: 22, role: "admin" })
# permitted = params.require(:user).permit(:name, :age)
# permitted.permitted? # => true
# permitted.has_key?(:name) # => true
# permitted.has_key?(:age) # => true
# permitted.has_key?(:role) # => false
#
# Only permitted scalars pass the filter. For example, given
#
# params.permit(:name)
#
# +:name+ passes if it is a key of +params+ whose associated value is of type
# +String+, +Symbol+, +NilClass+, +Numeric+, +TrueClass+, +FalseClass+,
# +Date+, +Time+, +DateTime+, +StringIO+, +IO+,
# +ActionDispatch::Http::UploadedFile+ or +Rack::Test::UploadedFile+.
# Otherwise, the key +:name+ is filtered out.
#
# You may declare that the parameter should be an array of permitted scalars
# by mapping it to an empty array:
#
# params = ActionController::Parameters.new(tags: ["rails", "parameters"])
# params.permit(tags: [])
#
# Sometimes it is not possible or convenient to declare the valid keys of
# a hash parameter or its internal structure. Just map to an empty hash:
#
# params.permit(preferences: {})
#
# Be careful because this opens the door to arbitrary input. In this
# case, +permit+ ensures values in the returned structure are permitted
# scalars and filters out anything else.
#
# You can also use +permit+ on nested parameters, like:
#
# params = ActionController::Parameters.new({
# person: {
# name: "Francesco",
# age: 22,
# pets: [{
# name: "Purplish",
# category: "dogs"
# }]
# }
# })
#
# permitted = params.permit(person: [ :name, { pets: :name } ])
# permitted.permitted? # => true
# permitted[:person][:name] # => "Francesco"
# permitted[:person][:age] # => nil
# permitted[:person][:pets][0][:name] # => "Purplish"
# permitted[:person][:pets][0][:category] # => nil
#
# Note that if you use +permit+ in a key that points to a hash,
# it won't allow all the hash. You also need to specify which
# attributes inside the hash should be permitted.
#
# params = ActionController::Parameters.new({
# person: {
# contact: {
# email: "none@test.com",
# phone: "555-1234"
# }
# }
# })
#
# params.require(:person).permit(:contact)
# # => <ActionController::Parameters {} permitted: true>
#
# params.require(:person).permit(contact: :phone)
# # => <ActionController::Parameters {"contact"=><ActionController::Parameters {"phone"=>"555-1234"} permitted: true>} permitted: true>
#
# params.require(:person).permit(contact: [ :email, :phone ])
# # => <ActionController::Parameters {"contact"=><ActionController::Parameters {"email"=>"none@test.com", "phone"=>"555-1234"} permitted: true>} permitted: true>
- 1
def permit(*filters)
params = self.class.new
filters.flatten.each do |filter|
case filter
when Symbol, String
permitted_scalar_filter(params, filter)
when Hash
hash_filter(params, filter)
end
end
unpermitted_parameters!(params) if self.class.action_on_unpermitted_parameters
params.permit!
end
# Returns a parameter for the given +key+. If not found,
# returns +nil+.
#
# params = ActionController::Parameters.new(person: { name: "Francesco" })
# params[:person] # => <ActionController::Parameters {"name"=>"Francesco"} permitted: false>
# params[:none] # => nil
- 1
def [](key)
convert_hashes_to_parameters(key, @parameters[key])
end
# Assigns a value to a given +key+. The given key may still get filtered out
# when +permit+ is called.
- 1
def []=(key, value)
@parameters[key] = value
end
# Returns a parameter for the given +key+. If the +key+
# can't be found, there are several options: With no other arguments,
# it will raise an <tt>ActionController::ParameterMissing</tt> error;
# if a second argument is given, then that is returned (converted to an
# instance of ActionController::Parameters if possible); if a block
# is given, then that will be run and its result returned.
#
# params = ActionController::Parameters.new(person: { name: "Francesco" })
# params.fetch(:person) # => <ActionController::Parameters {"name"=>"Francesco"} permitted: false>
# params.fetch(:none) # => ActionController::ParameterMissing: param is missing or the value is empty: none
# params.fetch(:none, {}) # => <ActionController::Parameters {} permitted: false>
# params.fetch(:none, "Francesco") # => "Francesco"
# params.fetch(:none) { "Francesco" } # => "Francesco"
- 1
def fetch(key, *args)
convert_value_to_parameters(
@parameters.fetch(key) {
if block_given?
yield
else
args.fetch(0) { raise ActionController::ParameterMissing.new(key, @parameters.keys) }
end
}
)
end
# Extracts the nested parameter from the given +keys+ by calling +dig+
# at each step. Returns +nil+ if any intermediate step is +nil+.
#
# params = ActionController::Parameters.new(foo: { bar: { baz: 1 } })
# params.dig(:foo, :bar, :baz) # => 1
# params.dig(:foo, :zot, :xyz) # => nil
#
# params2 = ActionController::Parameters.new(foo: [10, 11, 12])
# params2.dig(:foo, 1) # => 11
- 1
def dig(*keys)
convert_hashes_to_parameters(keys.first, @parameters[keys.first])
@parameters.dig(*keys)
end
# Returns a new <tt>ActionController::Parameters</tt> instance that
# includes only the given +keys+. If the given +keys+
# don't exist, returns an empty hash.
#
# params = ActionController::Parameters.new(a: 1, b: 2, c: 3)
# params.slice(:a, :b) # => <ActionController::Parameters {"a"=>1, "b"=>2} permitted: false>
# params.slice(:d) # => <ActionController::Parameters {} permitted: false>
- 1
def slice(*keys)
new_instance_with_inherited_permitted_status(@parameters.slice(*keys))
end
# Returns current <tt>ActionController::Parameters</tt> instance which
# contains only the given +keys+.
- 1
def slice!(*keys)
@parameters.slice!(*keys)
self
end
# Returns a new <tt>ActionController::Parameters</tt> instance that
# filters out the given +keys+.
#
# params = ActionController::Parameters.new(a: 1, b: 2, c: 3)
# params.except(:a, :b) # => <ActionController::Parameters {"c"=>3} permitted: false>
# params.except(:d) # => <ActionController::Parameters {"a"=>1, "b"=>2, "c"=>3} permitted: false>
- 1
def except(*keys)
new_instance_with_inherited_permitted_status(@parameters.except(*keys))
end
# Removes and returns the key/value pairs matching the given keys.
#
# params = ActionController::Parameters.new(a: 1, b: 2, c: 3)
# params.extract!(:a, :b) # => <ActionController::Parameters {"a"=>1, "b"=>2} permitted: false>
# params # => <ActionController::Parameters {"c"=>3} permitted: false>
- 1
def extract!(*keys)
new_instance_with_inherited_permitted_status(@parameters.extract!(*keys))
end
# Returns a new <tt>ActionController::Parameters</tt> with the results of
# running +block+ once for every value. The keys are unchanged.
#
# params = ActionController::Parameters.new(a: 1, b: 2, c: 3)
# params.transform_values { |x| x * 2 }
# # => <ActionController::Parameters {"a"=>2, "b"=>4, "c"=>6} permitted: false>
- 1
def transform_values
return to_enum(:transform_values) unless block_given?
new_instance_with_inherited_permitted_status(
@parameters.transform_values { |v| yield convert_value_to_parameters(v) }
)
end
# Performs values transformation and returns the altered
# <tt>ActionController::Parameters</tt> instance.
- 1
def transform_values!
return to_enum(:transform_values!) unless block_given?
@parameters.transform_values! { |v| yield convert_value_to_parameters(v) }
self
end
# Returns a new <tt>ActionController::Parameters</tt> instance with the
# results of running +block+ once for every key. The values are unchanged.
- 1
def transform_keys(&block)
return to_enum(:transform_keys) unless block_given?
new_instance_with_inherited_permitted_status(
@parameters.transform_keys(&block)
)
end
# Performs keys transformation and returns the altered
# <tt>ActionController::Parameters</tt> instance.
- 1
def transform_keys!(&block)
return to_enum(:transform_keys!) unless block_given?
@parameters.transform_keys!(&block)
self
end
# Returns a new <tt>ActionController::Parameters</tt> instance with the
# results of running +block+ once for every key. This includes the keys
# from the root hash and from all nested hashes and arrays. The values are unchanged.
- 1
def deep_transform_keys(&block)
new_instance_with_inherited_permitted_status(
@parameters.deep_transform_keys(&block)
)
end
# Returns the <tt>ActionController::Parameters</tt> instance changing its keys.
# This includes the keys from the root hash and from all nested hashes and arrays.
# The values are unchanged.
- 1
def deep_transform_keys!(&block)
@parameters.deep_transform_keys!(&block)
self
end
# Deletes a key-value pair from +Parameters+ and returns the value. If
# +key+ is not found, returns +nil+ (or, with optional code block, yields
# +key+ and returns the result). Cf. +#extract!+, which returns the
# corresponding +ActionController::Parameters+ object.
- 1
def delete(key, &block)
convert_value_to_parameters(@parameters.delete(key, &block))
end
# Returns a new instance of <tt>ActionController::Parameters</tt> with only
# items that the block evaluates to true.
- 1
def select(&block)
new_instance_with_inherited_permitted_status(@parameters.select(&block))
end
# Equivalent to Hash#keep_if, but returns +nil+ if no changes were made.
- 1
def select!(&block)
@parameters.select!(&block)
self
end
- 1
alias_method :keep_if, :select!
# Returns a new instance of <tt>ActionController::Parameters</tt> with items
# that the block evaluates to true removed.
- 1
def reject(&block)
new_instance_with_inherited_permitted_status(@parameters.reject(&block))
end
# Removes items that the block evaluates to true and returns self.
- 1
def reject!(&block)
@parameters.reject!(&block)
self
end
- 1
alias_method :delete_if, :reject!
# Returns a new instance of <tt>ActionController::Parameters</tt> with +nil+ values removed.
- 1
def compact
new_instance_with_inherited_permitted_status(@parameters.compact)
end
# Removes all +nil+ values in place and returns +self+, or +nil+ if no changes were made.
- 1
def compact!
self if @parameters.compact!
end
# Returns a new instance of <tt>ActionController::Parameters</tt> without the blank values.
# Uses Object#blank? for determining if a value is blank.
- 1
def compact_blank
reject { |_k, v| v.blank? }
end
# Removes all blank values in place and returns self.
# Uses Object#blank? for determining if a value is blank.
- 1
def compact_blank!
reject! { |_k, v| v.blank? }
end
# Returns values that were assigned to the given +keys+. Note that all the
# +Hash+ objects will be converted to <tt>ActionController::Parameters</tt>.
- 1
def values_at(*keys)
convert_value_to_parameters(@parameters.values_at(*keys))
end
# Returns a new <tt>ActionController::Parameters</tt> with all keys from
# +other_hash+ merged into current hash.
- 1
def merge(other_hash)
new_instance_with_inherited_permitted_status(
@parameters.merge(other_hash.to_h)
)
end
# Returns current <tt>ActionController::Parameters</tt> instance with
# +other_hash+ merged into current hash.
- 1
def merge!(other_hash)
@parameters.merge!(other_hash.to_h)
self
end
# Returns a new <tt>ActionController::Parameters</tt> with all keys from
# current hash merged into +other_hash+.
- 1
def reverse_merge(other_hash)
new_instance_with_inherited_permitted_status(
other_hash.to_h.merge(@parameters)
)
end
- 1
alias_method :with_defaults, :reverse_merge
# Returns current <tt>ActionController::Parameters</tt> instance with
# current hash merged into +other_hash+.
- 1
def reverse_merge!(other_hash)
@parameters.merge!(other_hash.to_h) { |key, left, right| left }
self
end
- 1
alias_method :with_defaults!, :reverse_merge!
# This is required by ActiveModel attribute assignment, so that user can
# pass +Parameters+ to a mass assignment methods in a model. It should not
# matter as we are using +HashWithIndifferentAccess+ internally.
- 1
def stringify_keys # :nodoc:
dup
end
- 1
def inspect
"#<#{self.class} #{@parameters} permitted: #{@permitted}>"
end
- 1
def self.hook_into_yaml_loading # :nodoc:
# Wire up YAML format compatibility with Rails 4.2 and Psych 2.0.8 and 2.0.9+.
# Makes the YAML parser call `init_with` when it encounters the keys below
# instead of trying its own parsing routines.
- 1
YAML.load_tags["!ruby/hash-with-ivars:ActionController::Parameters"] = name
- 1
YAML.load_tags["!ruby/hash:ActionController::Parameters"] = name
end
- 1
hook_into_yaml_loading
- 1
def init_with(coder) # :nodoc:
case coder.tag
when "!ruby/hash:ActionController::Parameters"
# YAML 2.0.8's format where hash instance variables weren't stored.
@parameters = coder.map.with_indifferent_access
@permitted = false
when "!ruby/hash-with-ivars:ActionController::Parameters"
# YAML 2.0.9's Hash subclass format where keys and values
# were stored under an elements hash and `permitted` within an ivars hash.
@parameters = coder.map["elements"].with_indifferent_access
@permitted = coder.map["ivars"][:@permitted]
when "!ruby/object:ActionController::Parameters"
# YAML's Object format. Only needed because of the format
# backwards compatibility above, otherwise equivalent to YAML's initialization.
@parameters, @permitted = coder.map["parameters"], coder.map["permitted"]
end
end
# Returns duplicate of object including all parameters.
- 1
def deep_dup
self.class.new(@parameters.deep_dup).tap do |duplicate|
duplicate.permitted = @permitted
end
end
- 1
protected
- 1
attr_reader :parameters
- 1
attr_writer :permitted
- 1
def nested_attributes?
@parameters.any? { |k, v| Parameters.nested_attribute?(k, v) }
end
- 1
def each_nested_attribute
hash = self.class.new
self.each { |k, v| hash[k] = yield v if Parameters.nested_attribute?(k, v) }
hash
end
- 1
private
- 1
def new_instance_with_inherited_permitted_status(hash)
self.class.new(hash).tap do |new_instance|
new_instance.permitted = @permitted
end
end
- 1
def convert_parameters_to_hashes(value, using)
case value
when Array
value.map { |v| convert_parameters_to_hashes(v, using) }
when Hash
value.transform_values do |v|
convert_parameters_to_hashes(v, using)
end.with_indifferent_access
when Parameters
value.send(using)
else
value
end
end
- 1
def convert_hashes_to_parameters(key, value)
converted = convert_value_to_parameters(value)
@parameters[key] = converted unless converted.equal?(value)
converted
end
- 1
def convert_value_to_parameters(value)
case value
when Array
return value if converted_arrays.member?(value)
converted = value.map { |_| convert_value_to_parameters(_) }
converted_arrays << converted
converted
when Hash
self.class.new(value)
else
value
end
end
- 1
def each_element(object, &block)
case object
when Array
object.grep(Parameters).map { |el| yield el }.compact
when Parameters
if object.nested_attributes?
object.each_nested_attribute(&block)
else
yield object
end
end
end
- 1
def unpermitted_parameters!(params)
unpermitted_keys = unpermitted_keys(params)
if unpermitted_keys.any?
case self.class.action_on_unpermitted_parameters
when :log
name = "unpermitted_parameters.action_controller"
ActiveSupport::Notifications.instrument(name, keys: unpermitted_keys)
when :raise
raise ActionController::UnpermittedParameters.new(unpermitted_keys)
end
end
end
- 1
def unpermitted_keys(params)
keys - params.keys - always_permitted_parameters
end
#
# --- Filtering ----------------------------------------------------------
#
# This is a list of permitted scalar types that includes the ones
# supported in XML and JSON requests.
#
# This list is in particular used to filter ordinary requests, String goes
# as first element to quickly short-circuit the common case.
#
# If you modify this collection please update the API of +permit+ above.
- 1
PERMITTED_SCALAR_TYPES = [
String,
Symbol,
NilClass,
Numeric,
TrueClass,
FalseClass,
Date,
Time,
# DateTimes are Dates, we document the type but avoid the redundant check.
StringIO,
IO,
ActionDispatch::Http::UploadedFile,
Rack::Test::UploadedFile,
]
- 1
def permitted_scalar?(value)
PERMITTED_SCALAR_TYPES.any? { |type| value.is_a?(type) }
end
# Adds existing keys to the params if their values are scalar.
#
# For example:
#
# puts self.keys #=> ["zipcode(90210i)"]
# params = {}
#
# permitted_scalar_filter(params, "zipcode")
#
# puts params.keys # => ["zipcode"]
- 1
def permitted_scalar_filter(params, permitted_key)
permitted_key = permitted_key.to_s
if has_key?(permitted_key) && permitted_scalar?(self[permitted_key])
params[permitted_key] = self[permitted_key]
end
each_key do |key|
next unless key =~ /\(\d+[if]?\)\z/
next unless $~.pre_match == permitted_key
params[key] = self[key] if permitted_scalar?(self[key])
end
end
- 1
def array_of_permitted_scalars?(value)
if value.is_a?(Array) && value.all? { |element| permitted_scalar?(element) }
yield value
end
end
- 1
def non_scalar?(value)
value.is_a?(Array) || value.is_a?(Parameters)
end
- 1
EMPTY_ARRAY = []
- 1
EMPTY_HASH = {}
- 1
def hash_filter(params, filter)
filter = filter.with_indifferent_access
# Slicing filters out non-declared keys.
slice(*filter.keys).each do |key, value|
next unless value
next unless has_key? key
if filter[key] == EMPTY_ARRAY
# Declaration { comment_ids: [] }.
array_of_permitted_scalars?(self[key]) do |val|
params[key] = val
end
elsif filter[key] == EMPTY_HASH
# Declaration { preferences: {} }.
if value.is_a?(Parameters)
params[key] = permit_any_in_parameters(value)
end
elsif non_scalar?(value)
# Declaration { user: :name } or { user: [:name, :age, { address: ... }] }.
params[key] = each_element(value) do |element|
element.permit(*Array.wrap(filter[key]))
end
end
end
end
- 1
def permit_any_in_parameters(params)
self.class.new.tap do |sanitized|
params.each do |key, value|
case value
when ->(v) { permitted_scalar?(v) }
sanitized[key] = value
when Array
sanitized[key] = permit_any_in_array(value)
when Parameters
sanitized[key] = permit_any_in_parameters(value)
else
# Filter this one out.
end
end
end
end
- 1
def permit_any_in_array(array)
[].tap do |sanitized|
array.each do |element|
case element
when ->(e) { permitted_scalar?(e) }
sanitized << element
when Parameters
sanitized << permit_any_in_parameters(element)
else
# Filter this one out.
end
end
end
end
- 1
def initialize_copy(source)
super
@parameters = @parameters.dup
end
end
# == Strong \Parameters
#
# It provides an interface for protecting attributes from end-user
# assignment. This makes Action Controller parameters forbidden
# to be used in Active Model mass assignment until they have been explicitly
# enumerated.
#
# In addition, parameters can be marked as required and flow through a
# predefined raise/rescue flow to end up as a <tt>400 Bad Request</tt> with no
# effort.
#
# class PeopleController < ActionController::Base
# # Using "Person.create(params[:person])" would raise an
# # ActiveModel::ForbiddenAttributesError exception because it'd
# # be using mass assignment without an explicit permit step.
# # This is the recommended form:
# def create
# Person.create(person_params)
# end
#
# # This will pass with flying colors as long as there's a person key in the
# # parameters, otherwise it'll raise an ActionController::ParameterMissing
# # exception, which will get caught by ActionController::Base and turned
# # into a 400 Bad Request reply.
# def update
# redirect_to current_account.people.find(params[:id]).tap { |person|
# person.update!(person_params)
# }
# end
#
# private
# # Using a private method to encapsulate the permissible parameters is
# # a good pattern since you'll be able to reuse the same permit
# # list between create and update. Also, you can specialize this method
# # with per-user checking of permissible attributes.
# def person_params
# params.require(:person).permit(:name, :age)
# end
# end
#
# In order to use <tt>accepts_nested_attributes_for</tt> with Strong \Parameters, you
# will need to specify which nested attributes should be permitted. You might want
# to allow +:id+ and +:_destroy+, see ActiveRecord::NestedAttributes for more information.
#
# class Person
# has_many :pets
# accepts_nested_attributes_for :pets
# end
#
# class PeopleController < ActionController::Base
# def create
# Person.create(person_params)
# end
#
# ...
#
# private
#
# def person_params
# # It's mandatory to specify the nested attributes that should be permitted.
# # If you use `permit` with just the key that points to the nested attributes hash,
# # it will return an empty hash.
# params.require(:person).permit(:name, :age, pets_attributes: [ :id, :name, :category ])
# end
# end
#
# See ActionController::Parameters.require and ActionController::Parameters.permit
# for more information.
- 1
module StrongParameters
# Returns a new ActionController::Parameters object that
# has been instantiated with the <tt>request.parameters</tt>.
- 1
def params
@_params ||= Parameters.new(request.parameters)
end
# Assigns the given +value+ to the +params+ hash. If +value+
# is a Hash, this will create an ActionController::Parameters
# object that has been instantiated with the given +value+ hash.
- 1
def params=(value)
@_params = value.is_a?(Hash) ? Parameters.new(value) : value
end
end
end
# frozen_string_literal: true
- 1
module ActionController
- 1
module Testing
- 1
extend ActiveSupport::Concern
# Behavior specific to functional tests
- 1
module Functional # :nodoc:
- 1
def recycle!
@_url_options = nil
self.formats = nil
self.params = nil
end
end
end
end
# frozen_string_literal: true
- 1
module ActionController
# Includes +url_for+ into the host class. The class has to provide a +RouteSet+ by implementing
# the <tt>_routes</tt> method. Otherwise, an exception will be raised.
#
# In addition to <tt>AbstractController::UrlFor</tt>, this module accesses the HTTP layer to define
# URL options like the +host+. In order to do so, this module requires the host class
# to implement +env+ which needs to be Rack-compatible and +request+
# which is either an instance of +ActionDispatch::Request+ or an object
# that responds to the +host+, +optional_port+, +protocol+ and
# +symbolized_path_parameter+ methods.
#
# class RootUrl
# include ActionController::UrlFor
# include Rails.application.routes.url_helpers
#
# delegate :env, :request, to: :controller
#
# def initialize(controller)
# @controller = controller
# @url = root_path # named route from the application.
# end
# end
- 1
module UrlFor
- 1
extend ActiveSupport::Concern
- 1
include AbstractController::UrlFor
- 1
def url_options
@_url_options ||= {
host: request.host,
port: request.optional_port,
protocol: request.protocol,
_recall: request.path_parameters
}.merge!(super).freeze
if (same_origin = _routes.equal?(request.routes)) ||
(script_name = request.engine_script_name(_routes)) ||
(original_script_name = request.original_script_name)
options = @_url_options.dup
if original_script_name
options[:original_script_name] = original_script_name
else
if same_origin
options[:script_name] = request.script_name.empty? ? "" : request.script_name.dup
else
options[:script_name] = script_name
end
end
options.freeze
else
@_url_options
end
end
end
end
# frozen_string_literal: true
require "rails"
require "action_controller"
require "action_dispatch/railtie"
require "abstract_controller/railties/routes_helpers"
require "action_controller/railties/helpers"
require "action_view/railtie"
module ActionController
class Railtie < Rails::Railtie #:nodoc:
config.action_controller = ActiveSupport::OrderedOptions.new
config.eager_load_namespaces << ActionController
initializer "action_controller.assets_config", group: :all do |app|
app.config.action_controller.assets_dir ||= app.config.paths["public"].first
end
initializer "action_controller.set_helpers_path" do |app|
ActionController::Helpers.helpers_path = app.helpers_paths
end
initializer "action_controller.parameters_config" do |app|
options = app.config.action_controller
ActiveSupport.on_load(:action_controller, run_once: true) do
ActionController::Parameters.permit_all_parameters = options.delete(:permit_all_parameters) { false }
if app.config.action_controller[:always_permitted_parameters]
ActionController::Parameters.always_permitted_parameters =
app.config.action_controller.delete(:always_permitted_parameters)
end
ActionController::Parameters.action_on_unpermitted_parameters = options.delete(:action_on_unpermitted_parameters) do
(Rails.env.test? || Rails.env.development?) ? :log : false
end
end
end
initializer "action_controller.set_configs" do |app|
paths = app.config.paths
options = app.config.action_controller
options.logger ||= Rails.logger
options.cache_store ||= Rails.cache
options.javascripts_dir ||= paths["public/javascripts"].first
options.stylesheets_dir ||= paths["public/stylesheets"].first
# Ensure readers methods get compiled.
options.asset_host ||= app.config.asset_host
options.relative_url_root ||= app.config.relative_url_root
ActiveSupport.on_load(:action_controller) do
include app.routes.mounted_helpers
extend ::AbstractController::Railties::RoutesHelpers.with(app.routes)
extend ::ActionController::Railties::Helpers
options.each do |k, v|
k = "#{k}="
if respond_to?(k)
send(k, v)
elsif !Base.respond_to?(k)
raise "Invalid option key: #{k}"
end
end
end
end
initializer "action_controller.compile_config_methods" do
ActiveSupport.on_load(:action_controller) do
config.compile_methods! if config.respond_to?(:compile_methods!)
end
end
initializer "action_controller.request_forgery_protection" do |app|
ActiveSupport.on_load(:action_controller_base) do
if app.config.action_controller.default_protect_from_forgery
protect_from_forgery with: :exception
end
end
end
initializer "action_controller.eager_load_actions" do
ActiveSupport.on_load(:after_initialize) do
ActionController::Metal.descendants.each(&:action_methods) if config.eager_load
end
end
end
end
# frozen_string_literal: true
module ActionController
module Railties
module Helpers
def inherited(klass)
super
return unless klass.respond_to?(:helpers_path=)
if namespace = klass.module_parents.detect { |m| m.respond_to?(:railtie_helpers_paths) }
paths = namespace.railtie_helpers_paths
else
paths = ActionController::Helpers.helpers_path
end
klass.helpers_path = paths
if klass.superclass == ActionController::Base && ActionController::Base.include_all_helpers
klass.helper :all
end
end
end
end
end
# frozen_string_literal: true
- 1
module ActionController
# ActionController::Renderer allows you to render arbitrary templates
# without requirement of being in controller actions.
#
# You get a concrete renderer class by invoking ActionController::Base#renderer.
# For example:
#
# ApplicationController.renderer
#
# It allows you to call method #render directly.
#
# ApplicationController.renderer.render template: '...'
#
# You can use this shortcut in a controller, instead of the previous example:
#
# ApplicationController.render template: '...'
#
# #render allows you to use the same options that you can use when rendering in a controller.
# For example:
#
# FooController.render :action, locals: { ... }, assigns: { ... }
#
# The template will be rendered in a Rack environment which is accessible through
# ActionController::Renderer#env. You can set it up in two ways:
#
# * by changing renderer defaults, like
#
# ApplicationController.renderer.defaults # => hash with default Rack environment
#
# * by initializing an instance of renderer by passing it a custom environment.
#
# ApplicationController.renderer.new(method: 'post', https: true)
#
- 1
class Renderer
- 1
attr_reader :defaults, :controller
- 1
DEFAULTS = {
http_host: "example.org",
https: false,
method: "get",
script_name: "",
input: ""
}.freeze
# Create a new renderer instance for a specific controller class.
- 1
def self.for(controller, env = {}, defaults = DEFAULTS.dup)
- 284
new(controller, env, defaults)
end
# Create a new renderer for the same controller but with a new env.
- 1
def new(env = {})
self.class.new controller, env, defaults
end
# Create a new renderer for the same controller but with new defaults.
- 1
def with_defaults(defaults)
self.class.new controller, @env, self.defaults.merge(defaults)
end
# Accepts a custom Rack environment to render templates in.
# It will be merged with the default Rack environment defined by
# +ActionController::Renderer::DEFAULTS+.
- 1
def initialize(controller, env, defaults)
- 284
@controller = controller
- 284
@defaults = defaults
- 284
@env = normalize_keys defaults, env
end
# Render templates with any options from ActionController::Base#render_to_string.
#
# The primary options are:
# * <tt>:partial</tt> - See <tt>ActionView::PartialRenderer</tt> for details.
# * <tt>:file</tt> - Renders an explicit template file. Add <tt>:locals</tt> to pass in, if so desired.
# It shouldn’t be used directly with unsanitized user input due to lack of validation.
# * <tt>:inline</tt> - Renders an ERB template string.
# * <tt>:plain</tt> - Renders provided text and sets the content type as <tt>text/plain</tt>.
# * <tt>:html</tt> - Renders the provided HTML safe string, otherwise
# performs HTML escape on the string first. Sets the content type as <tt>text/html</tt>.
# * <tt>:json</tt> - Renders the provided hash or object in JSON. You don't
# need to call <tt>.to_json</tt> on the object you want to render.
# * <tt>:body</tt> - Renders provided text and sets content type of <tt>text/plain</tt>.
#
# If no <tt>options</tt> hash is passed or if <tt>:update</tt> is specified, then:
#
# If an object responding to `render_in` is passed, `render_in` is called on the object,
# passing in the current view context.
#
# Otherwise, a partial is rendered using the second parameter as the locals hash.
- 1
def render(*args)
raise "missing controller" unless controller
request = ActionDispatch::Request.new @env
request.routes = controller._routes
instance = controller.new
instance.set_request! request
instance.set_response! controller.make_response!(request)
instance.render_to_string(*args)
end
- 1
private
- 1
def normalize_keys(defaults, env)
- 284
new_env = {}
- 284
env.each_pair { |k, v| new_env[rack_key_for(k)] = rack_value_for(k, v) }
- 284
defaults.each_pair do |k, v|
- 1420
key = rack_key_for(k)
- 1420
new_env[key] = rack_value_for(k, v) unless new_env.key?(key)
end
- 284
new_env["rack.url_scheme"] = new_env["HTTPS"] == "on" ? "https" : "http"
- 284
new_env
end
- 1
RACK_KEY_TRANSLATION = {
http_host: "HTTP_HOST",
https: "HTTPS",
method: "REQUEST_METHOD",
script_name: "SCRIPT_NAME",
input: "rack.input"
}
- 1
def rack_key_for(key)
- 1420
RACK_KEY_TRANSLATION[key] || key.to_s
end
- 1
def rack_value_for(key, value)
- 1420
case key
when :https
- 284
value ? "on" : "off"
when :method
- 284
-value.upcase
else
- 852
value
end
end
end
end
# frozen_string_literal: true
- 1
module ActionController
- 1
module TemplateAssertions # :nodoc:
- 1
def assert_template(options = {}, message = nil)
raise NoMethodError,
"assert_template has been extracted to a gem. To continue using it,
add `gem 'rails-controller-testing'` to your Gemfile."
end
end
end
# frozen_string_literal: true
- 1
require "rack/session/abstract/id"
- 1
require "active_support/core_ext/hash/conversions"
- 1
require "active_support/core_ext/object/to_query"
- 1
require "active_support/core_ext/module/anonymous"
- 1
require "active_support/core_ext/module/redefine_method"
- 1
require "active_support/core_ext/hash/keys"
- 1
require "active_support/testing/constant_lookup"
- 1
require "action_controller/template_assertions"
- 1
require "rails-dom-testing"
- 1
module ActionController
- 1
class Metal
- 1
include Testing::Functional
end
- 1
module Live
# Disable controller / rendering threads in tests. User tests can access
# the database on the main thread, so they could open a txn, then the
# controller thread will open a new connection and try to access data
# that's only visible to the main thread's txn. This is the problem in #23483.
- 1
silence_redefinition_of_method :new_controller_thread
- 1
def new_controller_thread # :nodoc:
yield
end
end
# ActionController::TestCase will be deprecated and moved to a gem in the future.
# Please use ActionDispatch::IntegrationTest going forward.
- 1
class TestRequest < ActionDispatch::TestRequest #:nodoc:
- 1
DEFAULT_ENV = ActionDispatch::TestRequest::DEFAULT_ENV.dup
- 1
DEFAULT_ENV.delete "PATH_INFO"
- 1
def self.new_session
TestSession.new
end
- 1
attr_reader :controller_class
# Create a new test request with default `env` values.
- 1
def self.create(controller_class)
env = {}
env = Rails.application.env_config.merge(env) if defined?(Rails.application) && Rails.application
env["rack.request.cookie_hash"] = {}.with_indifferent_access
new(default_env.merge(env), new_session, controller_class)
end
- 1
def self.default_env
DEFAULT_ENV
end
- 1
private_class_method :default_env
- 1
def initialize(env, session, controller_class)
super(env)
self.session = session
self.session_options = TestSession::DEFAULT_OPTIONS.dup
@controller_class = controller_class
@custom_param_parsers = {
xml: lambda { |raw_post| Hash.from_xml(raw_post)["hash"] }
}
end
- 1
def query_string=(string)
set_header Rack::QUERY_STRING, string
end
- 1
def content_type=(type)
set_header "CONTENT_TYPE", type
end
- 1
def assign_parameters(routes, controller_path, action, parameters, generated_path, query_string_keys)
non_path_parameters = {}
path_parameters = {}
if parameters[:format] == :json
parameters = JSON.load(JSON.dump(parameters))
query_string_keys = query_string_keys.map(&:to_s)
end
parameters.each do |key, value|
if query_string_keys.include?(key)
non_path_parameters[key] = value
else
unless parameters["format"] == "json"
if value.is_a?(Array)
value = value.map(&:to_param)
else
value = value.to_param
end
end
path_parameters[key.to_sym] = value
end
end
if get?
if query_string.blank?
self.query_string = non_path_parameters.to_query
end
else
if ENCODER.should_multipart?(non_path_parameters)
self.content_type = ENCODER.content_type
data = ENCODER.build_multipart non_path_parameters
else
fetch_header("CONTENT_TYPE") do |k|
set_header k, "application/x-www-form-urlencoded"
end
case content_mime_type.to_sym
when nil
raise "Unknown Content-Type: #{content_type}"
when :json
data = ActiveSupport::JSON.encode(non_path_parameters)
when :xml
data = non_path_parameters.to_xml
when :url_encoded_form
data = non_path_parameters.to_query
else
@custom_param_parsers[content_mime_type.symbol] = ->(_) { non_path_parameters }
data = non_path_parameters.to_query
end
end
data_stream = StringIO.new(data)
set_header "CONTENT_LENGTH", data_stream.length.to_s
set_header "rack.input", data_stream
end
fetch_header("PATH_INFO") do |k|
set_header k, generated_path
end
path_parameters[:controller] = controller_path
path_parameters[:action] = action
self.path_parameters = path_parameters
end
- 1
ENCODER = Class.new do
- 1
include Rack::Test::Utils
- 1
def should_multipart?(params)
# FIXME: lifted from Rack-Test. We should push this separation upstream.
multipart = false
query = lambda { |value|
case value
when Array
value.each(&query)
when Hash
value.values.each(&query)
when Rack::Test::UploadedFile
multipart = true
end
}
params.values.each(&query)
multipart
end
- 1
public :build_multipart
- 1
def content_type
"multipart/form-data; boundary=#{Rack::Test::MULTIPART_BOUNDARY}"
end
end.new
- 1
private
- 1
def params_parsers
super.merge @custom_param_parsers
end
end
- 1
class LiveTestResponse < Live::Response
# Was the response successful?
- 1
alias_method :success?, :successful?
# Was the URL not found?
- 1
alias_method :missing?, :not_found?
# Was there a server-side error?
- 1
alias_method :error?, :server_error?
end
# Methods #destroy and #load! are overridden to avoid calling methods on the
# @store object, which does not exist for the TestSession class.
- 1
class TestSession < Rack::Session::Abstract::PersistedSecure::SecureSessionHash #:nodoc:
- 1
DEFAULT_OPTIONS = Rack::Session::Abstract::Persisted::DEFAULT_OPTIONS
- 1
def initialize(session = {})
super(nil, nil)
@id = Rack::Session::SessionId.new(SecureRandom.hex(16))
@data = stringify_keys(session)
@loaded = true
end
- 1
def exists?
true
end
- 1
def keys
@data.keys
end
- 1
def values
@data.values
end
- 1
def destroy
clear
end
- 1
def dig(*keys)
keys = keys.map.with_index { |key, i| i.zero? ? key.to_s : key }
@data.dig(*keys)
end
- 1
def fetch(key, *args, &block)
@data.fetch(key.to_s, *args, &block)
end
- 1
private
- 1
def load!
@id
end
end
# Superclass for ActionController functional tests. Functional tests allow you to
# test a single controller action per test method.
#
# == Use integration style controller tests over functional style controller tests.
#
# Rails discourages the use of functional tests in favor of integration tests
# (use ActionDispatch::IntegrationTest).
#
# New Rails applications no longer generate functional style controller tests and they should
# only be used for backward compatibility. Integration style controller tests perform actual
# requests, whereas functional style controller tests merely simulate a request. Besides,
# integration tests are as fast as functional tests and provide lot of helpers such as +as+,
# +parsed_body+ for effective testing of controller actions including even API endpoints.
#
# == Basic example
#
# Functional tests are written as follows:
# 1. First, one uses the +get+, +post+, +patch+, +put+, +delete+ or +head+ method to simulate
# an HTTP request.
# 2. Then, one asserts whether the current state is as expected. "State" can be anything:
# the controller's HTTP response, the database contents, etc.
#
# For example:
#
# class BooksControllerTest < ActionController::TestCase
# def test_create
# # Simulate a POST response with the given HTTP parameters.
# post(:create, params: { book: { title: "Love Hina" }})
#
# # Asserts that the controller tried to redirect us to
# # the created book's URI.
# assert_response :found
#
# # Asserts that the controller really put the book in the database.
# assert_not_nil Book.find_by(title: "Love Hina")
# end
# end
#
# You can also send a real document in the simulated HTTP request.
#
# def test_create
# json = {book: { title: "Love Hina" }}.to_json
# post :create, body: json
# end
#
# == Special instance variables
#
# ActionController::TestCase will also automatically provide the following instance
# variables for use in the tests:
#
# <b>@controller</b>::
# The controller instance that will be tested.
# <b>@request</b>::
# An ActionController::TestRequest, representing the current HTTP
# request. 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.
# <b>@response</b>::
# An ActionDispatch::TestResponse object, representing the response
# of the last HTTP response. In the above example, <tt>@response</tt> becomes valid
# after 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
#
# ActionController::TestCase will automatically infer the controller under 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 SpecialEdgeCaseWidgetsControllerTest < ActionController::TestCase
# tests WidgetController
# end
#
# == \Testing controller internals
#
# In addition to these specific assertions, you also have easy access to various 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.
#
# 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 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>.
#
# For redirects within the same controller, you can even call follow_redirect and the redirect will be followed, triggering another
# action call which can then be asserted against.
#
# == Manipulating session and cookie variables
#
# Sometimes you need to set up the session and cookie variables for a test.
# To do this just assign a value to the session or cookie collection:
#
# session[:key] = "value"
# cookies[:key] = "value"
#
# To clear the cookies for a test just clear the cookie collection:
#
# cookies.clear
#
# == \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.
#
# assert_redirected_to page_url(title: 'foo')
- 1
class TestCase < ActiveSupport::TestCase
- 1
module Behavior
- 1
extend ActiveSupport::Concern
- 1
include ActionDispatch::TestProcess
- 1
include ActiveSupport::Testing::ConstantLookup
- 1
include Rails::Dom::Testing::Assertions
- 1
attr_reader :response, :request
- 1
module ClassMethods
# Sets the controller class name. Useful if the name can't be inferred from test class.
# Normalizes +controller_class+ before using.
#
# tests WidgetController
# tests :widget
# tests 'widget'
- 1
def tests(controller_class)
- 58
case controller_class
when String, Symbol
- 2
self.controller_class = "#{controller_class.to_s.camelize}Controller".constantize
when Class
- 56
self.controller_class = controller_class
else
raise ArgumentError, "controller class must be a String, Symbol, or Class"
end
end
- 1
def controller_class=(new_class)
- 58
self._controller_class = new_class
end
- 1
def controller_class
if current_controller_class = _controller_class
current_controller_class
else
self.controller_class = determine_default_controller_class(name)
end
end
- 1
def determine_default_controller_class(name)
determine_constant_from_test_name(name) do |constant|
Class === constant && constant < ActionController::Metal
end
end
end
# Simulate a GET request with the given parameters.
#
# - +action+: The controller action to call.
# - +params+: The hash with HTTP parameters that you want to pass. This may be +nil+.
# - +body+: The request body with a string that is appropriately encoded
# (<tt>application/x-www-form-urlencoded</tt> or <tt>multipart/form-data</tt>).
# - +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,
# 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
# available to make the tests more expressive.
- 1
def get(action, **args)
res = process(action, method: "GET", **args)
cookies.update res.cookies
res
end
# Simulate a POST request with the given parameters and set/volley the response.
# See +get+ for more details.
- 1
def post(action, **args)
process(action, method: "POST", **args)
end
# Simulate a PATCH request with the given parameters and set/volley the response.
# See +get+ for more details.
- 1
def patch(action, **args)
process(action, method: "PATCH", **args)
end
# Simulate a PUT request with the given parameters and set/volley the response.
# See +get+ for more details.
- 1
def put(action, **args)
process(action, method: "PUT", **args)
end
# Simulate a DELETE request with the given parameters and set/volley the response.
# See +get+ for more details.
- 1
def delete(action, **args)
process(action, method: "DELETE", **args)
end
# Simulate a HEAD request with the given parameters and set/volley the response.
# See +get+ for more details.
- 1
def head(action, **args)
process(action, method: "HEAD", **args)
end
# Simulate an HTTP request to +action+ by specifying request method,
# parameters and set/volley the response.
#
# - +action+: The controller action to call.
# - +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.
# - +params+: The hash with HTTP parameters that you want to pass. This may be +nil+.
# - +body+: The request body with a string that is appropriately encoded
# (<tt>application/x-www-form-urlencoded</tt> or <tt>multipart/form-data</tt>).
# - +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+.
# - +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,
# 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
# prefer using #get, #post, #patch, #put, #delete and #head methods
# respectively which will make tests more expressive.
#
# Note that the request method is not verified.
- 1
def process(action, method: "GET", params: nil, session: nil, body: nil, flash: {}, format: nil, xhr: false, as: nil)
check_required_ivars
action = +action.to_s
http_method = method.to_s.upcase
@html_document = nil
cookies.update(@request.cookies)
cookies.update_cookies_from_jar
@request.set_header "HTTP_COOKIE", cookies.to_header
@request.delete_header "action_dispatch.cookies"
@request = TestRequest.new scrub_env!(@request.env), @request.session, @controller.class
@response = build_response @response_klass
@response.request = @request
@controller.recycle!
if body
@request.set_header "RAW_POST_DATA", body
end
@request.set_header "REQUEST_METHOD", http_method
if as
@request.content_type = Mime[as].to_s
format ||= as
end
parameters = (params || {}).symbolize_keys
if format
parameters[:format] = format
end
setup_request(controller_class_name, action, parameters, session, flash, xhr)
process_controller_response(action, cookies, xhr)
end
- 1
def controller_class_name
@controller.class.anonymous? ? "anonymous" : @controller.class.controller_path
end
- 1
def generated_path(generated_extras)
generated_extras[0]
end
- 1
def query_parameter_names(generated_extras)
generated_extras[1] + [:controller, :action]
end
- 1
def setup_controller_request_and_response
@controller = nil unless defined? @controller
@response_klass = ActionDispatch::TestResponse
if klass = self.class.controller_class
if klass < ActionController::Live
@response_klass = LiveTestResponse
end
unless @controller
begin
@controller = klass.new
rescue
warn "could not construct controller #{klass}" if $VERBOSE
end
end
end
@request = TestRequest.create(@controller.class)
@response = build_response @response_klass
@response.request = @request
if @controller
@controller.request = @request
@controller.params = {}
end
end
- 1
def build_response(klass)
klass.create
end
- 1
included do
- 1
include ActionController::TemplateAssertions
- 1
include ActionDispatch::Assertions
- 1
class_attribute :_controller_class
- 1
setup :setup_controller_request_and_response
- 1
ActiveSupport.run_load_hooks(:action_controller_test_case, self)
end
- 1
private
- 1
def setup_request(controller_class_name, action, parameters, session, flash, xhr)
generated_extras = @routes.generate_extras(parameters.merge(controller: controller_class_name, action: action))
generated_path = generated_path(generated_extras)
query_string_keys = query_parameter_names(generated_extras)
@request.assign_parameters(@routes, controller_class_name, action, parameters, generated_path, query_string_keys)
@request.session.update(session) if session
@request.flash.update(flash || {})
if xhr
@request.set_header "HTTP_X_REQUESTED_WITH", "XMLHttpRequest"
@request.fetch_header("HTTP_ACCEPT") do |k|
@request.set_header k, [Mime[:js], Mime[:html], Mime[:xml], "text/xml", "*/*"].join(", ")
end
end
@request.fetch_header("SCRIPT_NAME") do |k|
@request.set_header k, @controller.config.relative_url_root
end
end
- 1
def process_controller_response(action, cookies, xhr)
begin
@controller.recycle!
@controller.dispatch(action, @request, @response)
ensure
@request = @controller.request
@response = @controller.response
if @request.have_cookie_jar?
unless @request.cookie_jar.committed?
@request.cookie_jar.write(@response)
cookies.update(@request.cookie_jar.instance_variable_get(:@cookies))
end
end
@response.prepare!
if flash_value = @request.flash.to_session_value
@request.session["flash"] = flash_value
else
@request.session.delete("flash")
end
if xhr
@request.delete_header "HTTP_X_REQUESTED_WITH"
@request.delete_header "HTTP_ACCEPT"
end
@request.query_string = ""
@response.sent!
end
@response
end
- 1
def scrub_env!(env)
env.delete_if do |k, _|
k.start_with?("rack.request", "action_dispatch.request", "action_dispatch.rescue")
end
env["rack.input"] = StringIO.new
env.delete "CONTENT_LENGTH"
env.delete "RAW_POST_DATA"
env
end
- 1
def document_root_element
html_document.root
end
- 1
def check_required_ivars
# Sanity check for required instance variables so we can give an
# understandable error message.
[:@routes, :@controller, :@request, :@response].each do |iv_name|
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."
end
end
end
end
- 1
include Behavior
end
end
# frozen_string_literal: true
#--
# Copyright (c) 2004-2020 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++
- 1
require "active_support"
- 1
require "active_support/rails"
- 1
require "active_support/core_ext/module/attribute_accessors"
- 1
require "action_pack"
- 1
require "rack"
- 1
module Rack
- 1
autoload :Test, "rack/test"
end
- 1
module ActionDispatch
- 1
extend ActiveSupport::Autoload
- 1
class IllegalStateError < StandardError
end
- 1
class MissingController < NameError
end
- 1
eager_autoload do
- 1
autoload_under "http" do
- 1
autoload :ContentSecurityPolicy
- 1
autoload :FeaturePolicy
- 1
autoload :Request
- 1
autoload :Response
end
end
- 1
autoload_under "middleware" do
- 1
autoload :HostAuthorization
- 1
autoload :RequestId
- 1
autoload :Callbacks
- 1
autoload :Cookies
- 1
autoload :ActionableExceptions
- 1
autoload :DebugExceptions
- 1
autoload :DebugLocks
- 1
autoload :DebugView
- 1
autoload :ExceptionWrapper
- 1
autoload :Executor
- 1
autoload :Flash
- 1
autoload :PublicExceptions
- 1
autoload :Reloader
- 1
autoload :RemoteIp
- 1
autoload :ShowExceptions
- 1
autoload :SSL
- 1
autoload :Static
end
- 1
autoload :Journey
- 1
autoload :MiddlewareStack, "action_dispatch/middleware/stack"
- 1
autoload :Routing
- 1
module Http
- 1
extend ActiveSupport::Autoload
- 1
autoload :Cache
- 1
autoload :Headers
- 1
autoload :MimeNegotiation
- 1
autoload :Parameters
- 1
autoload :ParameterFilter
- 1
autoload :UploadedFile, "action_dispatch/http/upload"
- 1
autoload :URL
end
- 1
module Session
- 1
autoload :AbstractStore, "action_dispatch/middleware/session/abstract_store"
- 1
autoload :AbstractSecureStore, "action_dispatch/middleware/session/abstract_store"
- 1
autoload :CookieStore, "action_dispatch/middleware/session/cookie_store"
- 1
autoload :MemCacheStore, "action_dispatch/middleware/session/mem_cache_store"
- 1
autoload :CacheStore, "action_dispatch/middleware/session/cache_store"
end
- 1
mattr_accessor :test_app
- 1
autoload_under "testing" do
- 1
autoload :Assertions
- 1
autoload :Integration
- 1
autoload :IntegrationTest, "action_dispatch/testing/integration"
- 1
autoload :TestProcess
- 1
autoload :TestRequest
- 1
autoload :TestResponse
- 1
autoload :AssertionResponse
end
- 1
autoload :SystemTestCase, "action_dispatch/system_test_case"
end
- 1
autoload :Mime, "action_dispatch/http/mime_type"
- 1
ActiveSupport.on_load(:action_view) do
ActionView::Base.default_formats ||= Mime::SET.symbols
ActionView::Template::Types.delegate_to Mime
ActionView::LookupContext::DetailsKey.clear
end
# frozen_string_literal: true
- 1
module ActionDispatch
- 1
module Http
- 1
module Cache
- 1
module Request
- 1
HTTP_IF_MODIFIED_SINCE = "HTTP_IF_MODIFIED_SINCE"
- 1
HTTP_IF_NONE_MATCH = "HTTP_IF_NONE_MATCH"
- 1
def if_modified_since
if since = get_header(HTTP_IF_MODIFIED_SINCE)
Time.rfc2822(since) rescue nil
end
end
- 1
def if_none_match
get_header HTTP_IF_NONE_MATCH
end
- 1
def if_none_match_etags
if_none_match ? if_none_match.split(/\s*,\s*/) : []
end
- 1
def not_modified?(modified_at)
if_modified_since && modified_at && if_modified_since >= modified_at
end
- 1
def etag_matches?(etag)
if etag
validators = if_none_match_etags
validators.include?(etag) || validators.include?("*")
end
end
# Check response freshness (Last-Modified and ETag) against request
# If-Modified-Since and If-None-Match conditions. If both headers are
# supplied, both must match, or the request is not considered fresh.
- 1
def fresh?(response)
last_modified = if_modified_since
etag = if_none_match
return false unless last_modified || etag
success = true
success &&= not_modified?(response.last_modified) if last_modified
success &&= etag_matches?(response.etag) if etag
success
end
end
- 1
module Response
- 1
attr_reader :cache_control
- 1
def last_modified
if last = get_header(LAST_MODIFIED)
Time.httpdate(last)
end
end
- 1
def last_modified?
has_header? LAST_MODIFIED
end
- 1
def last_modified=(utc_time)
set_header LAST_MODIFIED, utc_time.httpdate
end
- 1
def date
if date_header = get_header(DATE)
Time.httpdate(date_header)
end
end
- 1
def date?
has_header? DATE
end
- 1
def date=(utc_time)
set_header DATE, utc_time.httpdate
end
# This method sets a weak ETag validator on the response so browsers
# and proxies may cache the response, keyed on the ETag. On subsequent
# requests, the If-None-Match header is set to the cached ETag. If it
# matches the current ETag, we can return a 304 Not Modified response
# with no body, letting the browser or proxy know that their cache is
# current. Big savings in request time and network bandwidth.
#
# Weak ETags are considered to be semantically equivalent but not
# byte-for-byte identical. This is perfect for browser caching of HTML
# pages where we don't care about exact equality, just what the user
# is viewing.
#
# Strong ETags are considered byte-for-byte identical. They allow a
# browser or proxy cache to support Range requests, useful for paging
# through a PDF file or scrubbing through a video. Some CDNs only
# support strong ETags and will ignore weak ETags entirely.
#
# Weak ETags are what we almost always need, so they're the default.
# Check out #strong_etag= to provide a strong ETag validator.
- 1
def etag=(weak_validators)
self.weak_etag = weak_validators
end
- 1
def weak_etag=(weak_validators)
set_header "ETag", generate_weak_etag(weak_validators)
end
- 1
def strong_etag=(strong_validators)
set_header "ETag", generate_strong_etag(strong_validators)
end
- 1
def etag?; etag; end
# True if an ETag is set and it's a weak validator (preceded with W/)
- 1
def weak_etag?
etag? && etag.start_with?('W/"')
end
# True if an ETag is set and it isn't a weak validator (not preceded with W/)
- 1
def strong_etag?
etag? && !weak_etag?
end
- 1
private
- 1
DATE = "Date"
- 1
LAST_MODIFIED = "Last-Modified"
- 1
SPECIAL_KEYS = Set.new(%w[extras no-cache max-age public private must-revalidate])
- 1
def generate_weak_etag(validators)
"W/#{generate_strong_etag(validators)}"
end
- 1
def generate_strong_etag(validators)
%("#{ActiveSupport::Digest.hexdigest(ActiveSupport::Cache.expand_cache_key(validators))}")
end
- 1
def cache_control_segments
if cache_control = _cache_control
cache_control.delete(" ").split(",")
else
[]
end
end
- 1
def cache_control_headers
cache_control = {}
cache_control_segments.each do |segment|
directive, argument = segment.split("=", 2)
if SPECIAL_KEYS.include? directive
directive.tr!("-", "_")
cache_control[directive.to_sym] = argument || true
else
cache_control[:extras] ||= []
cache_control[:extras] << segment
end
end
cache_control
end
- 1
def prepare_cache_control!
@cache_control = cache_control_headers
end
- 1
DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate"
- 1
NO_CACHE = "no-cache"
- 1
PUBLIC = "public"
- 1
PRIVATE = "private"
- 1
MUST_REVALIDATE = "must-revalidate"
- 1
def handle_conditional_get!
# Normally default cache control setting is handled by ETag
# middleware. But, if an etag is already set, the middleware
# defaults to `no-cache` unless a default `Cache-Control` value is
# previously set. So, set a default one here.
if (etag? || last_modified?) && !self._cache_control
self._cache_control = DEFAULT_CACHE_CONTROL
end
end
- 1
def merge_and_normalize_cache_control!(cache_control)
control = cache_control_headers
return if control.empty? && cache_control.empty? # Let middleware handle default behavior
if extras = control.delete(:extras)
cache_control[:extras] ||= []
cache_control[:extras] += extras
cache_control[:extras].uniq!
end
control.merge! cache_control
if control[:no_cache]
options = []
options << PUBLIC if control[:public]
options << NO_CACHE
options.concat(control[:extras]) if control[:extras]
self._cache_control = options.join(", ")
else
extras = control[:extras]
max_age = control[:max_age]
stale_while_revalidate = control[:stale_while_revalidate]
stale_if_error = control[:stale_if_error]
options = []
options << "max-age=#{max_age.to_i}" if max_age
options << (control[:public] ? PUBLIC : PRIVATE)
options << MUST_REVALIDATE if control[:must_revalidate]
options << "stale-while-revalidate=#{stale_while_revalidate.to_i}" if stale_while_revalidate
options << "stale-if-error=#{stale_if_error.to_i}" if stale_if_error
options.concat(extras) if extras
self._cache_control = options.join(", ")
end
end
end
end
end
end
# frozen_string_literal: true
- 1
module ActionDispatch
- 1
module Http
- 1
class ContentDisposition # :nodoc:
- 1
def self.format(disposition:, filename:)
new(disposition: disposition, filename: filename).to_s
end
- 1
attr_reader :disposition, :filename
- 1
def initialize(disposition:, filename:)
@disposition = disposition
@filename = filename
end
- 1
TRADITIONAL_ESCAPED_CHAR = /[^ A-Za-z0-9!#$+.^_`|~-]/
- 1
def ascii_filename
'filename="' + percent_escape(I18n.transliterate(filename), TRADITIONAL_ESCAPED_CHAR) + '"'
end
- 1
RFC_5987_ESCAPED_CHAR = /[^A-Za-z0-9!#$&+.^_`|~-]/
- 1
def utf8_filename
"filename*=UTF-8''" + percent_escape(filename, RFC_5987_ESCAPED_CHAR)
end
- 1
def to_s
if filename
"#{disposition}; #{ascii_filename}; #{utf8_filename}"
else
"#{disposition}"
end
end
- 1
private
- 1
def percent_escape(string, pattern)
string.gsub(pattern) do |char|
char.bytes.map { |byte| "%%%02X" % byte }.join
end
end
end
end
end
# frozen_string_literal: true
- 1
require "active_support/core_ext/object/deep_dup"
- 1
module ActionDispatch #:nodoc:
- 1
class ContentSecurityPolicy
- 1
class Middleware
- 1
CONTENT_TYPE = "Content-Type"
- 1
POLICY = "Content-Security-Policy"
- 1
POLICY_REPORT_ONLY = "Content-Security-Policy-Report-Only"
- 1
def initialize(app)
- 4
@app = app
end
- 1
def call(env)
request = ActionDispatch::Request.new env
_, headers, _ = response = @app.call(env)
return response unless html_response?(headers)
return response if policy_present?(headers)
if policy = request.content_security_policy
nonce = request.content_security_policy_nonce
nonce_directives = request.content_security_policy_nonce_directives
context = request.controller_instance || request
headers[header_name(request)] = policy.build(context, nonce, nonce_directives)
end
response
end
- 1
private
- 1
def html_response?(headers)
if content_type = headers[CONTENT_TYPE]
/html/.match?(content_type)
end
end
- 1
def header_name(request)
if request.content_security_policy_report_only
POLICY_REPORT_ONLY
else
POLICY
end
end
- 1
def policy_present?(headers)
headers[POLICY] || headers[POLICY_REPORT_ONLY]
end
end
- 1
module Request
- 1
POLICY = "action_dispatch.content_security_policy"
- 1
POLICY_REPORT_ONLY = "action_dispatch.content_security_policy_report_only"
- 1
NONCE_GENERATOR = "action_dispatch.content_security_policy_nonce_generator"
- 1
NONCE = "action_dispatch.content_security_policy_nonce"
- 1
NONCE_DIRECTIVES = "action_dispatch.content_security_policy_nonce_directives"
- 1
def content_security_policy
get_header(POLICY)
end
- 1
def content_security_policy=(policy)
set_header(POLICY, policy)
end
- 1
def content_security_policy_report_only
get_header(POLICY_REPORT_ONLY)
end
- 1
def content_security_policy_report_only=(value)
set_header(POLICY_REPORT_ONLY, value)
end
- 1
def content_security_policy_nonce_generator
get_header(NONCE_GENERATOR)
end
- 1
def content_security_policy_nonce_generator=(generator)
set_header(NONCE_GENERATOR, generator)
end
- 1
def content_security_policy_nonce_directives
get_header(NONCE_DIRECTIVES)
end
- 1
def content_security_policy_nonce_directives=(generator)
set_header(NONCE_DIRECTIVES, generator)
end
- 1
def content_security_policy_nonce
if content_security_policy_nonce_generator
if nonce = get_header(NONCE)
nonce
else
set_header(NONCE, generate_content_security_policy_nonce)
end
end
end
- 1
private
- 1
def generate_content_security_policy_nonce
content_security_policy_nonce_generator.call(self)
end
end
- 1
MAPPINGS = {
self: "'self'",
unsafe_eval: "'unsafe-eval'",
unsafe_inline: "'unsafe-inline'",
none: "'none'",
http: "http:",
https: "https:",
data: "data:",
mediastream: "mediastream:",
blob: "blob:",
filesystem: "filesystem:",
report_sample: "'report-sample'",
strict_dynamic: "'strict-dynamic'",
ws: "ws:",
wss: "wss:"
}.freeze
- 1
DIRECTIVES = {
base_uri: "base-uri",
child_src: "child-src",
connect_src: "connect-src",
default_src: "default-src",
font_src: "font-src",
form_action: "form-action",
frame_ancestors: "frame-ancestors",
frame_src: "frame-src",
img_src: "img-src",
manifest_src: "manifest-src",
media_src: "media-src",
object_src: "object-src",
prefetch_src: "prefetch-src",
script_src: "script-src",
script_src_attr: "script-src-attr",
script_src_elem: "script-src-elem",
style_src: "style-src",
style_src_attr: "style-src-attr",
style_src_elem: "style-src-elem",
worker_src: "worker-src"
}.freeze
- 1
DEFAULT_NONCE_DIRECTIVES = %w[script-src style-src].freeze
- 1
private_constant :MAPPINGS, :DIRECTIVES, :DEFAULT_NONCE_DIRECTIVES
- 1
attr_reader :directives
- 1
def initialize
- 3
@directives = {}
- 3
yield self if block_given?
end
- 1
def initialize_copy(other)
@directives = other.directives.deep_dup
end
- 1
DIRECTIVES.each do |name, directive|
- 20
define_method(name) do |*sources|
- 6
if sources.first
- 6
@directives[directive] = apply_mappings(sources)
else
@directives.delete(directive)
end
end
end
- 1
def block_all_mixed_content(enabled = true)
if enabled
@directives["block-all-mixed-content"] = true
else
@directives.delete("block-all-mixed-content")
end
end
- 1
def plugin_types(*types)
if types.first
@directives["plugin-types"] = types
else
@directives.delete("plugin-types")
end
end
- 1
def report_uri(uri)
@directives["report-uri"] = [uri]
end
- 1
def require_sri_for(*types)
if types.first
@directives["require-sri-for"] = types
else
@directives.delete("require-sri-for")
end
end
- 1
def sandbox(*values)
if values.empty?
@directives["sandbox"] = true
elsif values.first
@directives["sandbox"] = values
else
@directives.delete("sandbox")
end
end
- 1
def upgrade_insecure_requests(enabled = true)
if enabled
@directives["upgrade-insecure-requests"] = true
else
@directives.delete("upgrade-insecure-requests")
end
end
- 1
def build(context = nil, nonce = nil, nonce_directives = nil)
nonce_directives = DEFAULT_NONCE_DIRECTIVES if nonce_directives.nil?
build_directives(context, nonce, nonce_directives).compact.join("; ")
end
- 1
private
- 1
def apply_mappings(sources)
- 6
sources.map do |source|
- 6
case source
when Symbol
- 1
apply_mapping(source)
when String, Proc
- 5
source
else
raise ArgumentError, "Invalid content security policy source: #{source.inspect}"
end
end
end
- 1
def apply_mapping(source)
- 1
MAPPINGS.fetch(source) do
raise ArgumentError, "Unknown content security policy source mapping: #{source.inspect}"
end
end
- 1
def build_directives(context, nonce, nonce_directives)
@directives.map do |directive, sources|
if sources.is_a?(Array)
if nonce && nonce_directive?(directive, nonce_directives)
"#{directive} #{build_directive(sources, context).join(' ')} 'nonce-#{nonce}'"
else
"#{directive} #{build_directive(sources, context).join(' ')}"
end
elsif sources
directive
else
nil
end
end
end
- 1
def build_directive(sources, context)
sources.map { |source| resolve_source(source, context) }
end
- 1
def resolve_source(source, context)
case source
when String
source
when Symbol
source.to_s
when Proc
if context.nil?
raise RuntimeError, "Missing context for the dynamic content security policy source: #{source.inspect}"
else
resolved = context.instance_exec(&source)
resolved.is_a?(Symbol) ? apply_mapping(resolved) : resolved
end
else
raise RuntimeError, "Unexpected content security policy source: #{source.inspect}"
end
end
- 1
def nonce_directive?(directive, nonce_directives)
nonce_directives.include?(directive)
end
end
end
# frozen_string_literal: true
- 1
require "active_support/core_ext/object/deep_dup"
- 1
module ActionDispatch #:nodoc:
- 1
class FeaturePolicy
- 1
class Middleware
- 1
CONTENT_TYPE = "Content-Type"
- 1
POLICY = "Feature-Policy"
- 1
def initialize(app)
- 1
@app = app
end
- 1
def call(env)
request = ActionDispatch::Request.new(env)
_, headers, _ = response = @app.call(env)
return response unless html_response?(headers)
return response if policy_present?(headers)
if policy = request.feature_policy
headers[POLICY] = policy.build(request.controller_instance)
end
if policy_empty?(policy)
headers.delete(POLICY)
end
response
end
- 1
private
- 1
def html_response?(headers)
if content_type = headers[CONTENT_TYPE]
/html/.match?(content_type)
end
end
- 1
def policy_present?(headers)
headers[POLICY]
end
- 1
def policy_empty?(policy)
policy&.directives&.empty?
end
end
- 1
module Request
- 1
POLICY = "action_dispatch.feature_policy"
- 1
def feature_policy
get_header(POLICY)
end
- 1
def feature_policy=(policy)
set_header(POLICY, policy)
end
end
- 1
MAPPINGS = {
self: "'self'",
none: "'none'",
}.freeze
# List of available features can be found at
# https://github.com/WICG/feature-policy/blob/master/features.md#policy-controlled-features
- 1
DIRECTIVES = {
accelerometer: "accelerometer",
ambient_light_sensor: "ambient-light-sensor",
autoplay: "autoplay",
camera: "camera",
encrypted_media: "encrypted-media",
fullscreen: "fullscreen",
geolocation: "geolocation",
gyroscope: "gyroscope",
magnetometer: "magnetometer",
microphone: "microphone",
midi: "midi",
payment: "payment",
picture_in_picture: "picture-in-picture",
speaker: "speaker",
usb: "usb",
vibrate: "vibrate",
vr: "vr",
}.freeze
- 1
private_constant :MAPPINGS, :DIRECTIVES
- 1
attr_reader :directives
- 1
def initialize
- 1
@directives = {}
- 1
yield self if block_given?
end
- 1
def initialize_copy(other)
@directives = other.directives.deep_dup
end
- 1
DIRECTIVES.each do |name, directive|
- 17
define_method(name) do |*sources|
- 1
if sources.first
- 1
@directives[directive] = apply_mappings(sources)
else
@directives.delete(directive)
end
end
end
- 1
def build(context = nil)
build_directives(context).compact.join("; ")
end
- 1
private
- 1
def apply_mappings(sources)
- 1
sources.map do |source|
- 1
case source
when Symbol
- 1
apply_mapping(source)
when String, Proc
source
else
raise ArgumentError, "Invalid HTTP feature policy source: #{source.inspect}"
end
end
end
- 1
def apply_mapping(source)
- 1
MAPPINGS.fetch(source) do
raise ArgumentError, "Unknown HTTP feature policy source mapping: #{source.inspect}"
end
end
- 1
def build_directives(context)
@directives.map do |directive, sources|
if sources.is_a?(Array)
"#{directive} #{build_directive(sources, context).join(' ')}"
elsif sources
directive
else
nil
end
end
end
- 1
def build_directive(sources, context)
sources.map { |source| resolve_source(source, context) }
end
- 1
def resolve_source(source, context)
case source
when String
source
when Symbol
source.to_s
when Proc
if context.nil?
raise RuntimeError, "Missing context for the dynamic feature policy source: #{source.inspect}"
else
context.instance_exec(&source)
end
else
raise RuntimeError, "Unexpected feature policy source: #{source.inspect}"
end
end
end
end
# frozen_string_literal: true
- 1
require "active_support/parameter_filter"
- 1
module ActionDispatch
- 1
module Http
# Allows you to specify sensitive parameters which will be replaced from
# the request log by looking in the query string of the request and all
# sub-hashes of the params hash to filter. Filtering only certain sub-keys
# from a hash is possible by using the dot notation: 'credit_card.number'.
# If a block is given, each key and value of the params hash and all
# sub-hashes are passed to it, where the value or the key can be replaced using
# String#replace or similar methods.
#
# env["action_dispatch.parameter_filter"] = [:password]
# => replaces the value to all keys matching /password/i with "[FILTERED]"
#
# env["action_dispatch.parameter_filter"] = [:foo, "bar"]
# => replaces the value to all keys matching /foo|bar/i with "[FILTERED]"
#
# env["action_dispatch.parameter_filter"] = [ "credit_card.code" ]
# => replaces { credit_card: {code: "xxxx"} } with "[FILTERED]", does not
# change { file: { code: "xxxx"} }
#
# env["action_dispatch.parameter_filter"] = -> (k, v) do
# v.reverse! if k.match?(/secret/i)
# end
# => reverses the value to all keys matching /secret/i
- 1
module FilterParameters
- 1
ENV_MATCH = [/RAW_POST_DATA/, "rack.request.form_vars"] # :nodoc:
- 1
NULL_PARAM_FILTER = ActiveSupport::ParameterFilter.new # :nodoc:
- 1
NULL_ENV_FILTER = ActiveSupport::ParameterFilter.new ENV_MATCH # :nodoc:
- 1
def initialize
super
@filtered_parameters = nil
@filtered_env = nil
@filtered_path = nil
end
# Returns a hash of parameters with all sensitive data replaced.
- 1
def filtered_parameters
@filtered_parameters ||= parameter_filter.filter(parameters)
rescue ActionDispatch::Http::Parameters::ParseError
@filtered_parameters = {}
end
# Returns a hash of request.env with all sensitive data replaced.
- 1
def filtered_env
@filtered_env ||= env_filter.filter(@env)
end
# Reconstructs a path with all sensitive GET parameters replaced.
- 1
def filtered_path
@filtered_path ||= query_string.empty? ? path : "#{path}?#{filtered_query_string}"
end
- 1
private
- 1
def parameter_filter # :doc:
parameter_filter_for fetch_header("action_dispatch.parameter_filter") {
return NULL_PARAM_FILTER
}
end
- 1
def env_filter # :doc:
user_key = fetch_header("action_dispatch.parameter_filter") {
return NULL_ENV_FILTER
}
parameter_filter_for(Array(user_key) + ENV_MATCH)
end
- 1
def parameter_filter_for(filters) # :doc:
ActiveSupport::ParameterFilter.new(filters)
end
- 1
KV_RE = "[^&;=]+"
- 1
PAIR_RE = %r{(#{KV_RE})=(#{KV_RE})}
- 1
def filtered_query_string # :doc:
query_string.gsub(PAIR_RE) do |_|
parameter_filter.filter($1 => $2).first.join("=")
end
end
end
end
end
# frozen_string_literal: true
- 1
module ActionDispatch
- 1
module Http
- 1
module FilterRedirect
- 1
FILTERED = "[FILTERED]" # :nodoc:
- 1
def filtered_location # :nodoc:
if location_filter_match?
FILTERED
else
location
end
end
- 1
private
- 1
def location_filters
if request
request.get_header("action_dispatch.redirect_filter") || []
else
[]
end
end
- 1
def location_filter_match?
location_filters.any? do |filter|
if String === filter
location.include?(filter)
elsif Regexp === filter
location.match?(filter)
end
end
end
end
end
end
# frozen_string_literal: true
- 1
module ActionDispatch
- 1
module Http
# Provides access to the request's HTTP headers from the environment.
#
# env = { "CONTENT_TYPE" => "text/plain", "HTTP_USER_AGENT" => "curl/7.43.0" }
# headers = ActionDispatch::Http::Headers.from_hash(env)
# headers["Content-Type"] # => "text/plain"
# headers["User-Agent"] # => "curl/7.43.0"
#
# Also note that when headers are mapped to CGI-like variables by the Rack
# server, both dashes and underscores are converted to underscores. This
# ambiguity cannot be resolved at this stage anymore. Both underscores and
# dashes have to be interpreted as if they were originally sent as dashes.
#
# # GET / HTTP/1.1
# # ...
# # User-Agent: curl/7.43.0
# # X_Custom_Header: token
#
# headers["X_Custom_Header"] # => nil
# headers["X-Custom-Header"] # => "token"
- 1
class Headers
- 1
CGI_VARIABLES = Set.new(%W[
AUTH_TYPE
CONTENT_LENGTH
CONTENT_TYPE
GATEWAY_INTERFACE
HTTPS
PATH_INFO
PATH_TRANSLATED
QUERY_STRING
REMOTE_ADDR
REMOTE_HOST
REMOTE_IDENT
REMOTE_USER
REQUEST_METHOD
SCRIPT_NAME
SERVER_NAME
SERVER_PORT
SERVER_PROTOCOL
SERVER_SOFTWARE
]).freeze
- 1
HTTP_HEADER = /\A[A-Za-z0-9-]+\z/
- 1
include Enumerable
- 1
def self.from_hash(hash)
new ActionDispatch::Request.new hash
end
- 1
def initialize(request) # :nodoc:
@req = request
end
# Returns the value for the given key mapped to @env.
- 1
def [](key)
@req.get_header env_name(key)
end
# Sets the given value for the key mapped to @env.
- 1
def []=(key, value)
@req.set_header env_name(key), value
end
# Add a value to a multivalued header like Vary or Accept-Encoding.
- 1
def add(key, value)
@req.add_header env_name(key), value
end
- 1
def key?(key)
@req.has_header? env_name(key)
end
- 1
alias :include? :key?
- 1
DEFAULT = Object.new # :nodoc:
# Returns the value for the given key mapped to @env.
#
# If the key is not found and an optional code block is not provided,
# raises a <tt>KeyError</tt> exception.
#
# If the code block is provided, then it will be run and
# its result returned.
- 1
def fetch(key, default = DEFAULT)
@req.fetch_header(env_name(key)) do
return default unless default == DEFAULT
return yield if block_given?
raise KeyError, key
end
end
- 1
def each(&block)
@req.each_header(&block)
end
# Returns a new Http::Headers instance containing the contents of
# <tt>headers_or_env</tt> and the original instance.
- 1
def merge(headers_or_env)
headers = @req.dup.headers
headers.merge!(headers_or_env)
headers
end
# Adds the contents of <tt>headers_or_env</tt> to original instance
# entries; duplicate keys are overwritten with the values from
# <tt>headers_or_env</tt>.
- 1
def merge!(headers_or_env)
headers_or_env.each do |key, value|
@req.set_header env_name(key), value
end
end
- 1
def env; @req.env.dup; end
- 1
private
# Converts an HTTP header name to an environment variable name if it is
# not contained within the headers hash.
- 1
def env_name(key)
key = key.to_s
if HTTP_HEADER.match?(key)
key = key.upcase
key.tr!("-", "_")
key.prepend("HTTP_") unless CGI_VARIABLES.include?(key)
end
key
end
end
end
end
# frozen_string_literal: true
- 1
require "active_support/core_ext/module/attribute_accessors"
- 1
module ActionDispatch
- 1
module Http
- 1
module MimeNegotiation
- 1
extend ActiveSupport::Concern
- 1
RESCUABLE_MIME_FORMAT_ERRORS = [
ActionController::BadRequest,
ActionDispatch::Http::Parameters::ParseError,
]
- 1
included do
- 1
mattr_accessor :ignore_accept_header, default: false
end
# The MIME type of the HTTP request, such as Mime[:xml].
- 1
def content_mime_type
fetch_header("action_dispatch.request.content_type") do |k|
v = if get_header("CONTENT_TYPE") =~ /^([^,\;]*)/
Mime::Type.lookup($1.strip.downcase)
else
nil
end
set_header k, v
end
end
- 1
def content_type
content_mime_type && content_mime_type.to_s
end
- 1
def has_content_type? # :nodoc:
get_header "CONTENT_TYPE"
end
# Returns the accepted MIME type for the request.
- 1
def accepts
fetch_header("action_dispatch.request.accepts") do |k|
header = get_header("HTTP_ACCEPT").to_s.strip
v = if header.empty?
[content_mime_type]
else
Mime::Type.parse(header)
end
set_header k, v
end
end
# Returns the MIME type for the \format used in the request.
#
# GET /posts/5.xml | request.format => Mime[:xml]
# GET /posts/5.xhtml | request.format => Mime[:html]
# GET /posts/5 | request.format => Mime[:html] or Mime[:js], or request.accepts.first
#
- 1
def format(view_path = [])
formats.first || Mime::NullType.instance
end
- 1
def formats
fetch_header("action_dispatch.request.formats") do |k|
v = if params_readable?
Array(Mime[parameters[:format]])
elsif use_accept_header && valid_accept_header
accepts
elsif extension_format = format_from_path_extension
[extension_format]
elsif xhr?
[Mime[:js]]
else
[Mime[:html]]
end
v = v.select do |format|
format.symbol || format.ref == "*/*"
end
set_header k, v
end
end
# Sets the \variant for template.
- 1
def variant=(variant)
variant = Array(variant)
if variant.all? { |v| v.is_a?(Symbol) }
@variant = ActiveSupport::ArrayInquirer.new(variant)
else
raise ArgumentError, "request.variant must be set to a Symbol or an Array of Symbols."
end
end
- 1
def variant
@variant ||= ActiveSupport::ArrayInquirer.new
end
# Sets the \format by string extension, which can be used to force custom formats
# that are not controlled by the extension.
#
# class ApplicationController < ActionController::Base
# before_action :adjust_format_for_iphone
#
# private
# def adjust_format_for_iphone
# request.format = :iphone if request.env["HTTP_USER_AGENT"][/iPhone/]
# end
# end
- 1
def format=(extension)
parameters[:format] = extension.to_s
set_header "action_dispatch.request.formats", [Mime::Type.lookup_by_extension(parameters[:format])]
end
# Sets the \formats by string extensions. This differs from #format= by allowing 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 fallback
# to the :html format.
#
# class ApplicationController < ActionController::Base
# before_action :adjust_format_for_iphone_with_html_fallback
#
# private
# def adjust_format_for_iphone_with_html_fallback
# request.formats = [ :iphone, :html ] if request.env["HTTP_USER_AGENT"][/iPhone/]
# end
# end
- 1
def formats=(extensions)
parameters[:format] = extensions.first.to_s
set_header "action_dispatch.request.formats", extensions.collect { |extension|
Mime::Type.lookup_by_extension(extension)
}
end
# Returns the first MIME type that matches the provided array of MIME types.
- 1
def negotiate_mime(order)
formats.each do |priority|
if priority == Mime::ALL
return order.first
elsif order.include?(priority)
return priority
end
end
order.include?(Mime::ALL) ? format : nil
end
- 1
def should_apply_vary_header?
!params_readable? && use_accept_header && valid_accept_header
end
- 1
private
# We use normal content negotiation unless you include */* in your list,
# in which case we assume you're a browser and send HTML.
- 1
BROWSER_LIKE_ACCEPTS = /,\s*\*\/\*|\*\/\*\s*,/
- 1
def params_readable? # :doc:
parameters[:format]
rescue *RESCUABLE_MIME_FORMAT_ERRORS
false
end
- 1
def valid_accept_header # :doc:
(xhr? && (accept.present? || content_mime_type)) ||
(accept.present? && !accept.match?(BROWSER_LIKE_ACCEPTS))
end
- 1
def use_accept_header # :doc:
!self.class.ignore_accept_header
end
- 1
def format_from_path_extension # :doc:
path = get_header("action_dispatch.original_path") || get_header("PATH_INFO")
if match = path && path.match(/\.(\w+)\z/)
Mime[match.captures.first]
end
end
end
end
end
# frozen_string_literal: true
- 1
require "singleton"
- 1
require "active_support/core_ext/symbol/starts_ends_with"
- 1
module Mime
- 1
class Mimes
- 1
attr_reader :symbols
- 1
include Enumerable
- 1
def initialize
- 1
@mimes = []
- 1
@symbols = []
end
- 1
def each
- 35
@mimes.each { |x| yield x }
end
- 1
def <<(type)
- 34
@mimes << type
- 34
@symbols << type.to_sym
end
- 1
def delete_if
@mimes.delete_if do |x|
if yield x
@symbols.delete(x.to_sym)
true
end
end
end
end
- 1
SET = Mimes.new
- 1
EXTENSION_LOOKUP = {}
- 1
LOOKUP = {}
- 1
class << self
- 1
def [](type)
- 2
return type if type.is_a?(Type)
- 2
Type.lookup_by_extension(type)
end
- 1
def fetch(type)
return type if type.is_a?(Type)
EXTENSION_LOOKUP.fetch(type.to_s) { |k| yield k }
end
end
# Encapsulates the notion of a MIME type. Can be used at render time, for example, with:
#
# class PostsController < ActionController::Base
# def show
# @post = Post.find(params[:id])
#
# respond_to do |format|
# format.html
# format.ics { render body: @post.to_ics, mime_type: Mime::Type.lookup("text/calendar") }
# format.xml { render xml: @post }
# end
# end
# end
- 1
class Type
- 1
attr_reader :symbol
- 1
@register_callbacks = []
# A simple helper class used in parsing the accept header.
- 1
class AcceptItem #:nodoc:
- 1
attr_accessor :index, :name, :q
- 1
alias :to_s :name
- 1
def initialize(index, name, q = nil)
@index = index
@name = name
q ||= 0.0 if @name == "*/*" # Default wildcard match to end of list.
@q = ((q || 1.0).to_f * 100).to_i
end
- 1
def <=>(item)
result = item.q <=> @q
result = @index <=> item.index if result == 0
result
end
end
- 1
class AcceptList #:nodoc:
- 1
def self.sort!(list)
list.sort!
text_xml_idx = find_item_by_name list, "text/xml"
app_xml_idx = find_item_by_name list, Mime[:xml].to_s
# Take care of the broken text/xml entry by renaming or deleting it.
if text_xml_idx && app_xml_idx
app_xml = list[app_xml_idx]
text_xml = list[text_xml_idx]
app_xml.q = [text_xml.q, app_xml.q].max # Set the q value to the max of the two.
if app_xml_idx > text_xml_idx # Make sure app_xml is ahead of text_xml in the list.
list[app_xml_idx], list[text_xml_idx] = text_xml, app_xml
app_xml_idx, text_xml_idx = text_xml_idx, app_xml_idx
end
list.delete_at(text_xml_idx) # Delete text_xml from the list.
elsif text_xml_idx
list[text_xml_idx].name = Mime[:xml].to_s
end
# Look for more specific XML-based types and sort them ahead of app/xml.
if app_xml_idx
app_xml = list[app_xml_idx]
idx = app_xml_idx
while idx < list.length
type = list[idx]
break if type.q < app_xml.q
if type.name.end_with? "+xml"
list[app_xml_idx], list[idx] = list[idx], app_xml
app_xml_idx = idx
end
idx += 1
end
end
list.map! { |i| Mime::Type.lookup(i.name) }.uniq!
list
end
- 1
def self.find_item_by_name(array, name)
array.index { |item| item.name == name }
end
end
- 1
class << self
- 1
TRAILING_STAR_REGEXP = /^(text|application)\/\*/
- 1
PARAMETER_SEPARATOR_REGEXP = /;\s*\w+="?\w+"?/
- 1
def register_callback(&block)
- 1
@register_callbacks << block
end
- 1
def lookup(string)
LOOKUP[string] || Type.new(string)
end
- 1
def lookup_by_extension(extension)
- 2
EXTENSION_LOOKUP[extension.to_s]
end
# Registers an alias that's not used on MIME type lookup, but can be referenced directly. Especially useful for
# rendering different HTML versions depending on the user agent, like an iPhone.
- 1
def register_alias(string, symbol, extension_synonyms = [])
register(string, symbol, [], extension_synonyms, true)
end
- 1
def register(string, symbol, mime_type_synonyms = [], extension_synonyms = [], skip_lookup = false)
- 34
new_mime = Type.new(string, symbol, mime_type_synonyms)
- 34
SET << new_mime
- 79
([string] + mime_type_synonyms).each { |str| LOOKUP[str] = new_mime } unless skip_lookup
- 104
([symbol] + extension_synonyms).each { |ext| EXTENSION_LOOKUP[ext.to_s] = new_mime }
- 34
@register_callbacks.each do |callback|
callback.call(new_mime)
end
- 34
new_mime
end
- 1
def parse(accept_header)
if !accept_header.include?(",")
accept_header = accept_header.split(PARAMETER_SEPARATOR_REGEXP).first
return [] unless accept_header
parse_trailing_star(accept_header) || [Mime::Type.lookup(accept_header)].compact
else
list, index = [], 0
accept_header.split(",").each do |header|
params, q = header.split(PARAMETER_SEPARATOR_REGEXP)
next unless params
params.strip!
next if params.empty?
params = parse_trailing_star(params) || [params]
params.each do |m|
list << AcceptItem.new(index, m.to_s, q)
index += 1
end
end
AcceptList.sort! list
end
end
- 1
def parse_trailing_star(accept_header)
parse_data_with_trailing_star($1) if accept_header =~ TRAILING_STAR_REGEXP
end
# For an input of <tt>'text'</tt>, returns <tt>[Mime[:json], Mime[:xml], Mime[:ics],
# Mime[:html], Mime[:css], Mime[:csv], Mime[:js], Mime[:yaml], Mime[:text]</tt>.
#
# For an input of <tt>'application'</tt>, returns <tt>[Mime[:html], Mime[:js],
# Mime[:xml], Mime[:yaml], Mime[:atom], Mime[:json], Mime[:rss], Mime[:url_encoded_form]</tt>.
- 1
def parse_data_with_trailing_star(type)
Mime::SET.select { |m| m.match?(type) }
end
# This method is opposite of register method.
#
# To unregister a MIME type:
#
# Mime::Type.unregister(:mobile)
- 1
def unregister(symbol)
symbol = symbol.downcase
if mime = Mime[symbol]
SET.delete_if { |v| v.eql?(mime) }
LOOKUP.delete_if { |_, v| v.eql?(mime) }
EXTENSION_LOOKUP.delete_if { |_, v| v.eql?(mime) }
end
end
end
- 1
attr_reader :hash
- 1
MIME_NAME = "[a-zA-Z0-9][a-zA-Z0-9#{Regexp.escape('!#$&-^_.+')}]{0,126}"
- 1
MIME_PARAMETER_KEY = "[a-zA-Z0-9][a-zA-Z0-9#{Regexp.escape('!#$&-^_.+')}]{0,126}"
- 1
MIME_PARAMETER_VALUE = "#{Regexp.escape('"')}?[a-zA-Z0-9][a-zA-Z0-9#{Regexp.escape('!#$&-^_.+')}]{0,126}#{Regexp.escape('"')}?"
- 1
MIME_PARAMETER = "\s*\;\s*#{MIME_PARAMETER_KEY}(?:\=#{MIME_PARAMETER_VALUE})?"
- 1
MIME_REGEXP = /\A(?:\*\/\*|#{MIME_NAME}\/(?:\*|#{MIME_NAME})(?:\s*#{MIME_PARAMETER}\s*)*)\z/
- 1
class InvalidMimeType < StandardError; end
- 1
def initialize(string, symbol = nil, synonyms = [])
- 35
unless MIME_REGEXP.match?(string)
raise InvalidMimeType, "#{string.inspect} is not a valid MIME type"
end
- 35
@symbol, @synonyms = symbol, synonyms
- 35
@string = string
- 35
@hash = [@string, @synonyms, @symbol].hash
end
- 1
def to_s
@string
end
- 1
def to_str
to_s
end
- 1
def to_sym
- 68
@symbol
end
- 1
def ref
symbol || to_s
end
- 1
def ===(list)
if list.is_a?(Array)
(@synonyms + [ self ]).any? { |synonym| list.include?(synonym) }
else
super
end
end
- 1
def ==(mime_type)
return false unless mime_type
(@synonyms + [ self ]).any? do |synonym|
synonym.to_s == mime_type.to_s || synonym.to_sym == mime_type.to_sym
end
end
- 1
def eql?(other)
super || (self.class == other.class &&
@string == other.string &&
@synonyms == other.synonyms &&
@symbol == other.symbol)
end
- 1
def =~(mime_type)
return false unless mime_type
regexp = Regexp.new(Regexp.quote(mime_type.to_s))
@synonyms.any? { |synonym| synonym.to_s =~ regexp } || @string =~ regexp
end
- 1
def match?(mime_type)
return false unless mime_type
regexp = Regexp.new(Regexp.quote(mime_type.to_s))
@synonyms.any? { |synonym| synonym.to_s.match?(regexp) } || @string.match?(regexp)
end
- 1
def html?
(symbol == :html) || /html/.match?(@string)
end
- 1
def all?; false; end
- 1
protected
- 1
attr_reader :string, :synonyms
- 1
private
- 1
def to_ary; end
- 1
def to_a; end
- 1
def method_missing(method, *args)
if method.end_with?("?")
method[0..-2].downcase.to_sym == to_sym
else
super
end
end
- 1
def respond_to_missing?(method, include_private = false)
method.end_with?("?") || super
end
end
- 1
class AllType < Type
- 1
include Singleton
- 1
def initialize
- 1
super "*/*", nil
end
- 1
def all?; true; end
- 1
def html?; true; end
end
# ALL isn't a real MIME type, so we don't register it for lookup with the
# other concrete types. It's a wildcard match that we use for `respond_to`
# negotiation internals.
- 1
ALL = AllType.instance
- 1
class NullType
- 1
include Singleton
- 1
def nil?
true
end
- 1
def to_s
""
end
- 1
def ref; end
- 1
private
- 1
def respond_to_missing?(method, _)
method.end_with?("?")
end
- 1
def method_missing(method, *args)
false if method.end_with?("?")
end
end
end
- 1
require "action_dispatch/http/mime_types"
# frozen_string_literal: true
# Build list of Mime types for HTTP responses
# https://www.iana.org/assignments/media-types/
- 1
Mime::Type.register "text/html", :html, %w( application/xhtml+xml ), %w( xhtml )
- 1
Mime::Type.register "text/plain", :text, [], %w(txt)
- 1
Mime::Type.register "text/javascript", :js, %w( application/javascript application/x-javascript )
- 1
Mime::Type.register "text/css", :css
- 1
Mime::Type.register "text/calendar", :ics
- 1
Mime::Type.register "text/csv", :csv
- 1
Mime::Type.register "text/vcard", :vcf
- 1
Mime::Type.register "text/vtt", :vtt, %w(vtt)
- 1
Mime::Type.register "image/png", :png, [], %w(png)
- 1
Mime::Type.register "image/jpeg", :jpeg, [], %w(jpg jpeg jpe pjpeg)
- 1
Mime::Type.register "image/gif", :gif, [], %w(gif)
- 1
Mime::Type.register "image/bmp", :bmp, [], %w(bmp)
- 1
Mime::Type.register "image/tiff", :tiff, [], %w(tif tiff)
- 1
Mime::Type.register "image/svg+xml", :svg
- 1
Mime::Type.register "video/mpeg", :mpeg, [], %w(mpg mpeg mpe)
- 1
Mime::Type.register "audio/mpeg", :mp3, [], %w(mp1 mp2 mp3)
- 1
Mime::Type.register "audio/ogg", :ogg, [], %w(oga ogg spx opus)
- 1
Mime::Type.register "audio/aac", :m4a, %w( audio/mp4 ), %w(m4a mpg4 aac)
- 1
Mime::Type.register "video/webm", :webm, [], %w(webm)
- 1
Mime::Type.register "video/mp4", :mp4, [], %w(mp4 m4v)
- 1
Mime::Type.register "font/otf", :otf, [], %w(otf)
- 1
Mime::Type.register "font/ttf", :ttf, [], %w(ttf)
- 1
Mime::Type.register "font/woff", :woff, [], %w(woff)
- 1
Mime::Type.register "font/woff2", :woff2, [], %w(woff2)
- 1
Mime::Type.register "application/xml", :xml, %w( text/xml application/x-xml )
- 1
Mime::Type.register "application/rss+xml", :rss
- 1
Mime::Type.register "application/atom+xml", :atom
- 1
Mime::Type.register "application/x-yaml", :yaml, %w( text/yaml ), %w(yml yaml)
- 1
Mime::Type.register "multipart/form-data", :multipart_form
- 1
Mime::Type.register "application/x-www-form-urlencoded", :url_encoded_form
# https://www.ietf.org/rfc/rfc4627.txt
# http://www.json.org/JSONRequest.html
- 1
Mime::Type.register "application/json", :json, %w( text/x-json application/jsonrequest )
- 1
Mime::Type.register "application/pdf", :pdf, [], %w(pdf)
- 1
Mime::Type.register "application/zip", :zip, [], %w(zip)
- 1
Mime::Type.register "application/gzip", :gzip, %w(application/x-gzip), %w(gz)
# frozen_string_literal: true
require "active_support/deprecation/constant_accessor"
require "active_support/parameter_filter"
module ActionDispatch
module Http
include ActiveSupport::Deprecation::DeprecatedConstantAccessor
deprecate_constant "ParameterFilter", "ActiveSupport::ParameterFilter",
message: "ActionDispatch::Http::ParameterFilter is deprecated and will be removed from Rails 6.1. Use ActiveSupport::ParameterFilter instead."
end
end
# frozen_string_literal: true
- 1
module ActionDispatch
- 1
module Http
- 1
module Parameters
- 1
extend ActiveSupport::Concern
- 1
PARAMETERS_KEY = "action_dispatch.request.path_parameters"
- 1
DEFAULT_PARSERS = {
Mime[:json].symbol => -> (raw_post) {
data = ActiveSupport::JSON.decode(raw_post)
data.is_a?(Hash) ? data : { _json: data }
}
}
# Raised when raw data from the request cannot be parsed by the parser
# defined for request's content MIME type.
- 1
class ParseError < StandardError
- 1
def initialize
super($!.message)
end
end
- 1
included do
- 1
class << self
# Returns the parameter parsers.
- 1
attr_reader :parameter_parsers
end
- 1
self.parameter_parsers = DEFAULT_PARSERS
end
- 1
module ClassMethods
# Configure the parameter parser for a given MIME type.
#
# It accepts a hash where the key is the symbol of the MIME type
# and the value is a proc.
#
# original_parsers = ActionDispatch::Request.parameter_parsers
# xml_parser = -> (raw_post) { Hash.from_xml(raw_post) || {} }
# new_parsers = original_parsers.merge(xml: xml_parser)
# ActionDispatch::Request.parameter_parsers = new_parsers
- 1
def parameter_parsers=(parsers)
- 2
@parameter_parsers = parsers.transform_keys { |key| key.respond_to?(:symbol) ? key.symbol : key }
end
end
# Returns both GET and POST \parameters in a single hash.
- 1
def parameters
params = get_header("action_dispatch.request.parameters")
return params if params
params = begin
request_parameters.merge(query_parameters)
rescue EOFError
query_parameters.dup
end
params.merge!(path_parameters)
params = set_binary_encoding(params, params[:controller], params[:action])
set_header("action_dispatch.request.parameters", params)
params
end
- 1
alias :params :parameters
- 1
def path_parameters=(parameters) #:nodoc:
delete_header("action_dispatch.request.parameters")
parameters = set_binary_encoding(parameters, parameters[:controller], parameters[:action])
# If any of the path parameters has an invalid encoding then
# raise since it's likely to trigger errors further on.
Request::Utils.check_param_encoding(parameters)
set_header PARAMETERS_KEY, parameters
rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e
raise ActionController::BadRequest.new("Invalid path parameters: #{e.message}")
end
# Returns a hash with the \parameters used to form the \path of the request.
# Returned hash keys are strings:
#
# {'action' => 'my_action', 'controller' => 'my_controller'}
- 1
def path_parameters
get_header(PARAMETERS_KEY) || set_header(PARAMETERS_KEY, {})
end
- 1
private
- 1
def set_binary_encoding(params, controller, action)
return params unless controller && controller.valid_encoding?
if binary_params_for?(controller, action)
ActionDispatch::Request::Utils.each_param_value(params.except(:controller, :action)) do |param|
param.force_encoding ::Encoding::ASCII_8BIT
end
end
params
end
- 1
def binary_params_for?(controller, action)
controller_class_for(controller).binary_params_for?(action)
rescue MissingController
false
end
- 1
def parse_formatted_parameters(parsers)
return yield if content_length.zero? || content_mime_type.nil?
strategy = parsers.fetch(content_mime_type.symbol) { return yield }
begin
strategy.call(raw_post)
rescue # JSON or Ruby code block errors.
log_parse_error_once
raise ParseError
end
end
- 1
def log_parse_error_once
@parse_error_logged ||= begin
parse_logger = logger || ActiveSupport::Logger.new($stderr)
parse_logger.debug <<~MSG.chomp
Error occurred while parsing request parameters.
Contents:
#{raw_post}
MSG
end
end
- 1
def params_parsers
ActionDispatch::Request.parameter_parsers
end
end
end
end
# frozen_string_literal: true
- 1
require "rack/cache"
- 1
require "rack/cache/context"
- 1
require "active_support/cache"
- 1
module ActionDispatch
- 1
class RailsMetaStore < Rack::Cache::MetaStore
- 1
def self.resolve(uri)
new
end
- 1
def initialize(store = Rails.cache)
@store = store
end
- 1
def read(key)
if data = @store.read(key)
Marshal.load(data)
else
[]
end
end
- 1
def write(key, value)
@store.write(key, Marshal.dump(value))
end
- 1
::Rack::Cache::MetaStore::RAILS = self
end
- 1
class RailsEntityStore < Rack::Cache::EntityStore
- 1
def self.resolve(uri)
new
end
- 1
def initialize(store = Rails.cache)
@store = store
end
- 1
def exist?(key)
@store.exist?(key)
end
- 1
def open(key)
@store.read(key)
end
- 1
def read(key)
body = open(key)
body.join if body
end
- 1
def write(body)
buf = []
key, size = slurp(body) { |part| buf << part }
@store.write(key, buf)
[key, size]
end
- 1
::Rack::Cache::EntityStore::RAILS = self
end
end
# frozen_string_literal: true
- 1
require "stringio"
- 1
require "active_support/inflector"
- 1
require "action_dispatch/http/headers"
- 1
require "action_controller/metal/exceptions"
- 1
require "rack/request"
- 1
require "action_dispatch/http/cache"
- 1
require "action_dispatch/http/mime_negotiation"
- 1
require "action_dispatch/http/parameters"
- 1
require "action_dispatch/http/filter_parameters"
- 1
require "action_dispatch/http/upload"
- 1
require "action_dispatch/http/url"
- 1
require "active_support/core_ext/array/conversions"
- 1
module ActionDispatch
- 1
class Request
- 1
include Rack::Request::Helpers
- 1
include ActionDispatch::Http::Cache::Request
- 1
include ActionDispatch::Http::MimeNegotiation
- 1
include ActionDispatch::Http::Parameters
- 1
include ActionDispatch::Http::FilterParameters
- 1
include ActionDispatch::Http::URL
- 1
include ActionDispatch::ContentSecurityPolicy::Request
- 1
include ActionDispatch::FeaturePolicy::Request
- 1
include Rack::Request::Env
- 1
autoload :Session, "action_dispatch/request/session"
- 1
autoload :Utils, "action_dispatch/request/utils"
- 1
LOCALHOST = Regexp.union [/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/, /^::1$/, /^0:0:0:0:0:0:0:1(%.*)?$/]
- 1
ENV_METHODS = %w[ AUTH_TYPE GATEWAY_INTERFACE
PATH_TRANSLATED REMOTE_HOST
REMOTE_IDENT REMOTE_USER REMOTE_ADDR
SERVER_NAME SERVER_PROTOCOL
ORIGINAL_SCRIPT_NAME
HTTP_ACCEPT HTTP_ACCEPT_CHARSET HTTP_ACCEPT_ENCODING
HTTP_ACCEPT_LANGUAGE HTTP_CACHE_CONTROL HTTP_FROM
HTTP_NEGOTIATE HTTP_PRAGMA HTTP_CLIENT_IP
HTTP_X_FORWARDED_FOR HTTP_ORIGIN HTTP_VERSION
HTTP_X_CSRF_TOKEN HTTP_X_REQUEST_ID HTTP_X_FORWARDED_HOST
SERVER_ADDR
].freeze
# TODO: Remove SERVER_ADDR when we remove support to Rack 2.1.
# See https://github.com/rack/rack/commit/c173b188d81ee437b588c1e046a1c9f031dea550
- 1
ENV_METHODS.each do |env|
- 26
class_eval <<-METHOD, __FILE__, __LINE__ + 1
# frozen_string_literal: true
def #{env.delete_prefix("HTTP_").downcase} # def accept_charset
get_header "#{env}" # get_header "HTTP_ACCEPT_CHARSET"
end # end
METHOD
end
- 1
def self.empty
new({})
end
- 1
def initialize(env)
super
@method = nil
@request_method = nil
@remote_ip = nil
@original_fullpath = nil
@fullpath = nil
@ip = nil
end
- 1
def commit_cookie_jar! # :nodoc:
end
- 1
PASS_NOT_FOUND = Class.new { # :nodoc:
- 1
def self.action(_); self; end
- 1
def self.call(_); [404, { "X-Cascade" => "pass" }, []]; end
- 1
def self.binary_params_for?(action); false; end
}
- 1
def controller_class
params = path_parameters
params[:action] ||= "index"
controller_class_for(params[:controller])
end
- 1
def controller_class_for(name)
if name
controller_param = name.underscore
const_name = controller_param.camelize << "Controller"
begin
ActiveSupport::Dependencies.constantize(const_name)
rescue NameError => error
if error.missing_name == const_name || const_name.start_with?("#{error.missing_name}::")
raise MissingController.new(error.message, error.name)
else
raise
end
end
else
PASS_NOT_FOUND
end
end
# Returns true if the request has a header matching the given key parameter.
#
# request.key? :ip_spoofing_check # => true
- 1
def key?(key)
has_header? key
end
# List of HTTP request methods from the following RFCs:
# Hypertext Transfer Protocol -- HTTP/1.1 (https://www.ietf.org/rfc/rfc2616.txt)
# HTTP Extensions for Distributed Authoring -- WEBDAV (https://www.ietf.org/rfc/rfc2518.txt)
# Versioning Extensions to WebDAV (https://www.ietf.org/rfc/rfc3253.txt)
# Ordered Collections Protocol (WebDAV) (https://www.ietf.org/rfc/rfc3648.txt)
# Web Distributed Authoring and Versioning (WebDAV) Access Control Protocol (https://www.ietf.org/rfc/rfc3744.txt)
# Web Distributed Authoring and Versioning (WebDAV) SEARCH (https://www.ietf.org/rfc/rfc5323.txt)
# Calendar Extensions to WebDAV (https://www.ietf.org/rfc/rfc4791.txt)
# PATCH Method for HTTP (https://www.ietf.org/rfc/rfc5789.txt)
- 1
RFC2616 = %w(OPTIONS GET HEAD POST PUT DELETE TRACE CONNECT)
- 1
RFC2518 = %w(PROPFIND PROPPATCH MKCOL COPY MOVE LOCK UNLOCK)
- 1
RFC3253 = %w(VERSION-CONTROL REPORT CHECKOUT CHECKIN UNCHECKOUT MKWORKSPACE UPDATE LABEL MERGE BASELINE-CONTROL MKACTIVITY)
- 1
RFC3648 = %w(ORDERPATCH)
- 1
RFC3744 = %w(ACL)
- 1
RFC5323 = %w(SEARCH)
- 1
RFC4791 = %w(MKCALENDAR)
- 1
RFC5789 = %w(PATCH)
- 1
HTTP_METHODS = RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC4791 + RFC5789
- 1
HTTP_METHOD_LOOKUP = {}
# Populate the HTTP method lookup cache.
- 1
HTTP_METHODS.each { |method|
- 31
HTTP_METHOD_LOOKUP[method] = method.underscore.to_sym
}
# Returns the HTTP \method that the application should see.
# In the case where the \method was overridden by a middleware
# (for instance, if a HEAD request was converted to a GET,
# or if a _method parameter was used to determine the \method
# the application should use), this \method returns the overridden
# value, not the original.
- 1
def request_method
@request_method ||= check_method(super)
end
- 1
def routes # :nodoc:
get_header("action_dispatch.routes")
end
- 1
def routes=(routes) # :nodoc:
set_header("action_dispatch.routes", routes)
end
- 1
def engine_script_name(_routes) # :nodoc:
get_header(_routes.env_key)
end
- 1
def engine_script_name=(name) # :nodoc:
set_header(routes.env_key, name.dup)
end
- 1
def request_method=(request_method) #:nodoc:
if check_method(request_method)
@request_method = set_header("REQUEST_METHOD", request_method)
end
end
- 1
def controller_instance # :nodoc:
get_header("action_controller.instance")
end
- 1
def controller_instance=(controller) # :nodoc:
set_header("action_controller.instance", controller)
end
- 1
def http_auth_salt
get_header "action_dispatch.http_auth_salt"
end
- 1
def show_exceptions? # :nodoc:
# We're treating `nil` as "unset", and we want the default setting to be
# `true`. This logic should be extracted to `env_config` and calculated
# once.
!(get_header("action_dispatch.show_exceptions") == false)
end
# Returns a symbol form of the #request_method.
- 1
def request_method_symbol
HTTP_METHOD_LOOKUP[request_method]
end
# Returns the original value of the environment's REQUEST_METHOD,
# even if it was overridden by middleware. See #request_method for
# more information.
- 1
def method
@method ||= check_method(get_header("rack.methodoverride.original_method") || get_header("REQUEST_METHOD"))
end
# Returns a symbol form of the #method.
- 1
def method_symbol
HTTP_METHOD_LOOKUP[method]
end
# Provides access to the request's HTTP headers, for example:
#
# request.headers["Content-Type"] # => "text/plain"
- 1
def headers
@headers ||= Http::Headers.new(self)
end
# Early Hints is an HTTP/2 status code that indicates hints to help a client start
# making preparations for processing the final response.
#
# 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:
#
# 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
# Early Hints headers are included by default if supported.
- 1
def send_early_hints(links)
return unless env["rack.early_hints"]
env["rack.early_hints"].call(links)
end
# Returns a +String+ with the last requested path including their params.
#
# # get '/foo'
# request.original_fullpath # => '/foo'
#
# # get '/foo?bar'
# request.original_fullpath # => '/foo?bar'
- 1
def original_fullpath
@original_fullpath ||= (get_header("ORIGINAL_FULLPATH") || fullpath)
end
# Returns the +String+ full path including params of the last URL requested.
#
# # get "/articles"
# request.fullpath # => "/articles"
#
# # get "/articles?page=2"
# request.fullpath # => "/articles?page=2"
- 1
def fullpath
@fullpath ||= super
end
# Returns the original request URL as a +String+.
#
# # get "/articles?page=2"
# request.original_url # => "http://www.example.com/articles?page=2"
- 1
def original_url
base_url + original_fullpath
end
# The +String+ MIME type of the request.
#
# # get "/articles"
# request.media_type # => "application/x-www-form-urlencoded"
- 1
def media_type
content_mime_type.to_s
end
# Returns the content length of the request as an integer.
- 1
def content_length
super.to_i
end
# Returns true if the "X-Requested-With" header contains "XMLHttpRequest"
# (case-insensitive), which may need to be manually added depending on the
# choice of JavaScript libraries and frameworks.
- 1
def xml_http_request?
/XMLHttpRequest/i.match?(get_header("HTTP_X_REQUESTED_WITH"))
end
- 1
alias :xhr? :xml_http_request?
# Returns the IP address of client as a +String+.
- 1
def ip
@ip ||= super
end
# Returns the IP address of client as a +String+,
# usually set by the RemoteIp middleware.
- 1
def remote_ip
@remote_ip ||= (get_header("action_dispatch.remote_ip") || ip).to_s
end
- 1
def remote_ip=(remote_ip)
@remote_ip = nil
set_header "action_dispatch.remote_ip", remote_ip
end
- 1
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
# be generated by a firewall, load balancer, or web server or 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 relies on the Rack variable set by the ActionDispatch::RequestId middleware.
- 1
def request_id
get_header ACTION_DISPATCH_REQUEST_ID
end
- 1
def request_id=(id) # :nodoc:
set_header ACTION_DISPATCH_REQUEST_ID, id
end
- 1
alias_method :uuid, :request_id
# Returns the lowercase name of the HTTP server software.
- 1
def server_software
(get_header("SERVER_SOFTWARE") && /^([a-zA-Z]+)/ =~ get_header("SERVER_SOFTWARE")) ? $1.downcase : nil
end
# Read the request \body. This is useful for web services that need to
# work with raw requests directly.
- 1
def raw_post
unless has_header? "RAW_POST_DATA"
raw_post_body = body
set_header("RAW_POST_DATA", raw_post_body.read(content_length))
raw_post_body.rewind if raw_post_body.respond_to?(:rewind)
end
get_header "RAW_POST_DATA"
end
# The request body is an IO input stream. If the RAW_POST_DATA environment
# variable is already set, wrap it in a StringIO.
- 1
def body
if raw_post = get_header("RAW_POST_DATA")
raw_post = (+raw_post).force_encoding(Encoding::BINARY)
StringIO.new(raw_post)
else
body_stream
end
end
# Determine whether the request body contains form-data by checking
# the request Content-Type for one of the media-types:
# "application/x-www-form-urlencoded" or "multipart/form-data". The
# list of form-data media types can be modified through the
# +FORM_DATA_MEDIA_TYPES+ array.
#
# A request body is not assumed to contain form-data when no
# Content-Type header is provided and the request_method is POST.
- 1
def form_data?
FORM_DATA_MEDIA_TYPES.include?(media_type)
end
- 1
def body_stream #:nodoc:
get_header("rack.input")
end
# TODO This should be broken apart into AD::Request::Session and probably
# be included by the session middleware.
- 1
def reset_session
if session && session.respond_to?(:destroy)
session.destroy
else
self.session = {}
end
end
- 1
def session=(session) #:nodoc:
Session.set self, session
end
- 1
def session_options=(options)
Session::Options.set self, options
end
# Override Rack's GET method to support indifferent access.
- 1
def GET
fetch_header("action_dispatch.request.query_parameters") do |k|
rack_query_params = super || {}
# Check for non UTF-8 parameter values, which would cause errors later
Request::Utils.check_param_encoding(rack_query_params)
set_header k, Request::Utils.normalize_encode_params(rack_query_params)
end
rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e
raise ActionController::BadRequest.new("Invalid query parameters: #{e.message}")
end
- 1
alias :query_parameters :GET
# Override Rack's POST method to support indifferent access.
- 1
def POST
fetch_header("action_dispatch.request.request_parameters") do
pr = parse_formatted_parameters(params_parsers) do |params|
super || {}
end
self.request_parameters = Request::Utils.normalize_encode_params(pr)
end
rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e
raise ActionController::BadRequest.new("Invalid request parameters: #{e.message}")
end
- 1
alias :request_parameters :POST
# Returns the authorization header regardless of whether it was specified directly or through one of the
# proxy alternatives.
- 1
def authorization
get_header("HTTP_AUTHORIZATION") ||
get_header("X-HTTP_AUTHORIZATION") ||
get_header("X_HTTP_AUTHORIZATION") ||
get_header("REDIRECT_X_HTTP_AUTHORIZATION")
end
# True if the request came from localhost, 127.0.0.1, or ::1.
- 1
def local?
LOCALHOST.match?(remote_addr) && LOCALHOST.match?(remote_ip)
end
- 1
def request_parameters=(params)
raise if params.nil?
set_header("action_dispatch.request.request_parameters", params)
end
- 1
def logger
get_header("action_dispatch.logger")
end
- 1
def commit_flash
end
- 1
def ssl?
super || scheme == "wss"
end
- 1
private
- 1
def check_method(name)
HTTP_METHOD_LOOKUP[name] || raise(ActionController::UnknownHttpMethod, "#{name}, accepted HTTP methods are #{HTTP_METHODS[0...-1].join(', ')}, and #{HTTP_METHODS[-1]}")
name
end
end
end
- 1
ActiveSupport.run_load_hooks :action_dispatch_request, ActionDispatch::Request
# frozen_string_literal: true
- 1
require "active_support/core_ext/module/attribute_accessors"
- 1
require "action_dispatch/http/filter_redirect"
- 1
require "action_dispatch/http/cache"
- 1
require "monitor"
- 1
module ActionDispatch # :nodoc:
# 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
# either represent a real HTTP response (i.e. one that is meant to be sent
# back to the web browser) or a TestResponse (i.e. one that is generated
# from integration tests).
#
# \Response is mostly a Ruby on \Rails framework implementation detail, and
# should never be used directly in controllers. Controllers should use the
# methods defined in ActionController::Base instead. For example, if you want
# to set the HTTP response's content MIME type, then use
# ActionControllerBase#headers instead of Response#headers.
#
# Nevertheless, integration tests may want to inspect controller responses in
# more detail, and that's when \Response can be useful for application
# developers. Integration test methods such as
# ActionDispatch::Integration::Session#get and
# ActionDispatch::Integration::Session#post return objects of type
# TestResponse (which are of course also of type \Response).
#
# For example, the following demo integration test prints the body of the
# controller response to the console:
#
# class DemoControllerTest < ActionDispatch::IntegrationTest
# def test_print_root_path_to_console
# get('/')
# puts response.body
# end
# end
- 1
class Response
- 1
class Header < DelegateClass(Hash) # :nodoc:
- 1
def initialize(response, header)
@response = response
super(header)
end
- 1
def []=(k, v)
if @response.sending? || @response.sent?
raise ActionDispatch::IllegalStateError, "header already sent"
end
super
end
- 1
def merge(other)
self.class.new @response, __getobj__.merge(other)
end
- 1
def to_hash
__getobj__.dup
end
end
# The request that the response is responding to.
- 1
attr_accessor :request
# The HTTP status code.
- 1
attr_reader :status
# Get headers for this response.
- 1
attr_reader :header
- 1
alias_method :headers, :header
- 1
delegate :[], :[]=, to: :@header
- 1
def each(&block)
sending!
x = @stream.each(&block)
sent!
x
end
- 1
CONTENT_TYPE = "Content-Type"
- 1
SET_COOKIE = "Set-Cookie"
- 1
LOCATION = "Location"
- 1
NO_CONTENT_CODES = [100, 101, 102, 103, 204, 205, 304]
- 1
cattr_accessor :default_charset, default: "utf-8"
- 1
cattr_accessor :default_headers
- 1
cattr_accessor :return_only_media_type_on_content_type, default: false
- 1
include Rack::Response::Helpers
# Aliasing these off because AD::Http::Cache::Response defines them.
- 1
alias :_cache_control :cache_control
- 1
alias :_cache_control= :cache_control=
- 1
include ActionDispatch::Http::FilterRedirect
- 1
include ActionDispatch::Http::Cache::Response
- 1
include MonitorMixin
- 1
class Buffer # :nodoc:
- 1
def initialize(response, buf)
@response = response
@buf = buf
@closed = false
@str_body = nil
end
- 1
def body
@str_body ||= begin
buf = +""
each { |chunk| buf << chunk }
buf
end
end
- 1
def write(string)
raise IOError, "closed stream" if closed?
@str_body = nil
@response.commit!
@buf.push string
end
- 1
def each(&block)
if @str_body
return enum_for(:each) unless block_given?
yield @str_body
else
each_chunk(&block)
end
end
- 1
def abort
end
- 1
def close
@response.commit!
@closed = true
end
- 1
def closed?
@closed
end
- 1
private
- 1
def each_chunk(&block)
@buf.each(&block)
end
end
- 1
def self.create(status = 200, header = {}, body = [], default_headers: self.default_headers)
header = merge_default_headers(header, default_headers)
new status, header, body
end
- 1
def self.merge_default_headers(original, default)
default.respond_to?(:merge) ? default.merge(original) : original
end
# The underlying body, as a streamable object.
- 1
attr_reader :stream
- 1
def initialize(status = 200, header = {}, body = [])
super()
@header = Header.new(self, header)
self.body, self.status = body, status
@cv = new_cond
@committed = false
@sending = false
@sent = false
prepare_cache_control!
yield self if block_given?
end
- 1
def has_header?(key); headers.key? key; end
- 1
def get_header(key); headers[key]; end
- 1
def set_header(key, v); headers[key] = v; end
- 1
def delete_header(key); headers.delete key; end
- 1
def await_commit
synchronize do
@cv.wait_until { @committed }
end
end
- 1
def await_sent
synchronize { @cv.wait_until { @sent } }
end
- 1
def commit!
synchronize do
before_committed
@committed = true
@cv.broadcast
end
end
- 1
def sending!
synchronize do
before_sending
@sending = true
@cv.broadcast
end
end
- 1
def sent!
synchronize do
@sent = true
@cv.broadcast
end
end
- 1
def sending?; synchronize { @sending }; end
- 1
def committed?; synchronize { @committed }; end
- 1
def sent?; synchronize { @sent }; end
# Sets the HTTP status code.
- 1
def status=(status)
@status = Rack::Utils.status_code(status)
end
# Sets the HTTP response's content MIME type. For example, in the controller
# you could write this:
#
# response.content_type = "text/plain"
#
# If a character set has been defined for this response (see charset=) then
# the character set information will also be included in the content type
# information.
- 1
def content_type=(content_type)
return unless content_type
new_header_info = parse_content_type(content_type.to_s)
prev_header_info = parsed_content_type_header
charset = new_header_info.charset || prev_header_info.charset
charset ||= self.class.default_charset unless prev_header_info.mime_type
set_content_type new_header_info.mime_type, charset
end
# Content type of response.
- 1
def content_type
if self.class.return_only_media_type_on_content_type
ActiveSupport::Deprecation.warn(
"Rails 6.1 will return Content-Type header without modification." \
" If you want just the MIME type, please use `#media_type` instead."
)
content_type = super
content_type ? content_type.split(/;\s*charset=/)[0].presence : content_type
else
super.presence
end
end
# Media type of response.
- 1
def media_type
parsed_content_type_header.mime_type
end
- 1
def sending_file=(v)
if true == v
self.charset = false
end
end
# Sets the HTTP character set. In case of +nil+ parameter
# it sets the charset to +default_charset+.
#
# response.charset = 'utf-16' # => 'utf-16'
# response.charset = nil # => 'utf-8'
- 1
def charset=(charset)
content_type = parsed_content_type_header.mime_type
if false == charset
set_content_type content_type, nil
else
set_content_type content_type, charset || self.class.default_charset
end
end
# The charset of the response. HTML wants to know the encoding of the
# content you're giving them, so we need to send that along.
- 1
def charset
header_info = parsed_content_type_header
header_info.charset || self.class.default_charset
end
# The response code of the request.
- 1
def response_code
@status
end
# Returns a string to ensure compatibility with <tt>Net::HTTPResponse</tt>.
- 1
def code
@status.to_s
end
# Returns the corresponding message for the current HTTP status code:
#
# response.status = 200
# response.message # => "OK"
#
# response.status = 404
# response.message # => "Not Found"
#
- 1
def message
Rack::Utils::HTTP_STATUS_CODES[@status]
end
- 1
alias_method :status_message, :message
# Returns the content of the response as a string. This contains the contents
# of any calls to <tt>render</tt>.
- 1
def body
@stream.body
end
- 1
def write(string)
@stream.write string
end
# Allows you to manually set or override the response body.
- 1
def body=(body)
if body.respond_to?(:to_path)
@stream = body
else
synchronize do
@stream = build_buffer self, munge_body_object(body)
end
end
end
# Avoid having to pass an open file handle as the response body.
# Rack::Sendfile will usually intercept the response and uses
# the path directly, so there is no reason to open the file.
- 1
class FileBody #:nodoc:
- 1
attr_reader :to_path
- 1
def initialize(path)
@to_path = path
end
- 1
def body
File.binread(to_path)
end
# Stream the file's contents if Rack::Sendfile isn't present.
- 1
def each
File.open(to_path, "rb") do |file|
while chunk = file.read(16384)
yield chunk
end
end
end
end
# Send the file stored at +path+ as the response body.
- 1
def send_file(path)
commit!
@stream = FileBody.new(path)
end
- 1
def reset_body!
@stream = build_buffer(self, [])
end
- 1
def body_parts
parts = []
@stream.each { |x| parts << x }
parts
end
# The location header we'll be responding with.
- 1
alias_method :redirect_url, :location
- 1
def close
stream.close if stream.respond_to?(:close)
end
- 1
def abort
if stream.respond_to?(:abort)
stream.abort
elsif stream.respond_to?(:close)
# `stream.close` should really be reserved for a close from the
# other direction, but we must fall back to it for
# compatibility.
stream.close
end
end
# Turns the Response into a Rack-compatible array of the status, headers,
# and body. Allows explicit splatting:
#
# status, headers, body = *response
- 1
def to_a
commit!
rack_response @status, @header.to_hash
end
- 1
alias prepare! to_a
# Returns the response cookies, converted to a Hash of (name => value) pairs
#
# assert_equal 'AuthorOfNewPage', r.cookies['author']
- 1
def cookies
cookies = {}
if header = get_header(SET_COOKIE)
header = header.split("\n") if header.respond_to?(:to_str)
header.each do |cookie|
if pair = cookie.split(";").first
key, value = pair.split("=").map { |v| Rack::Utils.unescape(v) }
cookies[key] = value
end
end
end
cookies
end
- 1
private
- 1
ContentTypeHeader = Struct.new :mime_type, :charset
- 1
NullContentTypeHeader = ContentTypeHeader.new nil, nil
- 1
CONTENT_TYPE_PARSER = /
\A
(?<mime_type>[^;\s]+\s*(?:;\s*(?:(?!charset)[^;\s])+)*)?
(?:;\s*charset=(?<quote>"?)(?<charset>[^;\s]+)\k<quote>)?
/x # :nodoc:
- 1
def parse_content_type(content_type)
if content_type && match = CONTENT_TYPE_PARSER.match(content_type)
ContentTypeHeader.new(match[:mime_type], match[:charset])
else
NullContentTypeHeader
end
end
# Small internal convenience method to get the parsed version of the current
# content type header.
- 1
def parsed_content_type_header
parse_content_type(get_header(CONTENT_TYPE))
end
- 1
def set_content_type(content_type, charset)
type = content_type || ""
type = "#{type}; charset=#{charset.to_s.downcase}" if charset
set_header CONTENT_TYPE, type
end
- 1
def before_committed
return if committed?
assign_default_content_type_and_charset!
merge_and_normalize_cache_control!(@cache_control)
handle_conditional_get!
handle_no_content!
end
- 1
def before_sending
# Normally we've already committed by now, but it's possible
# (e.g., if the controller action tries to read back its own
# response) to get here before that. In that case, we must force
# an "early" commit: we're about to freeze the headers, so this is
# our last chance.
commit! unless committed?
headers.freeze
request.commit_cookie_jar! unless committed?
end
- 1
def build_buffer(response, body)
Buffer.new response, body
end
- 1
def munge_body_object(body)
body.respond_to?(:each) ? body : [body]
end
- 1
def assign_default_content_type_and_charset!
return if media_type
ct = parsed_content_type_header
set_content_type(ct.mime_type || Mime[:html].to_s,
ct.charset || self.class.default_charset)
end
- 1
class RackBody
- 1
def initialize(response)
@response = response
end
- 1
def each(*args, &block)
@response.each(*args, &block)
end
- 1
def close
# Rack "close" maps to Response#abort, and *not* Response#close
# (which is used when the controller's finished writing)
@response.abort
end
- 1
def body
@response.body
end
- 1
def respond_to?(method, include_private = false)
if method.to_sym == :to_path
@response.stream.respond_to?(method)
else
super
end
end
- 1
def to_path
@response.stream.to_path
end
- 1
def to_ary
nil
end
end
- 1
def handle_no_content!
if NO_CONTENT_CODES.include?(@status)
@header.delete CONTENT_TYPE
@header.delete "Content-Length"
end
end
- 1
def rack_response(status, header)
if NO_CONTENT_CODES.include?(status)
[status, header, []]
else
[status, header, RackBody.new(self)]
end
end
end
- 1
ActiveSupport.run_load_hooks(:action_dispatch_response, Response)
end
# frozen_string_literal: true
- 1
module ActionDispatch
- 1
module Http
# Models uploaded files.
#
# The actual file is accessible via the +tempfile+ accessor, though some
# of its interface is available directly for convenience.
#
# Uploaded files are temporary files whose lifespan is one request. When
# the object is finalized Ruby unlinks the file, so there is no need to
# clean them with a separate maintenance task.
- 1
class UploadedFile
# The basename of the file in the client.
- 1
attr_accessor :original_filename
# A string with the MIME type of the file.
- 1
attr_accessor :content_type
# A +Tempfile+ object with the actual uploaded file. Note that some of
# its interface is available directly.
- 1
attr_accessor :tempfile
# A string with the headers of the multipart request.
- 1
attr_accessor :headers
- 1
def initialize(hash) # :nodoc:
@tempfile = hash[:tempfile]
raise(ArgumentError, ":tempfile is required") unless @tempfile
if hash[:filename]
@original_filename = hash[:filename].dup
begin
@original_filename.encode!(Encoding::UTF_8)
rescue EncodingError
@original_filename.force_encoding(Encoding::UTF_8)
end
else
@original_filename = nil
end
@content_type = hash[:type]
@headers = hash[:head]
end
# Shortcut for +tempfile.read+.
- 1
def read(length = nil, buffer = nil)
@tempfile.read(length, buffer)
end
# Shortcut for +tempfile.open+.
- 1
def open
@tempfile.open
end
# Shortcut for +tempfile.close+.
- 1
def close(unlink_now = false)
@tempfile.close(unlink_now)
end
# Shortcut for +tempfile.path+.
- 1
def path
@tempfile.path
end
# Shortcut for +tempfile.to_path+.
- 1
def to_path
@tempfile.to_path
end
# Shortcut for +tempfile.rewind+.
- 1
def rewind
@tempfile.rewind
end
# Shortcut for +tempfile.size+.
- 1
def size
@tempfile.size
end
# Shortcut for +tempfile.eof?+.
- 1
def eof?
@tempfile.eof?
end
- 1
def to_io
@tempfile.to_io
end
end
end
end
# frozen_string_literal: true
- 1
require "active_support/core_ext/module/attribute_accessors"
- 1
module ActionDispatch
- 1
module Http
- 1
module URL
- 1
IP_HOST_REGEXP = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/
- 1
HOST_REGEXP = /(^[^:]+:\/\/)?(\[[^\]]+\]|[^:]+)(?::(\d+$))?/
- 1
PROTOCOL_REGEXP = /^([^:]+)(:)?(\/\/)?$/
- 1
mattr_accessor :secure_protocol, default: false
- 1
mattr_accessor :tld_length, default: 1
- 1
class << self
# Returns the domain part of a host given the domain level.
#
# # Top-level domain example
# extract_domain('www.example.com', 1) # => "example.com"
# # Second-level domain example
# extract_domain('dev.www.example.co.uk', 2) # => "example.co.uk"
- 1
def extract_domain(host, tld_length)
extract_domain_from(host, tld_length) if named_host?(host)
end
# Returns the subdomains of a host as an Array given the domain level.
#
# # Top-level domain example
# extract_subdomains('www.example.com', 1) # => ["www"]
# # Second-level domain example
# extract_subdomains('dev.www.example.co.uk', 2) # => ["dev", "www"]
- 1
def extract_subdomains(host, tld_length)
if named_host?(host)
extract_subdomains_from(host, tld_length)
else
[]
end
end
# Returns the subdomains of a host as a String given the domain level.
#
# # Top-level domain example
# extract_subdomain('www.example.com', 1) # => "www"
# # Second-level domain example
# extract_subdomain('dev.www.example.co.uk', 2) # => "dev.www"
- 1
def extract_subdomain(host, tld_length)
extract_subdomains(host, tld_length).join(".")
end
- 1
def url_for(options)
if options[:only_path]
path_for options
else
full_url_for options
end
end
- 1
def full_url_for(options)
host = options[:host]
protocol = options[:protocol]
port = options[:port]
unless host
raise ArgumentError, "Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true"
end
build_host_url(host, port, protocol, options, path_for(options))
end
- 1
def path_for(options)
path = options[:script_name].to_s.chomp("/")
path << options[:path] if options.key?(:path)
add_trailing_slash(path) if options[:trailing_slash]
add_params(path, options[:params]) if options.key?(:params)
add_anchor(path, options[:anchor]) if options.key?(:anchor)
path
end
- 1
private
- 1
def add_params(path, params)
params = { params: params } unless params.is_a?(Hash)
params.reject! { |_, v| v.to_param.nil? }
query = params.to_query
path << "?#{query}" unless query.empty?
end
- 1
def add_anchor(path, anchor)
if anchor
path << "##{Journey::Router::Utils.escape_fragment(anchor.to_param)}"
end
end
- 1
def extract_domain_from(host, tld_length)
host.split(".").last(1 + tld_length).join(".")
end
- 1
def extract_subdomains_from(host, tld_length)
parts = host.split(".")
parts[0..-(tld_length + 2)]
end
- 1
def add_trailing_slash(path)
if path.include?("?")
path.sub!(/\?/, '/\&')
elsif !path.include?(".")
path.sub!(/[^\/]\z|\A\z/, '\&/')
end
end
- 1
def build_host_url(host, port, protocol, options, path)
if match = host.match(HOST_REGEXP)
protocol ||= match[1] unless protocol == false
host = match[2]
port = match[3] unless options.key? :port
end
protocol = normalize_protocol protocol
host = normalize_host(host, options)
result = protocol.dup
if options[:user] && options[:password]
result << "#{Rack::Utils.escape(options[:user])}:#{Rack::Utils.escape(options[:password])}@"
end
result << host
normalize_port(port, protocol) { |normalized_port|
result << ":#{normalized_port}"
}
result.concat path
end
- 1
def named_host?(host)
!IP_HOST_REGEXP.match?(host)
end
- 1
def normalize_protocol(protocol)
case protocol
when nil
secure_protocol ? "https://" : "http://"
when false, "//"
"//"
when PROTOCOL_REGEXP
"#{$1}://"
else
raise ArgumentError, "Invalid :protocol option: #{protocol.inspect}"
end
end
- 1
def normalize_host(_host, options)
return _host unless named_host?(_host)
tld_length = options[:tld_length] || @@tld_length
subdomain = options.fetch :subdomain, true
domain = options[:domain]
host = +""
if subdomain == true
return _host if domain.nil?
host << extract_subdomains_from(_host, tld_length).join(".")
elsif subdomain
host << subdomain.to_param
end
host << "." unless host.empty?
host << (domain || extract_domain_from(_host, tld_length))
host
end
- 1
def normalize_port(port, protocol)
return unless port
case protocol
when "//" then yield port
when "https://"
yield port unless port.to_i == 443
else
yield port unless port.to_i == 80
end
end
end
- 1
def initialize
super
@protocol = nil
@port = nil
end
# Returns the complete URL used for this request.
#
# req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com'
# req.url # => "http://example.com"
- 1
def url
protocol + host_with_port + fullpath
end
# Returns 'https://' if this is an SSL request and 'http://' otherwise.
#
# req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com'
# req.protocol # => "http://"
#
# req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com', 'HTTPS' => 'on'
# req.protocol # => "https://"
- 1
def protocol
@protocol ||= ssl? ? "https://" : "http://"
end
# Returns the \host and port for this request, such as "example.com:8080".
#
# req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com'
# req.raw_host_with_port # => "example.com"
#
# req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:80'
# req.raw_host_with_port # => "example.com:80"
#
# req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080'
# req.raw_host_with_port # => "example.com:8080"
- 1
def raw_host_with_port
if forwarded = x_forwarded_host.presence
forwarded.split(/,\s?/).last
else
get_header("HTTP_HOST") || "#{server_name || server_addr}:#{get_header('SERVER_PORT')}"
end
end
# Returns the host for this request, such as "example.com".
#
# req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080'
# req.host # => "example.com"
- 1
def host
raw_host_with_port.sub(/:\d+$/, "")
end
# 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
# (80 or 443)
#
# req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com'
# req.host_with_port # => "example.com"
#
# req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:80'
# req.host_with_port # => "example.com"
#
# req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080'
# req.host_with_port # => "example.com:8080"
- 1
def host_with_port
"#{host}#{port_string}"
end
# Returns the port number of this request as an integer.
#
# req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com'
# req.port # => 80
#
# req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080'
# req.port # => 8080
- 1
def port
@port ||= begin
if raw_host_with_port =~ /:(\d+)$/
$1.to_i
else
standard_port
end
end
end
# Returns the standard \port number for this request's protocol.
#
# req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080'
# req.standard_port # => 80
- 1
def standard_port
case protocol
when "https://" then 443
else 80
end
end
# Returns whether this request is using the standard port
#
# req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:80'
# req.standard_port? # => true
#
# req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080'
# req.standard_port? # => false
- 1
def standard_port?
port == standard_port
end
# Returns a number \port suffix like 8080 if the \port number of this request
# is not the default HTTP \port 80 or HTTPS \port 443.
#
# req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:80'
# req.optional_port # => nil
#
# req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080'
# req.optional_port # => 8080
- 1
def optional_port
standard_port? ? nil : port
end
# Returns a string \port suffix, including colon, like ":8080" if the \port
# number of this request is not the default HTTP \port 80 or HTTPS \port 443.
#
# req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:80'
# req.port_string # => ""
#
# req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080'
# req.port_string # => ":8080"
- 1
def port_string
standard_port? ? "" : ":#{port}"
end
# Returns the requested port, such as 8080, based on SERVER_PORT
#
# req = ActionDispatch::Request.new 'SERVER_PORT' => '80'
# req.server_port # => 80
#
# req = ActionDispatch::Request.new 'SERVER_PORT' => '8080'
# req.server_port # => 8080
- 1
def server_port
get_header("SERVER_PORT").to_i
end
# Returns the \domain part of a \host, such as "rubyonrails.org" in "www.rubyonrails.org". You can specify
# a different <tt>tld_length</tt>, such as 2 to catch rubyonrails.co.uk in "www.rubyonrails.co.uk".
- 1
def domain(tld_length = @@tld_length)
ActionDispatch::Http::URL.extract_domain(host, tld_length)
end
# Returns all the \subdomains as an array, so <tt>["dev", "www"]</tt> would be
# returned for "dev.www.rubyonrails.org". You can specify a different <tt>tld_length</tt>,
# such as 2 to catch <tt>["www"]</tt> instead of <tt>["www", "rubyonrails"]</tt>
# in "www.rubyonrails.co.uk".
- 1
def subdomains(tld_length = @@tld_length)
ActionDispatch::Http::URL.extract_subdomains(host, tld_length)
end
# Returns all the \subdomains as a string, so <tt>"dev.www"</tt> would be
# returned for "dev.www.rubyonrails.org". You can specify a different <tt>tld_length</tt>,
# such as 2 to catch <tt>"www"</tt> instead of <tt>"www.rubyonrails"</tt>
# in "www.rubyonrails.co.uk".
- 1
def subdomain(tld_length = @@tld_length)
ActionDispatch::Http::URL.extract_subdomain(host, tld_length)
end
end
end
end
# frozen_string_literal: true
- 1
require "action_dispatch/journey/router"
- 1
require "action_dispatch/journey/gtg/builder"
- 1
require "action_dispatch/journey/gtg/simulator"
# frozen_string_literal: true
- 1
require "action_controller/metal/exceptions"
- 1
module ActionDispatch
# :stopdoc:
- 1
module Journey
# The Formatter class is used for formatting URLs. For example, parameters
# passed to +url_for+ in Rails will eventually call Formatter#generate.
- 1
class Formatter
- 1
attr_reader :routes
- 1
def initialize(routes)
- 56
@routes = routes
- 56
@cache = nil
end
- 1
class RouteWithParams
- 1
attr_reader :params
- 1
def initialize(route, parameterized_parts, params)
@route = route
@parameterized_parts = parameterized_parts
@params = params
end
- 1
def path(_)
@route.format(@parameterized_parts)
end
end
- 1
class MissingRoute
- 1
attr_reader :routes, :name, :constraints, :missing_keys, :unmatched_keys
- 1
def initialize(constraints, missing_keys, unmatched_keys, routes, name)
@constraints = constraints
@missing_keys = missing_keys
@unmatched_keys = unmatched_keys
@routes = routes
@name = name
end
- 1
def path(method_name)
raise ActionController::UrlGenerationError.new(message, routes, name, method_name)
end
- 1
def params
path("unknown")
end
- 1
def message
message = +"No route matches #{Hash[constraints.sort_by { |k, v| k.to_s }].inspect}"
message << ", missing required keys: #{missing_keys.sort.inspect}" if missing_keys && !missing_keys.empty?
message << ", possible unmatched constraints: #{unmatched_keys.sort.inspect}" if unmatched_keys && !unmatched_keys.empty?
message
end
end
- 1
def generate(name, options, path_parameters)
constraints = path_parameters.merge(options)
missing_keys = nil
match_route(name, constraints) do |route|
parameterized_parts = extract_parameterized_parts(route, options, path_parameters)
# Skip this route unless a name has been provided or it is a
# standard Rails route since we can't determine whether an options
# hash passed to url_for matches a Rack application or a redirect.
next unless name || route.dispatcher?
missing_keys = missing_keys(route, parameterized_parts)
next if missing_keys && !missing_keys.empty?
params = options.dup.delete_if do |key, _|
parameterized_parts.key?(key) || route.defaults.key?(key)
end
defaults = route.defaults
required_parts = route.required_parts
route.parts.reverse_each do |key|
break if defaults[key].nil? && parameterized_parts[key].present?
next if parameterized_parts[key].to_s != defaults[key].to_s
break if required_parts.include?(key)
parameterized_parts.delete(key)
end
return RouteWithParams.new(route, parameterized_parts, params)
end
unmatched_keys = (missing_keys || []) & constraints.keys
missing_keys = (missing_keys || []) - unmatched_keys
MissingRoute.new(constraints, missing_keys, unmatched_keys, routes, name)
end
- 1
def clear
- 54
@cache = nil
end
- 1
private
- 1
def extract_parameterized_parts(route, options, recall)
parameterized_parts = recall.merge(options)
keys_to_keep = route.parts.reverse_each.drop_while { |part|
!(options.key?(part) || route.scope_options.key?(part)) || (options[part] || recall[part]).nil?
} | route.required_parts
parameterized_parts.delete_if do |bad_key, _|
!keys_to_keep.include?(bad_key)
end
parameterized_parts.each do |k, v|
if k == :controller
parameterized_parts[k] = v
else
parameterized_parts[k] = v.to_param
end
end
parameterized_parts.keep_if { |_, v| v }
parameterized_parts
end
- 1
def named_routes
routes.named_routes
end
- 1
def match_route(name, options)
if named_routes.key?(name)
yield named_routes[name]
else
routes = non_recursive(cache, options)
supplied_keys = options.each_with_object({}) do |(k, v), h|
h[k.to_s] = true if v
end
hash = routes.group_by { |_, r| r.score(supplied_keys) }
hash.keys.sort.reverse_each do |score|
break if score < 0
hash[score].sort_by { |i, _| i }.each do |_, route|
yield route
end
end
end
end
- 1
def non_recursive(cache, options)
routes = []
queue = [cache]
while queue.any?
c = queue.shift
routes.concat(c[:___routes]) if c.key?(:___routes)
options.each do |pair|
queue << c[pair] if c.key?(pair)
end
end
routes
end
# Returns an array populated with missing keys if any are present.
- 1
def missing_keys(route, parts)
missing_keys = nil
tests = route.path.requirements_for_missing_keys_check
route.required_parts.each { |key|
case tests[key]
when nil
unless parts[key]
missing_keys ||= []
missing_keys << key
end
else
unless tests[key].match?(parts[key])
missing_keys ||= []
missing_keys << key
end
end
}
missing_keys
end
- 1
def possibles(cache, options, depth = 0)
cache.fetch(:___routes) { [] } + options.find_all { |pair|
cache.key?(pair)
}.flat_map { |pair|
possibles(cache[pair], options, depth + 1)
}
end
- 1
def build_cache
root = { ___routes: [] }
routes.routes.each_with_index do |route, i|
leaf = route.required_defaults.inject(root) do |h, tuple|
h[tuple] ||= {}
end
(leaf[:___routes] ||= []) << [i, route]
end
root
end
- 1
def cache
@cache ||= build_cache
end
end
end
# :startdoc:
end
# frozen_string_literal: true
- 1
require "action_dispatch/journey/gtg/transition_table"
- 1
module ActionDispatch
- 1
module Journey # :nodoc:
- 1
module GTG # :nodoc:
- 1
class Builder # :nodoc:
- 1
DUMMY = Nodes::Dummy.new
- 1
attr_reader :root, :ast, :endpoints
- 1
def initialize(root)
@root = root
@ast = Nodes::Cat.new root, DUMMY
@followpos = build_followpos
end
- 1
def transition_table
dtrans = TransitionTable.new
marked = {}
state_id = Hash.new { |h, k| h[k] = h.length }
dstates = [firstpos(root)]
until dstates.empty?
s = dstates.shift
next if marked[s]
marked[s] = true # mark s
s.group_by { |state| symbol(state) }.each do |sym, ps|
u = ps.flat_map { |l| @followpos[l] }
next if u.empty?
from = state_id[s]
if u.all? { |pos| pos == DUMMY }
to = state_id[Object.new]
dtrans[from, to] = sym
dtrans.add_accepting(to)
ps.each { |state| dtrans.add_memo(to, state.memo) }
else
to = state_id[u]
dtrans[from, to] = sym
if u.include?(DUMMY)
ps.each do |state|
if @followpos[state].include?(DUMMY)
dtrans.add_memo(to, state.memo)
end
end
dtrans.add_accepting(to)
end
end
dstates << u
end
end
dtrans
end
- 1
def nullable?(node)
case node
when Nodes::Group
true
when Nodes::Star
true
when Nodes::Or
node.children.any? { |c| nullable?(c) }
when Nodes::Cat
nullable?(node.left) && nullable?(node.right)
when Nodes::Terminal
!node.left
when Nodes::Unary
nullable?(node.left)
else
raise ArgumentError, "unknown nullable: %s" % node.class.name
end
end
- 1
def firstpos(node)
case node
when Nodes::Star
firstpos(node.left)
when Nodes::Cat
if nullable?(node.left)
firstpos(node.left) | firstpos(node.right)
else
firstpos(node.left)
end
when Nodes::Or
node.children.flat_map { |c| firstpos(c) }.tap(&:uniq!)
when Nodes::Unary
firstpos(node.left)
when Nodes::Terminal
nullable?(node) ? [] : [node]
else
raise ArgumentError, "unknown firstpos: %s" % node.class.name
end
end
- 1
def lastpos(node)
case node
when Nodes::Star
firstpos(node.left)
when Nodes::Or
node.children.flat_map { |c| lastpos(c) }.tap(&:uniq!)
when Nodes::Cat
if nullable?(node.right)
lastpos(node.left) | lastpos(node.right)
else
lastpos(node.right)
end
when Nodes::Terminal
nullable?(node) ? [] : [node]
when Nodes::Unary
lastpos(node.left)
else
raise ArgumentError, "unknown lastpos: %s" % node.class.name
end
end
- 1
private
- 1
def build_followpos
table = Hash.new { |h, k| h[k] = [] }
@ast.each do |n|
case n
when Nodes::Cat
lastpos(n.left).each do |i|
table[i] += firstpos(n.right)
end
when Nodes::Star
lastpos(n).each do |i|
table[i] += firstpos(n)
end
end
end
table
end
- 1
def symbol(edge)
edge.symbol? ? edge.regexp : edge.left
end
end
end
end
end
# frozen_string_literal: true
- 1
require "strscan"
- 1
module ActionDispatch
- 1
module Journey # :nodoc:
- 1
module GTG # :nodoc:
- 1
class MatchData # :nodoc:
- 1
attr_reader :memos
- 1
def initialize(memos)
@memos = memos
end
end
- 1
class Simulator # :nodoc:
- 1
INITIAL_STATE = [0].freeze
- 1
attr_reader :tt
- 1
def initialize(transition_table)
@tt = transition_table
end
- 1
def memos(string)
input = StringScanner.new(string)
state = INITIAL_STATE
while sym = input.scan(%r([/.?]|[^/.?]+))
state = tt.move(state, sym)
end
acceptance_states = state.each_with_object([]) do |s, memos|
memos.concat(tt.memo(s)) if tt.accepting?(s)
end
acceptance_states.empty? ? yield : acceptance_states
end
end
end
end
end
# frozen_string_literal: true
- 1
require "action_dispatch/journey/nfa/dot"
- 1
module ActionDispatch
- 1
module Journey # :nodoc:
- 1
module GTG # :nodoc:
- 1
class TransitionTable # :nodoc:
- 1
include Journey::NFA::Dot
- 1
attr_reader :memos
- 1
def initialize
@regexp_states = {}
@string_states = {}
@accepting = {}
@memos = Hash.new { |h, k| h[k] = [] }
end
- 1
def add_accepting(state)
@accepting[state] = true
end
- 1
def accepting_states
@accepting.keys
end
- 1
def accepting?(state)
@accepting[state]
end
- 1
def add_memo(idx, memo)
@memos[idx] << memo
end
- 1
def memo(idx)
@memos[idx]
end
- 1
def eclosure(t)
Array(t)
end
- 1
def move(t, a)
return [] if t.empty?
regexps = []
strings = []
t.each { |s|
if states = @regexp_states[s]
states.each { |re, v| regexps << v if re.match?(a) && !v.nil? }
end
if states = @string_states[s]
strings << states[a] unless states[a].nil?
end
}
strings.concat regexps
end
- 1
def as_json(options = nil)
simple_regexp = Hash.new { |h, k| h[k] = {} }
@regexp_states.each do |from, hash|
hash.each do |re, to|
simple_regexp[from][re.source] = to
end
end
{
regexp_states: simple_regexp,
string_states: @string_states,
accepting: @accepting
}
end
- 1
def to_svg
svg = IO.popen("dot -Tsvg", "w+") { |f|
f.write(to_dot)
f.close_write
f.readlines
}
3.times { svg.shift }
svg.join.sub(/width="[^"]*"/, "").sub(/height="[^"]*"/, "")
end
- 1
def visualizer(paths, title = "FSM")
viz_dir = File.join __dir__, "..", "visualizer"
fsm_js = File.read File.join(viz_dir, "fsm.js")
fsm_css = File.read File.join(viz_dir, "fsm.css")
erb = File.read File.join(viz_dir, "index.html.erb")
states = "function tt() { return #{to_json}; }"
fun_routes = paths.sample(3).map do |ast|
ast.map { |n|
case n
when Nodes::Symbol
case n.left
when ":id" then rand(100).to_s
when ":format" then %w{ xml json }.sample
else
"omg"
end
when Nodes::Terminal then n.symbol
else
nil
end
}.compact.join
end
stylesheets = [fsm_css]
svg = to_svg
javascripts = [states, fsm_js]
fun_routes = fun_routes
stylesheets = stylesheets
svg = svg
javascripts = javascripts
require "erb"
template = ERB.new erb
template.result(binding)
end
- 1
def []=(from, to, sym)
to_mappings = states_hash_for(sym)[from] ||= {}
to_mappings[sym] = to
end
- 1
def states
ss = @string_states.keys + @string_states.values.flat_map(&:values)
rs = @regexp_states.keys + @regexp_states.values.flat_map(&:values)
(ss + rs).uniq
end
- 1
def transitions
@string_states.flat_map { |from, hash|
hash.map { |s, to| [from, s, to] }
} + @regexp_states.flat_map { |from, hash|
hash.map { |s, to| [from, s, to] }
}
end
- 1
private
- 1
def states_hash_for(sym)
case sym
when String
@string_states
when Regexp
@regexp_states
else
raise ArgumentError, "unknown symbol: %s" % sym.class
end
end
end
end
end
end
# frozen_string_literal: true
- 1
module ActionDispatch
- 1
module Journey # :nodoc:
- 1
module NFA # :nodoc:
- 1
module Dot # :nodoc:
- 1
def to_dot
edges = transitions.map { |from, sym, to|
" #{from} -> #{to} [label=\"#{sym || 'ε'}\"];"
}
<<-eodot
digraph nfa {
rankdir=LR;
node [shape = doublecircle];
#{accepting_states.join ' '};
node [shape = circle];
#{edges.join "\n"}
}
eodot
end
end
end
end
end
# frozen_string_literal: true
- 1
require "action_dispatch/journey/visitors"
- 1
module ActionDispatch
- 1
module Journey # :nodoc:
- 1
module Nodes # :nodoc:
- 1
class Node # :nodoc:
- 1
include Enumerable
- 1
attr_accessor :left, :memo
- 1
def initialize(left)
- 3828
@left = left
- 3828
@memo = nil
end
- 1
def each(&block)
- 2146
Visitors::Each::INSTANCE.accept(self, block)
end
- 1
def to_s
Visitors::String::INSTANCE.accept(self, "")
end
- 1
def to_dot
Visitors::Dot::INSTANCE.accept(self)
end
- 1
def to_sym
- 1520
name.to_sym
end
- 1
def name
-left.tr("*:", "")
end
- 1
def type
raise NotImplementedError
end
- 13901
def symbol?; false; end
- 2605
def literal?; false; end
- 1882
def terminal?; false; end
- 3247
def star?; false; end
- 2007
def cat?; false; end
- 1983
def group?; false; end
end
- 1
class Terminal < Node # :nodoc:
- 1
alias :symbol :left
- 1881
def terminal?; true; end
end
- 1
class Literal < Terminal # :nodoc:
- 869
def literal?; true; end
- 3189
def type; :LITERAL; end
end
- 1
class Dummy < Literal # :nodoc:
- 1
def initialize(x = Object.new)
- 1
super
end
- 1
def literal?; false; end
end
- 1
class Slash < Terminal # :nodoc:
- 4711
def type; :SLASH; end
end
- 1
class Dot < Terminal # :nodoc:
- 2292
def type; :DOT; end
end
- 1
class Symbol < Terminal # :nodoc:
- 1
attr_accessor :regexp
- 1
alias :symbol :regexp
- 1
attr_reader :name
- 1
DEFAULT_EXP = /[^\.\/\?]+/
- 1
GREEDY_EXP = /(.+)/
- 1
def initialize(left, regexp = DEFAULT_EXP)
- 508
super(left)
- 508
@regexp = regexp
- 508
@name = -left.tr("*:", "")
end
- 1
def default_regexp?
- 472
regexp == DEFAULT_EXP
end
- 3740
def type; :SYMBOL; end
- 2097
def symbol?; true; end
end
- 1
class Unary < Node # :nodoc:
- 1
def children; [left] end
end
- 1
class Group < Unary # :nodoc:
- 2390
def type; :GROUP; end
- 178
def group?; true; end
end
- 1
class Star < Unary # :nodoc:
- 4
def star?; true; end
- 19
def type; :STAR; end
- 1
def name
- 3
left.name.tr "*:", ""
end
end
- 1
class Binary < Node # :nodoc:
- 1
attr_accessor :right
- 1
def initialize(left, right)
- 1594
super(left)
- 1594
@right = right
end
- 1
def children; [left, right] end
end
- 1
class Cat < Binary # :nodoc:
- 1938
def cat?; true; end
- 11460
def type; :CAT; end
end
- 1
class Or < Node # :nodoc:
- 1
attr_reader :children
- 1
def initialize(children)
@children = children
end
- 1
def type; :OR; end
end
end
end
end
#
# DO NOT MODIFY!!!!
# This file is automatically generated by Racc 1.4.16
# from Racc grammar file "".
#
- 1
require 'racc/parser.rb'
# :stopdoc:
- 1
require "action_dispatch/journey/parser_extras"
- 1
module ActionDispatch
- 1
module Journey
- 1
class Parser < Racc::Parser
##### State transition tables begin ###
- 1
racc_action_table = [
13, 15, 14, 7, 19, 16, 8, 19, 13, 15,
14, 7, 17, 16, 8, 13, 15, 14, 7, 21,
16, 8, 13, 15, 14, 7, 24, 16, 8 ]
- 1
racc_action_check = [
2, 2, 2, 2, 22, 2, 2, 2, 19, 19,
19, 19, 1, 19, 19, 7, 7, 7, 7, 17,
7, 7, 0, 0, 0, 0, 20, 0, 0 ]
- 1
racc_action_pointer = [
20, 12, -2, nil, nil, nil, nil, 13, nil, nil,
nil, nil, nil, nil, nil, nil, nil, 19, nil, 6,
20, nil, -5, nil, nil ]
- 1
racc_action_default = [
-19, -19, -2, -3, -4, -5, -6, -19, -10, -11,
-12, -13, -14, -15, -16, -17, -18, -19, -1, -19,
-19, 25, -8, -9, -7 ]
- 1
racc_goto_table = [
1, 22, 18, 23, nil, nil, nil, 20 ]
- 1
racc_goto_check = [
1, 2, 1, 3, nil, nil, nil, 1 ]
- 1
racc_goto_pointer = [
nil, 0, -18, -16, nil, nil, nil, nil, nil, nil,
nil ]
- 1
racc_goto_default = [
nil, nil, 2, 3, 4, 5, 6, 9, 10, 11,
12 ]
- 1
racc_reduce_table = [
0, 0, :racc_error,
2, 11, :_reduce_1,
1, 11, :_reduce_2,
1, 11, :_reduce_none,
1, 12, :_reduce_none,
1, 12, :_reduce_none,
1, 12, :_reduce_none,
3, 15, :_reduce_7,
3, 13, :_reduce_8,
3, 13, :_reduce_9,
1, 16, :_reduce_10,
1, 14, :_reduce_none,
1, 14, :_reduce_none,
1, 14, :_reduce_none,
1, 14, :_reduce_none,
1, 19, :_reduce_15,
1, 17, :_reduce_16,
1, 18, :_reduce_17,
1, 20, :_reduce_18 ]
- 1
racc_reduce_n = 19
- 1
racc_shift_n = 25
- 1
racc_token_table = {
false => 0,
:error => 1,
:SLASH => 2,
:LITERAL => 3,
:SYMBOL => 4,
:LPAREN => 5,
:RPAREN => 6,
:DOT => 7,
:STAR => 8,
:OR => 9 }
- 1
racc_nt_base = 10
- 1
racc_use_result_var = false
- 1
Racc_arg = [
racc_action_table,
racc_action_check,
racc_action_default,
racc_action_pointer,
racc_goto_table,
racc_goto_check,
racc_goto_default,
racc_goto_pointer,
racc_nt_base,
racc_reduce_table,
racc_token_table,
racc_shift_n,
racc_reduce_n,
racc_use_result_var ]
- 1
Racc_token_to_s_table = [
"$end",
"error",
"SLASH",
"LITERAL",
"SYMBOL",
"LPAREN",
"RPAREN",
"DOT",
"STAR",
"OR",
"$start",
"expressions",
"expression",
"or",
"terminal",
"group",
"star",
"symbol",
"literal",
"slash",
"dot" ]
- 1
Racc_debug_parser = false
##### State transition tables end #####
# reduce 0 omitted
- 1
def _reduce_1(val, _values)
- 1594
Cat.new(val.first, val.last)
end
- 1
def _reduce_2(val, _values)
- 636
val.first
end
# reduce 3 omitted
# reduce 4 omitted
# reduce 5 omitted
# reduce 6 omitted
- 1
def _reduce_7(val, _values)
- 310
Group.new(val[1])
end
- 1
def _reduce_8(val, _values)
Or.new([val.first, val.last])
end
- 1
def _reduce_9(val, _values)
Or.new([val.first, val.last])
end
- 1
def _reduce_10(val, _values)
- 3
Star.new(Symbol.new(val.last, Symbol::GREEDY_EXP))
end
# reduce 11 omitted
# reduce 12 omitted
# reduce 13 omitted
# reduce 14 omitted
- 1
def _reduce_15(val, _values)
- 670
Slash.new(val.first)
end
- 1
def _reduce_16(val, _values)
- 505
Symbol.new(val.first)
end
- 1
def _reduce_17(val, _values)
- 447
Literal.new(val.first)
end
- 1
def _reduce_18(val, _values)
- 295
Dot.new(val.first)
end
- 1
def _reduce_none(val, _values)
val[0]
end
end # class Parser
end # module Journey
end # module ActionDispatch
# frozen_string_literal: true
- 1
require "action_dispatch/journey/scanner"
- 1
require "action_dispatch/journey/nodes/node"
- 1
module ActionDispatch
# :stopdoc:
- 1
module Journey
- 1
class Parser < Racc::Parser
- 1
include Journey::Nodes
- 1
def self.parse(string)
- 326
new.parse string
end
- 1
def initialize
- 326
@scanner = Scanner.new
end
- 1
def parse(string)
- 326
@scanner.scan_setup(string)
- 326
do_parse
end
- 1
def next_token
- 2866
@scanner.next_token
end
end
end
# :startdoc:
end
# frozen_string_literal: true
- 1
module ActionDispatch
- 1
module Journey # :nodoc:
- 1
module Path # :nodoc:
- 1
class Pattern # :nodoc:
- 1
attr_reader :spec, :requirements, :anchored
- 1
def initialize(ast, requirements, separators, anchored)
- 326
@spec = ast
- 326
@requirements = requirements
- 326
@separators = separators
- 326
@anchored = anchored
- 326
@names = nil
- 326
@optional_names = nil
- 326
@required_names = nil
- 326
@re = nil
- 326
@offsets = nil
end
- 1
def build_formatter
- 326
Visitors::FormatBuilder.new.accept(spec)
end
- 1
def eager_load!
required_names
offsets
to_regexp
nil
end
- 1
def ast
- 311
@spec.find_all(&:symbol?).each do |node|
- 504
re = @requirements[node.to_sym]
- 504
node.regexp = re if re
end
- 311
@spec
end
- 1
def names
- 518
@names ||= spec.find_all(&:symbol?).map(&:name)
end
- 1
def required_names
- 192
@required_names ||= names - optional_names
end
- 1
def optional_names
- 192
@optional_names ||= spec.find_all(&:group?).flat_map { |group|
- 177
group.find_all(&:symbol?)
}.map(&:name).uniq
end
- 1
class AnchoredRegexp < Journey::Visitors::Visitor # :nodoc:
- 1
def initialize(separator, matchers)
@separator = separator
@matchers = matchers
@separator_re = "([^#{separator}]+)"
super()
end
- 1
def accept(node)
%r{\A#{visit node}\Z}
end
- 1
def visit_CAT(node)
"#{visit(node.left)}#{visit(node.right)}"
end
- 1
def visit_SYMBOL(node)
node = node.to_sym
return @separator_re unless @matchers.key?(node)
re = @matchers[node]
"(#{Regexp.union(re)})"
end
- 1
def visit_GROUP(node)
"(?:#{visit node.left})?"
end
- 1
def visit_LITERAL(node)
Regexp.escape(node.left)
end
- 1
alias :visit_DOT :visit_LITERAL
- 1
def visit_SLASH(node)
node.left
end
- 1
def visit_STAR(node)
re = @matchers[node.left.to_sym]
re ? "(#{re})" : "(.+)"
end
- 1
def visit_OR(node)
children = node.children.map { |n| visit n }
"(?:#{children.join(?|)})"
end
end
- 1
class UnanchoredRegexp < AnchoredRegexp # :nodoc:
- 1
def accept(node)
path = visit node
path == "/" ? %r{\A/} : %r{\A#{path}(?:\b|\Z|/)}
end
end
- 1
class MatchData # :nodoc:
- 1
attr_reader :names
- 1
def initialize(names, offsets, match)
@names = names
@offsets = offsets
@match = match
end
- 1
def captures
Array.new(length - 1) { |i| self[i + 1] }
end
- 1
def named_captures
@names.zip(captures).to_h
end
- 1
def [](x)
idx = @offsets[x - 1] + x
@match[idx]
end
- 1
def length
@offsets.length
end
- 1
def post_match
@match.post_match
end
- 1
def to_s
@match.to_s
end
end
- 1
def match(other)
return unless match = to_regexp.match(other)
MatchData.new(names, offsets, match)
end
- 1
alias :=~ :match
- 1
def match?(other)
to_regexp.match?(other)
end
- 1
def source
to_regexp.source
end
- 1
def to_regexp
@re ||= regexp_visitor.new(@separators, @requirements).accept spec
end
- 1
def requirements_for_missing_keys_check
@requirements_for_missing_keys_check ||= requirements.transform_values do |regex|
/\A#{regex}\Z/
end
end
- 1
private
- 1
def regexp_visitor
@anchored ? AnchoredRegexp : UnanchoredRegexp
end
- 1
def offsets
return @offsets if @offsets
@offsets = [0]
spec.find_all(&:symbol?).each do |node|
node = node.to_sym
if @requirements.key?(node)
re = /#{Regexp.union(@requirements[node])}|/
@offsets.push((re.match("").length - 1) + @offsets.last)
else
@offsets << @offsets.last
end
end
@offsets
end
end
end
end
end
# frozen_string_literal: true
- 1
module ActionDispatch
# :stopdoc:
- 1
module Journey
- 1
class Route
- 1
attr_reader :app, :path, :defaults, :name, :precedence, :constraints,
:internal, :scope_options
- 1
alias :conditions :constraints
- 1
module VerbMatchers
- 1
VERBS = %w{ DELETE GET HEAD OPTIONS LINK PATCH POST PUT TRACE UNLINK }
- 1
VERBS.each do |v|
- 10
class_eval <<-eoc, __FILE__, __LINE__ + 1
# frozen_string_literal: true
class #{v}
def self.verb; name.split("::").last; end
def self.call(req); req.#{v.downcase}?; end
end
eoc
end
- 1
class Unknown
- 1
attr_reader :verb
- 1
def initialize(verb)
@verb = verb
end
- 1
def call(request); @verb == request.request_method; end
end
- 1
class All
- 1
def self.call(_); true; end
- 1
def self.verb; ""; end
end
- 1
VERB_TO_CLASS = VERBS.each_with_object(all: All) do |verb, hash|
- 10
klass = const_get verb
- 10
hash[verb] = klass
- 10
hash[verb.downcase] = klass
- 10
hash[verb.downcase.to_sym] = klass
end
end
- 1
def self.verb_matcher(verb)
- 326
VerbMatchers::VERB_TO_CLASS.fetch(verb) do
VerbMatchers::Unknown.new verb.to_s.dasherize.upcase
end
end
##
# +path+ is a path constraint.
# +constraints+ is a hash of constraints to be applied to this route.
- 1
def initialize(name:, app: nil, path:, constraints: {}, required_defaults: [], defaults: {}, request_method_match: nil, precedence: 0, scope_options: {}, internal: false)
- 326
@name = name
- 326
@app = app
- 326
@path = path
- 326
@request_method_match = request_method_match
- 326
@constraints = constraints
- 326
@defaults = defaults
- 326
@required_defaults = nil
- 326
@_required_defaults = required_defaults
- 326
@required_parts = nil
- 326
@parts = nil
- 326
@decorated_ast = nil
- 326
@precedence = precedence
- 326
@path_formatter = @path.build_formatter
- 326
@scope_options = scope_options
- 326
@internal = internal
end
- 1
def eager_load!
path.eager_load!
ast
parts
required_defaults
nil
end
- 1
def ast
- 311
@decorated_ast ||= begin
- 311
decorated_ast = path.ast
- 2191
decorated_ast.find_all(&:terminal?).each { |n| n.memo = self }
- 311
decorated_ast
end
end
# Needed for `bin/rails routes`. Picks up succinctly defined requirements
# for a route, for example route
#
# get 'photo/:id', :controller => 'photos', :action => 'show',
# :id => /[A-Z]\d{5}/
#
# will have {:controller=>"photos", :action=>"show", :id=>/[A-Z]\d{5}/}
# as requirements.
- 1
def requirements
@defaults.merge(path.requirements).delete_if { |_, v|
/.+?/ == v
}
end
- 1
def segments
- 326
path.names
end
- 1
def required_keys
required_parts + required_defaults.keys
end
- 1
def score(supplied_keys)
path.required_names.each do |k|
return -1 unless supplied_keys.include?(k)
end
(required_defaults.length * 2) + path.names.count { |k| supplied_keys.include?(k) }
end
- 1
def parts
- 855
@parts ||= segments.map(&:to_sym)
end
- 1
alias :segment_keys :parts
- 1
def format(path_options)
@path_formatter.evaluate path_options
end
- 1
def required_parts
- 192
@required_parts ||= path.required_names.map(&:to_sym)
end
- 1
def required_default?(key)
@_required_defaults.include?(key)
end
- 1
def required_defaults
@required_defaults ||= @defaults.dup.delete_if do |k, _|
parts.include?(k) || !required_default?(k)
end
end
- 1
def glob?
- 192
path.spec.any?(Nodes::Star)
end
- 1
def dispatcher?
@app.dispatcher?
end
- 1
def matches?(request)
match_verb(request) &&
constraints.all? { |method, value|
case value
when Regexp, String
value === request.send(method).to_s
when Array
value.include?(request.send(method))
when TrueClass
request.send(method).present?
when FalseClass
request.send(method).blank?
else
value === request.send(method)
end
}
end
- 1
def ip
constraints[:ip] || //
end
- 1
def requires_matching_verb?
!@request_method_match.all? { |x| x == VerbMatchers::All }
end
- 1
def verb
verbs.join("|")
end
- 1
private
- 1
def verbs
@request_method_match.map(&:verb)
end
- 1
def match_verb(request)
@request_method_match.any? { |m| m.call request }
end
end
end
# :startdoc:
end
# frozen_string_literal: true
- 1
require "action_dispatch/journey/router/utils"
- 1
require "action_dispatch/journey/routes"
- 1
require "action_dispatch/journey/formatter"
- 1
before = $-w
- 1
$-w = false
- 1
require "action_dispatch/journey/parser"
- 1
$-w = before
- 1
require "action_dispatch/journey/route"
- 1
require "action_dispatch/journey/path/pattern"
- 1
module ActionDispatch
- 1
module Journey # :nodoc:
- 1
class Router # :nodoc:
- 1
attr_accessor :routes
- 1
def initialize(routes)
- 56
@routes = routes
end
- 1
def eager_load!
# Eagerly trigger the simulator's initialization so
# it doesn't happen during a request cycle.
simulator
nil
end
- 1
def serve(req)
find_routes(req).each do |match, parameters, route|
set_params = req.path_parameters
path_info = req.path_info
script_name = req.script_name
unless route.path.anchored
req.script_name = (script_name.to_s + match.to_s).chomp("/")
req.path_info = match.post_match
req.path_info = "/" + req.path_info unless req.path_info.start_with? "/"
end
tmp_params = set_params.merge route.defaults
parameters.each_pair { |key, val|
tmp_params[key] = val.force_encoding(::Encoding::UTF_8)
}
req.path_parameters = tmp_params
status, headers, body = route.app.serve(req)
if "pass" == headers["X-Cascade"]
req.script_name = script_name
req.path_info = path_info
req.path_parameters = set_params
next
end
return [status, headers, body]
end
[404, { "X-Cascade" => "pass" }, ["Not Found"]]
end
- 1
def recognize(rails_req)
find_routes(rails_req).each do |match, parameters, route|
unless route.path.anchored
rails_req.script_name = match.to_s
rails_req.path_info = match.post_match
rails_req.path_info = "/" + rails_req.path_info unless rails_req.path_info.start_with? "/"
end
parameters = route.defaults.merge parameters
yield(route, parameters)
end
end
- 1
def visualizer
tt = GTG::Builder.new(ast).transition_table
groups = partitioned_routes.first.map(&:ast).group_by(&:to_s)
asts = groups.values.map(&:first)
tt.visualizer(asts)
end
- 1
private
- 1
def partitioned_routes
routes.partition { |r|
r.path.anchored && r.ast.grep(Nodes::Symbol).all? { |n| n.default_regexp? }
}
end
- 1
def ast
routes.ast
end
- 1
def simulator
routes.simulator
end
- 1
def custom_routes
routes.custom_routes
end
- 1
def filter_routes(path)
return [] unless ast
simulator.memos(path) { [] }
end
- 1
def find_routes(req)
path_info = req.path_info
routes = filter_routes(path_info).concat custom_routes.find_all { |r|
r.path.match?(path_info)
}
if req.head?
routes = match_head_routes(routes, req)
else
routes.select! { |r| r.matches?(req) }
end
routes.sort_by!(&:precedence)
routes.map! { |r|
match_data = r.path.match(path_info)
path_parameters = {}
match_data.names.each_with_index { |name, i|
val = match_data[i + 1]
path_parameters[name.to_sym] = Utils.unescape_uri(val) if val
}
[match_data, path_parameters, r]
}
end
- 1
def match_head_routes(routes, req)
head_routes = routes.select { |r| r.requires_matching_verb? && r.matches?(req) }
return head_routes unless head_routes.empty?
begin
req.request_method = "GET"
routes.select! { |r| r.matches?(req) }
routes
ensure
req.request_method = "HEAD"
end
end
end
end
end
# frozen_string_literal: true
- 1
module ActionDispatch
- 1
module Journey # :nodoc:
- 1
class Router # :nodoc:
- 1
class Utils # :nodoc:
# Normalizes URI path.
#
# Strips off trailing slash and ensures there is a leading slash.
# Also converts downcase URL encoded string to uppercase.
#
# normalize_path("/foo") # => "/foo"
# normalize_path("/foo/") # => "/foo"
# normalize_path("foo") # => "/foo"
# normalize_path("") # => "/"
# normalize_path("/%ab") # => "/%AB"
- 1
def self.normalize_path(path)
- 574
path ||= ""
- 574
encoding = path.encoding
- 574
path = +"/#{path}"
- 574
path.squeeze!("/")
- 574
unless path == "/"
- 556
path.delete_suffix!("/")
- 559
path.gsub!(/(%[a-f0-9]{2})/) { $1.upcase }
end
- 574
path.force_encoding(encoding)
end
# URI path and fragment escaping
# https://tools.ietf.org/html/rfc3986
- 1
class UriEncoder # :nodoc:
- 1
ENCODE = "%%%02X"
- 1
US_ASCII = Encoding::US_ASCII
- 1
UTF_8 = Encoding::UTF_8
- 1
EMPTY = (+"").force_encoding(US_ASCII).freeze
- 513
DEC2HEX = (0..255).to_a.map { |i| ENCODE % i }.map { |s| s.force_encoding(US_ASCII) }
- 1
ALPHA = "a-zA-Z"
- 1
DIGIT = "0-9"
- 1
UNRESERVED = "#{ALPHA}#{DIGIT}\\-\\._~"
- 1
SUB_DELIMS = "!\\$&'\\(\\)\\*\\+,;="
- 1
ESCAPED = /%[a-zA-Z0-9]{2}/.freeze
- 1
FRAGMENT = /[^#{UNRESERVED}#{SUB_DELIMS}:@\/\?]/.freeze
- 1
SEGMENT = /[^#{UNRESERVED}#{SUB_DELIMS}:@]/.freeze
- 1
PATH = /[^#{UNRESERVED}#{SUB_DELIMS}:@\/]/.freeze
- 1
def escape_fragment(fragment)
escape(fragment, FRAGMENT)
end
- 1
def escape_path(path)
escape(path, PATH)
end
- 1
def escape_segment(segment)
escape(segment, SEGMENT)
end
- 1
def unescape_uri(uri)
encoding = uri.encoding == US_ASCII ? UTF_8 : uri.encoding
uri.gsub(ESCAPED) { |match| [match[1, 2].hex].pack("C") }.force_encoding(encoding)
end
- 1
private
- 1
def escape(component, pattern)
component.gsub(pattern) { |unsafe| percent_encode(unsafe) }.force_encoding(US_ASCII)
end
- 1
def percent_encode(unsafe)
safe = EMPTY.dup
unsafe.each_byte { |b| safe << DEC2HEX[b] }
safe
end
end
- 1
ENCODER = UriEncoder.new
- 1
def self.escape_path(path)
ENCODER.escape_path(path.to_s)
end
- 1
def self.escape_segment(segment)
ENCODER.escape_segment(segment.to_s)
end
- 1
def self.escape_fragment(fragment)
ENCODER.escape_fragment(fragment.to_s)
end
# Replaces any escaped sequences with their unescaped representations.
#
# uri = "/topics?title=Ruby%20on%20Rails"
# unescape_uri(uri) #=> "/topics?title=Ruby on Rails"
- 1
def self.unescape_uri(uri)
ENCODER.unescape_uri(uri)
end
end
end
end
end
# frozen_string_literal: true
- 1
module ActionDispatch
- 1
module Journey # :nodoc:
# The Routing table. Contains all routes for a system. Routes can be
# added to the table by calling Routes#add_route.
- 1
class Routes # :nodoc:
- 1
include Enumerable
- 1
attr_reader :routes, :custom_routes, :anchored_routes
- 1
def initialize
- 56
@routes = []
- 56
@ast = nil
- 56
@anchored_routes = []
- 56
@custom_routes = []
- 56
@simulator = nil
end
- 1
def empty?
routes.empty?
end
- 1
def length
routes.length
end
- 1
alias :size :length
- 1
def last
routes.last
end
- 1
def each(&block)
routes.each(&block)
end
- 1
def clear
- 54
routes.clear
- 54
anchored_routes.clear
- 54
custom_routes.clear
end
- 1
def partition_route(route)
- 326
if route.path.anchored && route.ast.grep(Nodes::Symbol).all?(&:default_regexp?)
- 283
anchored_routes << route
else
- 43
custom_routes << route
end
end
- 1
def ast
@ast ||= begin
asts = anchored_routes.map(&:ast)
Nodes::Or.new(asts)
end
end
- 1
def simulator
@simulator ||= begin
gtg = GTG::Builder.new(ast).transition_table
GTG::Simulator.new(gtg)
end
end
- 1
def add_route(name, mapping)
- 326
route = mapping.make_route name, routes.length
- 326
routes << route
- 326
partition_route(route)
- 326
clear_cache!
- 326
route
end
- 1
private
- 1
def clear_cache!
- 326
@ast = nil
- 326
@simulator = nil
end
end
end
end
# frozen_string_literal: true
- 1
require "strscan"
- 1
module ActionDispatch
- 1
module Journey # :nodoc:
- 1
class Scanner # :nodoc:
- 1
def initialize
- 326
@ss = nil
end
- 1
def scan_setup(str)
- 326
@ss = StringScanner.new(str)
end
- 1
def eos?
@ss.eos?
end
- 1
def pos
@ss.pos
end
- 1
def pre_match
@ss.pre_match
end
- 1
def next_token
- 2866
return if @ss.eos?
- 2540
until token = scan || @ss.eos?; end
- 2540
token
end
- 1
private
# takes advantage of String @- deduping capabilities in Ruby 2.5 upwards
# see: https://bugs.ruby-lang.org/issues/13077
- 1
def dedup_scan(regex)
- 1405
r = @ss.scan(regex)
- 1405
r ? -r : nil
end
- 1
def scan
case
# /
when @ss.skip(/\//)
- 670
[:SLASH, "/"]
when @ss.skip(/\(/)
- 310
[:LPAREN, "("]
when @ss.skip(/\)/)
- 310
[:RPAREN, ")"]
when @ss.skip(/\|/)
[:OR, "|"]
when @ss.skip(/\./)
- 295
[:DOT, "."]
when text = dedup_scan(/:\w+/)
- 505
[:SYMBOL, text]
when text = dedup_scan(/\*\w+/)
- 3
[:STAR, text]
when text = @ss.scan(/(?:[\w%\-~!$&'*+,;=@]|\\[:()])+/)
- 447
text.tr! "\\", ""
- 447
[:LITERAL, -text]
# any char
when text = dedup_scan(/./)
[:LITERAL, text]
- 2540
end
end
end
end
end
# frozen_string_literal: true
- 1
module ActionDispatch
# :stopdoc:
- 1
module Journey
- 1
class Format
- 1
ESCAPE_PATH = ->(value) { Router::Utils.escape_path(value) }
- 1
ESCAPE_SEGMENT = ->(value) { Router::Utils.escape_segment(value) }
- 1
Parameter = Struct.new(:name, :escaper) do
- 1
def escape(value); escaper.call value; end
end
- 1
def self.required_path(symbol)
- 8
Parameter.new symbol, ESCAPE_PATH
end
- 1
def self.required_segment(symbol)
- 500
Parameter.new symbol, ESCAPE_SEGMENT
end
- 1
def initialize(parts)
- 636
@parts = parts
- 636
@children = []
- 636
@parameters = []
- 636
parts.each_with_index do |object, i|
- 2230
case object
when Journey::Format
- 310
@children << i
when Parameter
- 508
@parameters << i
end
end
end
- 1
def evaluate(hash)
parts = @parts.dup
@parameters.each do |index|
param = parts[index]
value = hash[param.name]
return "" unless value
parts[index] = param.escape value
end
@children.each { |index| parts[index] = parts[index].evaluate(hash) }
parts.join
end
end
- 1
module Visitors # :nodoc:
- 1
class Visitor # :nodoc:
- 1
DISPATCH_CACHE = {}
- 1
def accept(node)
- 326
visit(node)
end
- 1
private
- 1
def visit(node)
- 3824
send(DISPATCH_CACHE[node.type], node)
end
- 1
def binary(node)
visit(node.left)
visit(node.right)
end
- 1595
def visit_CAT(n); binary(n); end
- 1
def nary(node)
node.children.each { |c| visit(c) }
end
- 1
def visit_OR(n); nary(n); end
- 1
def unary(node)
- 310
visit(node.left)
end
- 1
def visit_GROUP(n); unary(n); end
- 1
def visit_STAR(n); unary(n); end
- 1
def terminal(node); end
- 448
def visit_LITERAL(n); terminal(n); end
- 1
def visit_SYMBOL(n); terminal(n); end
- 671
def visit_SLASH(n); terminal(n); end
- 296
def visit_DOT(n); terminal(n); end
- 1
private_instance_methods(false).each do |pim|
- 13
next unless pim =~ /^visit_(.*)$/
- 8
DISPATCH_CACHE[$1.to_sym] = pim
end
end
- 1
class FunctionalVisitor # :nodoc:
- 1
DISPATCH_CACHE = {}
- 1
def accept(node, seed)
- 2146
visit(node, seed)
end
- 1
def visit(node, seed)
- 23970
send(DISPATCH_CACHE[node.type], node, seed)
end
- 1
def binary(node, seed)
- 9865
visit(node.right, visit(node.left, seed))
end
- 9866
def visit_CAT(n, seed); binary(n, seed); end
- 1
def nary(node, seed)
node.children.inject(seed) { |s, c| visit(c, s) }
end
- 1
def visit_OR(n, seed); nary(n, seed); end
- 1
def unary(node, seed)
- 2094
visit(node.left, seed)
end
- 2080
def visit_GROUP(n, seed); unary(n, seed); end
- 16
def visit_STAR(n, seed); unary(n, seed); end
- 12012
def terminal(node, seed); seed; end
- 2742
def visit_LITERAL(n, seed); terminal(n, seed); end
- 3235
def visit_SYMBOL(n, seed); terminal(n, seed); end
- 4041
def visit_SLASH(n, seed); terminal(n, seed); end
- 1997
def visit_DOT(n, seed); terminal(n, seed); end
- 1
instance_methods(false).each do |pim|
- 14
next unless pim =~ /^visit_(.*)$/
- 8
DISPATCH_CACHE[$1.to_sym] = pim
end
end
- 1
class FormatBuilder < Visitor # :nodoc:
- 327
def accept(node); Journey::Format.new(super); end
- 1413
def terminal(node); [node.left]; end
- 1
def binary(node)
- 1594
visit(node.left) + visit(node.right)
end
- 311
def visit_GROUP(n); [Journey::Format.new(unary(n))]; end
- 1
def visit_STAR(n)
- 3
[Journey::Format.required_path(n.left.to_sym)]
end
- 1
def visit_SYMBOL(n)
- 505
symbol = n.to_sym
- 505
if symbol == :controller
- 5
[Journey::Format.required_path(symbol)]
else
- 500
[Journey::Format.required_segment(symbol)]
end
end
end
# Loop through the requirements AST.
- 1
class Each < FunctionalVisitor # :nodoc:
- 1
def visit(node, block)
- 23970
block.call(node)
- 23970
super
end
- 1
INSTANCE = new
end
- 1
class String < FunctionalVisitor # :nodoc:
- 1
private
- 1
def binary(node, seed)
visit(node.right, visit(node.left, seed))
end
- 1
def nary(node, seed)
last_child = node.children.last
node.children.inject(seed) { |s, c|
string = visit(c, s)
string << "|" unless last_child == c
string
}
end
- 1
def terminal(node, seed)
seed + node.left
end
- 1
def visit_GROUP(node, seed)
visit(node.left, seed.dup << "(") << ")"
end
- 1
INSTANCE = new
end
- 1
class Dot < FunctionalVisitor # :nodoc:
- 1
def initialize
- 1
@nodes = []
- 1
@edges = []
end
- 1
def accept(node, seed = [[], []])
super
nodes, edges = seed
<<-eodot
digraph parse_tree {
size="8,5"
node [shape = none];
edge [dir = none];
#{nodes.join "\n"}
#{edges.join("\n")}
}
eodot
end
- 1
private
- 1
def binary(node, seed)
seed.last.concat node.children.map { |c|
"#{node.object_id} -> #{c.object_id};"
}
super
end
- 1
def nary(node, seed)
seed.last.concat node.children.map { |c|
"#{node.object_id} -> #{c.object_id};"
}
super
end
- 1
def unary(node, seed)
seed.last << "#{node.object_id} -> #{node.left.object_id};"
super
end
- 1
def visit_GROUP(node, seed)
seed.first << "#{node.object_id} [label=\"()\"];"
super
end
- 1
def visit_CAT(node, seed)
seed.first << "#{node.object_id} [label=\"○\"];"
super
end
- 1
def visit_STAR(node, seed)
seed.first << "#{node.object_id} [label=\"*\"];"
super
end
- 1
def visit_OR(node, seed)
seed.first << "#{node.object_id} [label=\"|\"];"
super
end
- 1
def terminal(node, seed)
value = node.left
seed.first << "#{node.object_id} [label=\"#{value}\"];"
seed
end
- 1
INSTANCE = new
end
end
end
# :startdoc:
end
# frozen_string_literal: true
- 1
require "erb"
- 1
require "action_dispatch/http/request"
- 1
require "active_support/actionable_error"
- 1
module ActionDispatch
- 1
class ActionableExceptions # :nodoc:
- 1
cattr_accessor :endpoint, default: "/rails/actions"
- 1
def initialize(app)
- 37
@app = app
end
- 1
def call(env)
request = ActionDispatch::Request.new(env)
return @app.call(env) unless actionable_request?(request)
ActiveSupport::ActionableError.dispatch(request.params[:error].to_s.safe_constantize, request.params[:action])
redirect_to request.params[:location]
end
- 1
private
- 1
def actionable_request?(request)
request.get_header("action_dispatch.show_detailed_exceptions") && request.post? && request.path == endpoint
end
- 1
def redirect_to(location)
body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(location)}\">redirected</a>.</body></html>"
[302, {
"Content-Type" => "text/html; charset=#{Response.default_charset}",
"Content-Length" => body.bytesize.to_s,
"Location" => location,
}, [body]]
end
end
end
# frozen_string_literal: true
- 1
module ActionDispatch
# Provides callbacks to be executed before and after dispatching the request.
- 1
class Callbacks
- 1
include ActiveSupport::Callbacks
- 1
define_callbacks :call
- 1
class << self
- 1
def before(*args, &block)
set_callback(:call, :before, *args, &block)
end
- 1
def after(*args, &block)
set_callback(:call, :after, *args, &block)
end
end
- 1
def initialize(app)
- 37
@app = app
end
- 1
def call(env)
error = nil
result = run_callbacks :call do
@app.call(env)
rescue => error
end
raise error if error
result
end
end
end
# frozen_string_literal: true
- 1
require "active_support/core_ext/hash/keys"
- 1
require "active_support/key_generator"
- 1
require "active_support/message_verifier"
- 1
require "active_support/json"
- 1
require "rack/utils"
- 1
module ActionDispatch
- 1
class Request
- 1
def cookie_jar
fetch_header("action_dispatch.cookies") do
self.cookie_jar = Cookies::CookieJar.build(self, cookies)
end
end
# :stopdoc:
- 1
prepend Module.new {
- 1
def commit_cookie_jar!
cookie_jar.commit!
end
}
- 1
def have_cookie_jar?
has_header? "action_dispatch.cookies"
end
- 1
def cookie_jar=(jar)
set_header "action_dispatch.cookies", jar
end
- 1
def key_generator
get_header Cookies::GENERATOR_KEY
end
- 1
def signed_cookie_salt
get_header Cookies::SIGNED_COOKIE_SALT
end
- 1
def encrypted_cookie_salt
get_header Cookies::ENCRYPTED_COOKIE_SALT
end
- 1
def encrypted_signed_cookie_salt
get_header Cookies::ENCRYPTED_SIGNED_COOKIE_SALT
end
- 1
def authenticated_encrypted_cookie_salt
get_header Cookies::AUTHENTICATED_ENCRYPTED_COOKIE_SALT
end
- 1
def use_authenticated_cookie_encryption
get_header Cookies::USE_AUTHENTICATED_COOKIE_ENCRYPTION
end
- 1
def encrypted_cookie_cipher
get_header Cookies::ENCRYPTED_COOKIE_CIPHER
end
- 1
def signed_cookie_digest
get_header Cookies::SIGNED_COOKIE_DIGEST
end
- 1
def secret_key_base
get_header Cookies::SECRET_KEY_BASE
end
- 1
def cookies_serializer
get_header Cookies::COOKIES_SERIALIZER
end
- 1
def cookies_same_site_protection
get_header Cookies::COOKIES_SAME_SITE_PROTECTION
end
- 1
def cookies_digest
get_header Cookies::COOKIES_DIGEST
end
- 1
def cookies_rotations
get_header Cookies::COOKIES_ROTATIONS
end
- 1
def use_cookies_with_metadata
get_header Cookies::USE_COOKIES_WITH_METADATA
end
# :startdoc:
end
# Read and write data to cookies through ActionController#cookies.
#
# When reading cookie data, the data is read from the HTTP request header, Cookie.
# When writing cookie data, the data is sent out in the HTTP response header, Set-Cookie.
#
# Examples of writing:
#
# # Sets a simple session cookie.
# # This cookie will be deleted when the user's browser is closed.
# cookies[:user_name] = "david"
#
# # Cookie values are String based. Other data types need to be serialized.
# cookies[:lat_lon] = JSON.generate([47.68, -122.37])
#
# # Sets a cookie that expires in 1 hour.
# cookies[:login] = { value: "XJ-122", expires: 1.hour }
#
# # Sets a cookie that expires at a specific time.
# cookies[:login] = { value: "XJ-122", expires: Time.utc(2020, 10, 15, 5) }
#
# # Sets a signed cookie, which prevents users from tampering with its value.
# # It can be read using the signed method `cookies.signed[:name]`
# cookies.signed[:user_id] = current_user.id
#
# # Sets an encrypted cookie value before sending it to the client which
# # prevent users from reading and tampering with its value.
# # It can be read using the encrypted method `cookies.encrypted[:name]`
# cookies.encrypted[:discount] = 45
#
# # Sets a "permanent" cookie (which expires in 20 years from now).
# cookies.permanent[:login] = "XJ-122"
#
# # You can also chain these methods:
# cookies.signed.permanent[:login] = "XJ-122"
#
# Examples of reading:
#
# cookies[:user_name] # => "david"
# cookies.size # => 2
# JSON.parse(cookies[:lat_lon]) # => [47.68, -122.37]
# cookies.signed[:login] # => "XJ-122"
# cookies.encrypted[:discount] # => 45
#
# Example for deleting:
#
# cookies.delete :user_name
#
# Please note that if you specify a :domain when setting a cookie, you must also specify the domain when deleting the cookie:
#
# cookies[:name] = {
# value: 'a yummy cookie',
# expires: 1.year,
# domain: 'domain.com'
# }
#
# cookies.delete(:name, domain: 'domain.com')
#
# The option symbols for setting cookies are:
#
# * <tt>:value</tt> - The cookie's value.
# * <tt>:path</tt> - The path for which this cookie applies. Defaults to the root
# of the application.
# * <tt>:domain</tt> - The domain for which this cookie applies so you can
# restrict to the domain level. If you use a schema like www.example.com
# and want to share session with user.example.com set <tt>:domain</tt>
# to <tt>:all</tt>. To support multiple domains, provide an array, and
# the first domain matching <tt>request.host</tt> will be used. Make
# sure to specify the <tt>:domain</tt> option with <tt>:all</tt> or
# <tt>Array</tt> again when deleting cookies.
#
# domain: nil # Does not set cookie domain. (default)
# domain: :all # Allow the cookie for the top most level
# # domain and subdomains.
# domain: %w(.example.com .example.org) # Allow the cookie
# # for concrete domain names.
#
# * <tt>:tld_length</tt> - When using <tt>:domain => :all</tt>, this option can be used to explicitly
# set the TLD length when using a short (<= 3 character) domain that is being interpreted as part of a TLD.
# For example, to share cookies between user1.lvh.me and user2.lvh.me, set <tt>:tld_length</tt> to 2.
# * <tt>:expires</tt> - The time at which this cookie expires, as a \Time or ActiveSupport::Duration object.
# * <tt>:secure</tt> - Whether this cookie is only transmitted to HTTPS servers.
# Default is +false+.
# * <tt>:httponly</tt> - Whether this cookie is accessible via scripting or
# only HTTP. Defaults to +false+.
- 1
class Cookies
- 1
HTTP_HEADER = "Set-Cookie"
- 1
GENERATOR_KEY = "action_dispatch.key_generator"
- 1
SIGNED_COOKIE_SALT = "action_dispatch.signed_cookie_salt"
- 1
ENCRYPTED_COOKIE_SALT = "action_dispatch.encrypted_cookie_salt"
- 1
ENCRYPTED_SIGNED_COOKIE_SALT = "action_dispatch.encrypted_signed_cookie_salt"
- 1
AUTHENTICATED_ENCRYPTED_COOKIE_SALT = "action_dispatch.authenticated_encrypted_cookie_salt"
- 1
USE_AUTHENTICATED_COOKIE_ENCRYPTION = "action_dispatch.use_authenticated_cookie_encryption"
- 1
ENCRYPTED_COOKIE_CIPHER = "action_dispatch.encrypted_cookie_cipher"
- 1
SIGNED_COOKIE_DIGEST = "action_dispatch.signed_cookie_digest"
- 1
SECRET_KEY_BASE = "action_dispatch.secret_key_base"
- 1
COOKIES_SERIALIZER = "action_dispatch.cookies_serializer"
- 1
COOKIES_DIGEST = "action_dispatch.cookies_digest"
- 1
COOKIES_ROTATIONS = "action_dispatch.cookies_rotations"
- 1
COOKIES_SAME_SITE_PROTECTION = "action_dispatch.cookies_same_site_protection"
- 1
USE_COOKIES_WITH_METADATA = "action_dispatch.use_cookies_with_metadata"
# Cookies can typically store 4096 bytes.
- 1
MAX_COOKIE_SIZE = 4096
# Raised when storing more than 4K of session data.
- 1
CookieOverflow = Class.new StandardError
# Include in a cookie jar to allow chaining, e.g. cookies.permanent.signed.
- 1
module ChainedCookieJars
# Returns a jar that'll automatically set the assigned cookies to have an expiration date 20 years from now. Example:
#
# cookies.permanent[:prefers_open_id] = true
# # => Set-Cookie: prefers_open_id=true; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT
#
# This jar is only meant for writing. You'll read permanent cookies through the regular accessor.
#
# This jar allows chaining with the signed jar as well, so you can set permanent, signed cookies. Examples:
#
# cookies.permanent.signed[:remember_me] = current_user.id
# # => Set-Cookie: remember_me=BAhU--848956038e692d7046deab32b7131856ab20e14e; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT
- 1
def permanent
@permanent ||= PermanentCookieJar.new(self)
end
# Returns a jar that'll automatically generate a signed representation of cookie value and verify it when reading from
# the cookie again. This is useful for creating cookies with values that the user is not supposed to change. If a signed
# cookie was tampered with by the user (or a 3rd party), +nil+ will be returned.
#
# This jar requires that you set a suitable secret for the verification on your app's +secret_key_base+.
#
# Example:
#
# cookies.signed[:discount] = 45
# # => Set-Cookie: discount=BAhpMg==--2c1c6906c90a3bc4fd54a51ffb41dffa4bf6b5f7; path=/
#
# cookies.signed[:discount] # => 45
- 1
def signed
@signed ||= SignedKeyRotatingCookieJar.new(self)
end
# Returns a jar that'll automatically encrypt cookie values before sending them to the client and will decrypt them for read.
# If the cookie was tampered with by the user (or a 3rd party), +nil+ will be returned.
#
# If +config.action_dispatch.encrypted_cookie_salt+ and +config.action_dispatch.encrypted_signed_cookie_salt+
# are both set, legacy cookies encrypted with HMAC AES-256-CBC will be transparently upgraded.
#
# This jar requires that you set a suitable secret for the verification on your app's +secret_key_base+.
#
# Example:
#
# cookies.encrypted[:discount] = 45
# # => Set-Cookie: discount=DIQ7fw==--K3n//8vvnSbGq9dA--7Xh91HfLpwzbj1czhBiwOg==; path=/
#
# cookies.encrypted[:discount] # => 45
- 1
def encrypted
@encrypted ||= EncryptedKeyRotatingCookieJar.new(self)
end
# Returns the +signed+ or +encrypted+ jar, preferring +encrypted+ if +secret_key_base+ is set.
# Used by ActionDispatch::Session::CookieStore to avoid the need to introduce new cookie stores.
- 1
def signed_or_encrypted
@signed_or_encrypted ||=
if request.secret_key_base.present?
encrypted
else
signed
end
end
- 1
private
- 1
def upgrade_legacy_hmac_aes_cbc_cookies?
request.secret_key_base.present? &&
request.encrypted_signed_cookie_salt.present? &&
request.encrypted_cookie_salt.present? &&
request.use_authenticated_cookie_encryption
end
- 1
def prepare_upgrade_legacy_hmac_aes_cbc_cookies?
request.secret_key_base.present? &&
request.authenticated_encrypted_cookie_salt.present? &&
!request.use_authenticated_cookie_encryption
end
- 1
def encrypted_cookie_cipher
request.encrypted_cookie_cipher || "aes-256-gcm"
end
- 1
def signed_cookie_digest
request.signed_cookie_digest || "SHA1"
end
end
- 1
class CookieJar #:nodoc:
- 1
include Enumerable, ChainedCookieJars
# This regular expression is used to split the levels of a domain.
# The top level domain can be any string without a period or
# **.**, ***.** style TLDs like co.uk or com.au
#
# www.example.co.uk gives:
# $& => example.co.uk
#
# example.com gives:
# $& => example.com
#
# lots.of.subdomains.example.local gives:
# $& => example.local
- 1
DOMAIN_REGEXP = /[^.]*\.([^.]*|..\...|...\...)$/
- 1
def self.build(req, cookies)
jar = new(req)
jar.update(cookies)
jar
end
- 1
attr_reader :request
- 1
def initialize(request)
@set_cookies = {}
@delete_cookies = {}
@request = request
@cookies = {}
@committed = false
end
- 1
def committed?; @committed; end
- 1
def commit!
@committed = true
@set_cookies.freeze
@delete_cookies.freeze
end
- 1
def each(&block)
@cookies.each(&block)
end
# Returns the value of the cookie by +name+, or +nil+ if no such cookie exists.
- 1
def [](name)
@cookies[name.to_s]
end
- 1
def fetch(name, *args, &block)
@cookies.fetch(name.to_s, *args, &block)
end
- 1
def key?(name)
@cookies.key?(name.to_s)
end
- 1
alias :has_key? :key?
# Returns the cookies as Hash.
- 1
alias :to_hash :to_h
- 1
def update(other_hash)
@cookies.update other_hash.stringify_keys
self
end
- 1
def update_cookies_from_jar
request_jar = @request.cookie_jar.instance_variable_get(:@cookies)
set_cookies = request_jar.reject { |k, _| @delete_cookies.key?(k) || @set_cookies.key?(k) }
@cookies.update set_cookies if set_cookies
end
- 1
def to_header
@cookies.map { |k, v| "#{escape(k)}=#{escape(v)}" }.join "; "
end
# Sets the cookie named +name+. The second argument may be the cookie's
# value or a hash of options as documented above.
- 1
def []=(name, options)
if options.is_a?(Hash)
options.symbolize_keys!
value = options[:value]
else
value = options
options = { value: value }
end
handle_options(options)
if @cookies[name.to_s] != value || options[:expires]
@cookies[name.to_s] = value
@set_cookies[name.to_s] = options
@delete_cookies.delete(name.to_s)
end
value
end
# Removes the cookie on the client machine by setting the value to an empty string
# and the expiration date in the past. Like <tt>[]=</tt>, you can pass in
# an options hash to delete cookies with extra data such as a <tt>:path</tt>.
- 1
def delete(name, options = {})
return unless @cookies.has_key? name.to_s
options.symbolize_keys!
handle_options(options)
value = @cookies.delete(name.to_s)
@delete_cookies[name.to_s] = options
value
end
# Whether the given cookie is to be deleted by this CookieJar.
# Like <tt>[]=</tt>, you can pass in an options hash to test if a
# deletion applies to a specific <tt>:path</tt>, <tt>:domain</tt> etc.
- 1
def deleted?(name, options = {})
options.symbolize_keys!
handle_options(options)
@delete_cookies[name.to_s] == options
end
# Removes all cookies on the client machine by calling <tt>delete</tt> for each cookie.
- 1
def clear(options = {})
@cookies.each_key { |k| delete(k, options) }
end
- 1
def write(headers)
if header = make_set_cookie_header(headers[HTTP_HEADER])
headers[HTTP_HEADER] = header
end
end
- 1
mattr_accessor :always_write_cookie, default: false
- 1
private
- 1
def escape(string)
::Rack::Utils.escape(string)
end
- 1
def make_set_cookie_header(header)
header = @set_cookies.inject(header) { |m, (k, v)|
if write_cookie?(v)
::Rack::Utils.add_cookie_to_header(m, k, v)
else
m
end
}
@delete_cookies.inject(header) { |m, (k, v)|
::Rack::Utils.add_remove_cookie_to_header(m, k, v)
}
end
- 1
def write_cookie?(cookie)
request.ssl? || !cookie[:secure] || always_write_cookie
end
- 1
def handle_options(options)
if options[:expires].respond_to?(:from_now)
options[:expires] = options[:expires].from_now
end
options[:path] ||= "/"
options[:same_site] ||= request.cookies_same_site_protection
if options[:domain] == :all || options[:domain] == "all"
# If there is a provided tld length then we use it otherwise default domain regexp.
domain_regexp = options[:tld_length] ? /([^.]+\.?){#{options[:tld_length]}}$/ : DOMAIN_REGEXP
# If host is not ip and matches domain regexp.
# (ip confirms to domain regexp so we explicitly check for ip)
options[:domain] = if !request.host.match?(/^[\d.]+$/) && (request.host =~ domain_regexp)
".#{$&}"
end
elsif options[:domain].is_a? Array
# If host matches one of the supplied domains.
options[:domain] = options[:domain].find do |domain|
domain = domain.delete_prefix(".")
request.host == domain || request.host.end_with?(".#{domain}")
end
end
end
end
- 1
class AbstractCookieJar # :nodoc:
- 1
include ChainedCookieJars
- 1
def initialize(parent_jar)
@parent_jar = parent_jar
end
- 1
def [](name)
if data = @parent_jar[name.to_s]
result = parse(name, data, purpose: "cookie.#{name}")
if result.nil?
parse(name, data)
else
result
end
end
end
- 1
def []=(name, options)
if options.is_a?(Hash)
options.symbolize_keys!
else
options = { value: options }
end
commit(name, options)
@parent_jar[name] = options
end
- 1
protected
- 1
def request; @parent_jar.request; end
- 1
private
- 1
def expiry_options(options)
if options[:expires].respond_to?(:from_now)
{ expires_in: options[:expires] }
else
{ expires_at: options[:expires] }
end
end
- 1
def cookie_metadata(name, options)
expiry_options(options).tap do |metadata|
metadata[:purpose] = "cookie.#{name}" if request.use_cookies_with_metadata
end
end
- 1
def parse(name, data, purpose: nil); data; end
- 1
def commit(name, options); end
end
- 1
class PermanentCookieJar < AbstractCookieJar # :nodoc:
- 1
private
- 1
def commit(name, options)
options[:expires] = 20.years.from_now
end
end
- 1
class MarshalWithJsonFallback # :nodoc:
- 1
def self.load(value)
Marshal.load(value)
rescue TypeError => e
ActiveSupport::JSON.decode(value) rescue raise e
end
- 1
def self.dump(value)
Marshal.dump(value)
end
end
- 1
class JsonSerializer # :nodoc:
- 1
def self.load(value)
ActiveSupport::JSON.decode(value)
end
- 1
def self.dump(value)
ActiveSupport::JSON.encode(value)
end
end
- 1
module SerializedCookieJars # :nodoc:
- 1
MARSHAL_SIGNATURE = "\x04\x08"
- 1
SERIALIZER = ActiveSupport::MessageEncryptor::NullSerializer
- 1
protected
- 1
def needs_migration?(value)
request.cookies_serializer == :hybrid && value.start_with?(MARSHAL_SIGNATURE)
end
- 1
def serialize(value)
serializer.dump(value)
end
- 1
def deserialize(name)
rotate = false
value = yield -> { rotate = true }
if value
case
when needs_migration?(value)
Marshal.load(value).tap do |v|
self[name] = { value: v }
end
when rotate
serializer.load(value).tap do |v|
self[name] = { value: v }
end
else
serializer.load(value)
end
end
end
- 1
def serializer
serializer = request.cookies_serializer || :marshal
case serializer
when :marshal
MarshalWithJsonFallback
when :json, :hybrid
JsonSerializer
else
serializer
end
end
- 1
def digest
request.cookies_digest || "SHA1"
end
end
- 1
class SignedKeyRotatingCookieJar < AbstractCookieJar # :nodoc:
- 1
include SerializedCookieJars
- 1
def initialize(parent_jar)
super
secret = request.key_generator.generate_key(request.signed_cookie_salt)
@verifier = ActiveSupport::MessageVerifier.new(secret, digest: signed_cookie_digest, serializer: SERIALIZER)
request.cookies_rotations.signed.each do |(*secrets)|
options = secrets.extract_options!
@verifier.rotate(*secrets, serializer: SERIALIZER, **options)
end
end
- 1
private
- 1
def parse(name, signed_message, purpose: nil)
deserialize(name) do |rotate|
@verifier.verified(signed_message, on_rotation: rotate, purpose: purpose)
end
end
- 1
def commit(name, options)
options[:value] = @verifier.generate(serialize(options[:value]), **cookie_metadata(name, options))
raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE
end
end
- 1
class EncryptedKeyRotatingCookieJar < AbstractCookieJar # :nodoc:
- 1
include SerializedCookieJars
- 1
def initialize(parent_jar)
super
if request.use_authenticated_cookie_encryption
key_len = ActiveSupport::MessageEncryptor.key_len(encrypted_cookie_cipher)
secret = request.key_generator.generate_key(request.authenticated_encrypted_cookie_salt, key_len)
@encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: encrypted_cookie_cipher, serializer: SERIALIZER)
else
key_len = ActiveSupport::MessageEncryptor.key_len("aes-256-cbc")
secret = request.key_generator.generate_key(request.encrypted_cookie_salt, key_len)
sign_secret = request.key_generator.generate_key(request.encrypted_signed_cookie_salt)
@encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, cipher: "aes-256-cbc", serializer: SERIALIZER)
end
request.cookies_rotations.encrypted.each do |(*secrets)|
options = secrets.extract_options!
@encryptor.rotate(*secrets, serializer: SERIALIZER, **options)
end
if upgrade_legacy_hmac_aes_cbc_cookies?
legacy_cipher = "aes-256-cbc"
secret = request.key_generator.generate_key(request.encrypted_cookie_salt, ActiveSupport::MessageEncryptor.key_len(legacy_cipher))
sign_secret = request.key_generator.generate_key(request.encrypted_signed_cookie_salt)
@encryptor.rotate(secret, sign_secret, cipher: legacy_cipher, digest: digest, serializer: SERIALIZER)
elsif prepare_upgrade_legacy_hmac_aes_cbc_cookies?
future_cipher = encrypted_cookie_cipher
secret = request.key_generator.generate_key(request.authenticated_encrypted_cookie_salt, ActiveSupport::MessageEncryptor.key_len(future_cipher))
@encryptor.rotate(secret, nil, cipher: future_cipher, serializer: SERIALIZER)
end
end
- 1
private
- 1
def parse(name, encrypted_message, purpose: nil)
deserialize(name) do |rotate|
@encryptor.decrypt_and_verify(encrypted_message, on_rotation: rotate, purpose: purpose)
end
rescue ActiveSupport::MessageEncryptor::InvalidMessage, ActiveSupport::MessageVerifier::InvalidSignature
nil
end
- 1
def commit(name, options)
options[:value] = @encryptor.encrypt_and_sign(serialize(options[:value]), **cookie_metadata(name, options))
raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE
end
end
- 1
def initialize(app)
- 37
@app = app
end
- 1
def call(env)
request = ActionDispatch::Request.new env
status, headers, body = @app.call(env)
if request.have_cookie_jar?
cookie_jar = request.cookie_jar
unless cookie_jar.committed?
cookie_jar.write(headers)
if headers[HTTP_HEADER].respond_to?(:join)
headers[HTTP_HEADER] = headers[HTTP_HEADER].join("\n")
end
end
end
[status, headers, body]
end
end
end
# frozen_string_literal: true
- 1
require "action_dispatch/http/request"
- 1
require "action_dispatch/middleware/exception_wrapper"
- 1
require "action_dispatch/routing/inspector"
- 1
require "action_view"
- 1
module ActionDispatch
# This middleware is responsible for logging exceptions and
# showing a debugging page in case the request is local.
- 1
class DebugExceptions
- 1
cattr_reader :interceptors, instance_accessor: false, default: []
- 1
def self.register_interceptor(object = nil, &block)
interceptor = object || block
interceptors << interceptor
end
- 1
def initialize(app, routes_app = nil, response_format = :default, interceptors = self.class.interceptors)
- 42
@app = app
- 42
@routes_app = routes_app
- 42
@response_format = response_format
- 42
@interceptors = interceptors
end
- 1
def call(env)
request = ActionDispatch::Request.new env
_, headers, body = response = @app.call(env)
if headers["X-Cascade"] == "pass"
body.close if body.respond_to?(:close)
raise ActionController::RoutingError, "No route matches [#{env['REQUEST_METHOD']}] #{env['PATH_INFO'].inspect}"
end
response
rescue Exception => exception
invoke_interceptors(request, exception)
raise exception unless request.show_exceptions?
render_exception(request, exception)
end
- 1
private
- 1
def invoke_interceptors(request, exception)
backtrace_cleaner = request.get_header("action_dispatch.backtrace_cleaner")
wrapper = ExceptionWrapper.new(backtrace_cleaner, exception)
@interceptors.each do |interceptor|
interceptor.call(request, exception)
rescue Exception
log_error(request, wrapper)
end
end
- 1
def render_exception(request, exception)
backtrace_cleaner = request.get_header("action_dispatch.backtrace_cleaner")
wrapper = ExceptionWrapper.new(backtrace_cleaner, exception)
log_error(request, wrapper)
if request.get_header("action_dispatch.show_detailed_exceptions")
begin
content_type = request.formats.first
rescue Mime::Type::InvalidMimeType
content_type = Mime[:text]
end
if api_request?(content_type)
render_for_api_request(content_type, wrapper)
else
render_for_browser_request(request, wrapper)
end
else
raise exception
end
end
- 1
def render_for_browser_request(request, wrapper)
template = create_template(request, wrapper)
file = "rescues/#{wrapper.rescue_template}"
if request.xhr?
body = template.render(template: file, layout: false, formats: [:text])
format = "text/plain"
else
body = template.render(template: file, layout: "rescues/layout")
format = "text/html"
end
render(wrapper.status_code, body, format)
end
- 1
def render_for_api_request(content_type, wrapper)
body = {
status: wrapper.status_code,
error: Rack::Utils::HTTP_STATUS_CODES.fetch(
wrapper.status_code,
Rack::Utils::HTTP_STATUS_CODES[500]
),
exception: wrapper.exception.inspect,
traces: wrapper.traces
}
to_format = "to_#{content_type.to_sym}"
if content_type && body.respond_to?(to_format)
formatted_body = body.public_send(to_format)
format = content_type
else
formatted_body = body.to_json
format = Mime[:json]
end
render(wrapper.status_code, formatted_body, format)
end
- 1
def create_template(request, wrapper)
DebugView.new(
request: request,
exception_wrapper: wrapper,
exception: wrapper.exception,
traces: wrapper.traces,
show_source_idx: wrapper.source_to_show_id,
trace_to_show: wrapper.trace_to_show,
routes_inspector: routes_inspector(wrapper.exception),
source_extracts: wrapper.source_extracts,
line_number: wrapper.line_number,
file: wrapper.file
)
end
- 1
def render(status, body, format)
[status, { "Content-Type" => "#{format}; charset=#{Response.default_charset}", "Content-Length" => body.bytesize.to_s }, [body]]
end
- 1
def log_error(request, wrapper)
logger = logger(request)
return unless logger
exception = wrapper.exception
trace = wrapper.exception_trace
message = []
message << " "
message << "#{exception.class} (#{exception.message}):"
message.concat(exception.annotated_source_code) if exception.respond_to?(:annotated_source_code)
message << " "
message.concat(trace)
log_array(logger, message)
end
- 1
def log_array(logger, array)
lines = Array(array)
return if lines.empty?
if logger.formatter && logger.formatter.respond_to?(:tags_text)
logger.fatal lines.join("\n#{logger.formatter.tags_text}")
else
logger.fatal lines.join("\n")
end
end
- 1
def logger(request)
request.logger || ActionView::Base.logger || stderr_logger
end
- 1
def stderr_logger
@stderr_logger ||= ActiveSupport::Logger.new($stderr)
end
- 1
def routes_inspector(exception)
if @routes_app.respond_to?(:routes) && (exception.is_a?(ActionController::RoutingError) || exception.is_a?(ActionView::Template::Error))
ActionDispatch::Routing::RoutesInspector.new(@routes_app.routes.routes)
end
end
- 1
def api_request?(content_type)
@response_format == :api && !content_type.html?
end
end
end
# frozen_string_literal: true
module ActionDispatch
# This middleware can be used to diagnose deadlocks in the autoload interlock.
#
# To use it, insert it near the top of the middleware stack, using
# <tt>config/application.rb</tt>:
#
# config.middleware.insert_before Rack::Sendfile, ActionDispatch::DebugLocks
#
# After restarting the application and re-triggering the deadlock condition,
# <tt>/rails/locks</tt> will show a summary of all threads currently known to
# the interlock, which lock level they are holding or awaiting, and their
# current backtrace.
#
# Generally a deadlock will be caused by the interlock conflicting with some
# other external lock or blocking I/O call. These cannot be automatically
# identified, but should be visible in the displayed backtraces.
#
# NOTE: The formatting and content of this middleware's output is intended for
# human consumption, and should be expected to change between releases.
#
# This middleware exposes operational details of the server, with no access
# control. It should only be enabled when in use, and removed thereafter.
class DebugLocks
def initialize(app, path = "/rails/locks")
@app = app
@path = path
end
def call(env)
req = ActionDispatch::Request.new env
if req.get?
path = req.path_info.chomp("/")
if path == @path
return render_details(req)
end
end
@app.call(env)
end
private
def render_details(req)
threads = ActiveSupport::Dependencies.interlock.raw_state do |raw_threads|
# The Interlock itself comes to a complete halt as long as this block
# is executing. That gives us a more consistent picture of everything,
# but creates a pretty strong Observer Effect.
#
# Most directly, that means we need to do as little as possible in
# this block. More widely, it means this middleware should remain a
# strictly diagnostic tool (to be used when something has gone wrong),
# and not for any sort of general monitoring.
raw_threads.each.with_index do |(thread, info), idx|
info[:index] = idx
info[:backtrace] = thread.backtrace
end
raw_threads
end
str = threads.map do |thread, info|
if info[:exclusive]
lock_state = +"Exclusive"
elsif info[:sharing] > 0
lock_state = +"Sharing"
lock_state << " x#{info[:sharing]}" if info[:sharing] > 1
else
lock_state = +"No lock"
end
if info[:waiting]
lock_state << " (yielded share)"
end
msg = +"Thread #{info[:index]} [0x#{thread.__id__.to_s(16)} #{thread.status || 'dead'}] #{lock_state}\n"
if info[:sleeper]
msg << " Waiting in #{info[:sleeper]}"
msg << " to #{info[:purpose].to_s.inspect}" unless info[:purpose].nil?
msg << "\n"
if info[:compatible]
compat = info[:compatible].map { |c| c == false ? "share" : c.to_s.inspect }
msg << " may be pre-empted for: #{compat.join(', ')}\n"
end
blockers = threads.values.select { |binfo| blocked_by?(info, binfo, threads.values) }
msg << " blocked by: #{blockers.map { |i| i[:index] }.join(', ')}\n" if blockers.any?
end
blockees = threads.values.select { |binfo| blocked_by?(binfo, info, threads.values) }
msg << " blocking: #{blockees.map { |i| i[:index] }.join(', ')}\n" if blockees.any?
msg << "\n#{info[:backtrace].join("\n")}\n" if info[:backtrace]
end.join("\n\n---\n\n\n")
[200, { "Content-Type" => "text/plain", "Content-Length" => str.size }, [str]]
end
def blocked_by?(victim, blocker, all_threads)
return false if victim.equal?(blocker)
case victim[:sleeper]
when :start_sharing
blocker[:exclusive] ||
(!victim[:waiting] && blocker[:compatible] && !blocker[:compatible].include?(false))
when :start_exclusive
blocker[:sharing] > 0 ||
blocker[:exclusive] ||
(blocker[:compatible] && !blocker[:compatible].include?(victim[:purpose]))
when :yield_shares
blocker[:exclusive]
when :stop_exclusive
blocker[:exclusive] ||
victim[:compatible] &&
victim[:compatible].include?(blocker[:purpose]) &&
all_threads.all? { |other| !other[:compatible] || blocker.equal?(other) || other[:compatible].include?(blocker[:purpose]) }
end
end
end
end
# frozen_string_literal: true
require "pp"
require "action_view"
require "action_view/base"
module ActionDispatch
class DebugView < ActionView::Base # :nodoc:
RESCUES_TEMPLATE_PATH = File.expand_path("templates", __dir__)
def initialize(assigns)
paths = [RESCUES_TEMPLATE_PATH]
lookup_context = ActionView::LookupContext.new(paths)
super(lookup_context, assigns)
end
def compiled_method_container
self.class
end
def debug_params(params)
clean_params = params.clone
clean_params.delete("action")
clean_params.delete("controller")
if clean_params.empty?
"None"
else
PP.pp(clean_params, +"", 200)
end
end
def debug_headers(headers)
if headers.present?
headers.inspect.gsub(",", ",\n")
else
"None"
end
end
def debug_hash(object)
object.to_hash.sort_by { |k, _| k.to_s }.map { |k, v| "#{k}: #{v.inspect rescue $!.message}" }.join("\n")
end
def render(*)
logger = ActionView::Base.logger
if logger && logger.respond_to?(:silence)
logger.silence { super }
else
super
end
end
def protect_against_forgery?
false
end
def params_valid?
@request.parameters
rescue ActionController::BadRequest
false
end
end
end
# frozen_string_literal: true
- 1
require "active_support/core_ext/module/attribute_accessors"
- 1
require "rack/utils"
- 1
module ActionDispatch
- 1
class ExceptionWrapper
- 1
cattr_accessor :rescue_responses, default: Hash.new(:internal_server_error).merge!(
"ActionController::RoutingError" => :not_found,
"AbstractController::ActionNotFound" => :not_found,
"ActionController::MethodNotAllowed" => :method_not_allowed,
"ActionController::UnknownHttpMethod" => :method_not_allowed,
"ActionController::NotImplemented" => :not_implemented,
"ActionController::UnknownFormat" => :not_acceptable,
"Mime::Type::InvalidMimeType" => :not_acceptable,
"ActionController::MissingExactTemplate" => :not_acceptable,
"ActionController::InvalidAuthenticityToken" => :unprocessable_entity,
"ActionController::InvalidCrossOriginRequest" => :unprocessable_entity,
"ActionDispatch::Http::Parameters::ParseError" => :bad_request,
"ActionController::BadRequest" => :bad_request,
"ActionController::ParameterMissing" => :bad_request,
"Rack::QueryParser::ParameterTypeError" => :bad_request,
"Rack::QueryParser::InvalidParameterError" => :bad_request
)
- 1
cattr_accessor :rescue_templates, default: Hash.new("diagnostics").merge!(
"ActionView::MissingTemplate" => "missing_template",
"ActionController::RoutingError" => "routing_error",
"AbstractController::ActionNotFound" => "unknown_action",
"ActiveRecord::StatementInvalid" => "invalid_statement",
"ActionView::Template::Error" => "template_error",
"ActionController::MissingExactTemplate" => "missing_exact_template",
)
- 1
cattr_accessor :wrapper_exceptions, default: [
"ActionView::Template::Error"
]
- 1
cattr_accessor :silent_exceptions, default: [
"ActionController::RoutingError"
]
- 1
attr_reader :backtrace_cleaner, :exception, :wrapped_causes, :line_number, :file
- 1
def initialize(backtrace_cleaner, exception)
@backtrace_cleaner = backtrace_cleaner
@exception = exception
@exception_class_name = @exception.class.name
@wrapped_causes = wrapped_causes_for(exception, backtrace_cleaner)
expand_backtrace if exception.is_a?(SyntaxError) || exception.cause.is_a?(SyntaxError)
end
- 1
def unwrapped_exception
if wrapper_exceptions.include?(@exception_class_name)
exception.cause
else
exception
end
end
- 1
def rescue_template
@@rescue_templates[@exception_class_name]
end
- 1
def status_code
self.class.status_code_for_exception(unwrapped_exception.class.name)
end
- 1
def exception_trace
trace = application_trace
trace = framework_trace if trace.empty? && !silent_exceptions.include?(@exception_class_name)
trace
end
- 1
def application_trace
clean_backtrace(:silent)
end
- 1
def framework_trace
clean_backtrace(:noise)
end
- 1
def full_trace
clean_backtrace(:all)
end
- 1
def traces
application_trace_with_ids = []
framework_trace_with_ids = []
full_trace_with_ids = []
full_trace.each_with_index do |trace, idx|
trace_with_id = {
exception_object_id: @exception.object_id,
id: idx,
trace: trace
}
if application_trace.include?(trace)
application_trace_with_ids << trace_with_id
else
framework_trace_with_ids << trace_with_id
end
full_trace_with_ids << trace_with_id
end
{
"Application Trace" => application_trace_with_ids,
"Framework Trace" => framework_trace_with_ids,
"Full Trace" => full_trace_with_ids
}
end
- 1
def self.status_code_for_exception(class_name)
Rack::Utils.status_code(@@rescue_responses[class_name])
end
- 1
def source_extracts
backtrace.map do |trace|
file, line_number = extract_file_and_line_number(trace)
{
code: source_fragment(file, line_number),
line_number: line_number
}
end
end
- 1
def trace_to_show
if traces["Application Trace"].empty? && rescue_template != "routing_error"
"Full Trace"
else
"Application Trace"
end
end
- 1
def source_to_show_id
(traces[trace_to_show].first || {})[:id]
end
- 1
private
- 1
def backtrace
Array(@exception.backtrace)
end
- 1
def causes_for(exception)
return enum_for(__method__, exception) unless block_given?
yield exception while exception = exception.cause
end
- 1
def wrapped_causes_for(exception, backtrace_cleaner)
causes_for(exception).map { |cause| self.class.new(backtrace_cleaner, cause) }
end
- 1
def clean_backtrace(*args)
if backtrace_cleaner
backtrace_cleaner.clean(backtrace, *args)
else
backtrace
end
end
- 1
def source_fragment(path, line)
return unless Rails.respond_to?(:root) && Rails.root
full_path = Rails.root.join(path)
if File.exist?(full_path)
File.open(full_path, "r") do |file|
start = [line - 3, 0].max
lines = file.each_line.drop(start).take(6)
Hash[*(start + 1..(lines.count + start)).zip(lines).flatten]
end
end
end
- 1
def extract_file_and_line_number(trace)
# Split by the first colon followed by some digits, which works for both
# Windows and Unix path styles.
file, line = trace.match(/^(.+?):(\d+).*$/, &:captures) || trace
[file, line.to_i]
end
- 1
def expand_backtrace
@exception.backtrace.unshift(
@exception.to_s.split("\n")
).flatten!
end
end
end
# frozen_string_literal: true
require "rack/body_proxy"
module ActionDispatch
class Executor
def initialize(app, executor)
@app, @executor = app, executor
end
def call(env)
state = @executor.run!
begin
response = @app.call(env)
returned = response << ::Rack::BodyProxy.new(response.pop) { state.complete! }
ensure
state.complete! unless returned
end
end
end
end
# frozen_string_literal: true
- 1
require "active_support/core_ext/hash/keys"
- 1
module ActionDispatch
# The flash provides a way to pass temporary primitive-types (String, Array, Hash) between actions. Anything you place in the flash will be exposed
# to the very next action and then cleared out. This is a great way of doing notices and alerts, such as a create
# action that sets <tt>flash[:notice] = "Post successfully created"</tt> before redirecting to a display action that can
# then expose the flash to its template. Actually, that exposure is automatically done.
#
# class PostsController < ActionController::Base
# def create
# # save post
# flash[:notice] = "Post successfully created"
# redirect_to @post
# end
#
# def show
# # doesn't need to assign the flash notice to the template, that's done automatically
# end
# end
#
# show.html.erb
# <% if flash[:notice] %>
# <div class="notice"><%= flash[:notice] %></div>
# <% end %>
#
# Since the +notice+ and +alert+ keys are a common idiom, convenience accessors are available:
#
# flash.alert = "You must be logged in"
# flash.notice = "Post successfully created"
#
# This example places a string in the flash. And of course, you can put as many as you like at a time too. If you want to pass
# non-primitive types, you will have to handle that in your application. Example: To show messages with links, you will have to
# use sanitize helper.
#
# Just remember: They'll be gone by the time the next action has been performed.
#
# See docs on the FlashHash class for more details about the flash.
- 1
class Flash
- 1
KEY = "action_dispatch.request.flash_hash"
- 1
module RequestMethods
# Access the contents of the flash. Use <tt>flash["notice"]</tt> to
# read a notice you put there or <tt>flash["notice"] = "hello"</tt>
# to put a new one.
- 1
def flash
flash = flash_hash
return flash if flash
self.flash = Flash::FlashHash.from_session_value(session["flash"])
end
- 1
def flash=(flash)
set_header Flash::KEY, flash
end
- 1
def flash_hash # :nodoc:
get_header Flash::KEY
end
- 1
def commit_flash # :nodoc:
session = self.session || {}
flash_hash = self.flash_hash
if flash_hash && (flash_hash.present? || session.key?("flash"))
session["flash"] = flash_hash.to_session_value
self.flash = flash_hash.dup
end
if (!session.respond_to?(:loaded?) || session.loaded?) && # reset_session uses {}, which doesn't implement #loaded?
session.key?("flash") && session["flash"].nil?
session.delete("flash")
end
end
- 1
def reset_session # :nodoc:
super
self.flash = nil
end
end
- 1
class FlashNow #:nodoc:
- 1
attr_accessor :flash
- 1
def initialize(flash)
@flash = flash
end
- 1
def []=(k, v)
k = k.to_s
@flash[k] = v
@flash.discard(k)
v
end
- 1
def [](k)
@flash[k.to_s]
end
# Convenience accessor for <tt>flash.now[:alert]=</tt>.
- 1
def alert=(message)
self[:alert] = message
end
# Convenience accessor for <tt>flash.now[:notice]=</tt>.
- 1
def notice=(message)
self[:notice] = message
end
end
- 1
class FlashHash
- 1
include Enumerable
- 1
def self.from_session_value(value) #:nodoc:
case value
when FlashHash # Rails 3.1, 3.2
flashes = value.instance_variable_get(:@flashes)
if discard = value.instance_variable_get(:@used)
flashes.except!(*discard)
end
new(flashes, flashes.keys)
when Hash # Rails 4.0
flashes = value["flashes"]
if discard = value["discard"]
flashes.except!(*discard)
end
new(flashes, flashes.keys)
else
new
end
end
# Builds a hash containing the flashes to keep for the next request.
# If there are none to keep, returns +nil+.
- 1
def to_session_value #:nodoc:
flashes_to_keep = @flashes.except(*@discard)
return nil if flashes_to_keep.empty?
{ "discard" => [], "flashes" => flashes_to_keep }
end
- 1
def initialize(flashes = {}, discard = []) #:nodoc:
@discard = Set.new(stringify_array(discard))
@flashes = flashes.stringify_keys
@now = nil
end
- 1
def initialize_copy(other)
if other.now_is_loaded?
@now = other.now.dup
@now.flash = self
end
super
end
- 1
def []=(k, v)
k = k.to_s
@discard.delete k
@flashes[k] = v
end
- 1
def [](k)
@flashes[k.to_s]
end
- 1
def update(h) #:nodoc:
@discard.subtract stringify_array(h.keys)
@flashes.update h.stringify_keys
self
end
- 1
def keys
@flashes.keys
end
- 1
def key?(name)
@flashes.key? name.to_s
end
- 1
def delete(key)
key = key.to_s
@discard.delete key
@flashes.delete key
self
end
- 1
def to_hash
@flashes.dup
end
- 1
def empty?
@flashes.empty?
end
- 1
def clear
@discard.clear
@flashes.clear
end
- 1
def each(&block)
@flashes.each(&block)
end
- 1
alias :merge! :update
- 1
def replace(h) #:nodoc:
@discard.clear
@flashes.replace h.stringify_keys
self
end
# Sets a flash that will not be available to the next action, only to the current.
#
# flash.now[:message] = "Hello current action"
#
# This method enables you to use the flash as a central messaging system in your app.
# When you need to pass an object to the next action, you use the standard flash assign (<tt>[]=</tt>).
# When you need to pass an object to the current action, you use <tt>now</tt>, and your object will
# vanish when the current action is done.
#
# Entries set via <tt>now</tt> are accessed the same way as standard entries: <tt>flash['my-key']</tt>.
#
# Also, brings two convenience accessors:
#
# flash.now.alert = "Beware now!"
# # Equivalent to flash.now[:alert] = "Beware now!"
#
# flash.now.notice = "Good luck now!"
# # Equivalent to flash.now[:notice] = "Good luck now!"
- 1
def now
@now ||= FlashNow.new(self)
end
# Keeps either the entire current flash or a specific flash entry available for the next action:
#
# flash.keep # keeps the entire flash
# flash.keep(:notice) # keeps only the "notice" entry, the rest of the flash is discarded
- 1
def keep(k = nil)
k = k.to_s if k
@discard.subtract Array(k || keys)
k ? self[k] : self
end
# Marks the entire flash or a single flash entry to be discarded by the end of the current action:
#
# flash.discard # discard the entire flash at the end of the current action
# flash.discard(:warning) # discard only the "warning" entry at the end of the current action
- 1
def discard(k = nil)
k = k.to_s if k
@discard.merge Array(k || keys)
k ? self[k] : self
end
# Mark for removal entries that were kept, and delete unkept ones.
#
# This method is called automatically by filters, so you generally don't need to care about it.
- 1
def sweep #:nodoc:
@discard.each { |k| @flashes.delete k }
@discard.replace @flashes.keys
end
# Convenience accessor for <tt>flash[:alert]</tt>.
- 1
def alert
self[:alert]
end
# Convenience accessor for <tt>flash[:alert]=</tt>.
- 1
def alert=(message)
self[:alert] = message
end
# Convenience accessor for <tt>flash[:notice]</tt>.
- 1
def notice
self[:notice]
end
# Convenience accessor for <tt>flash[:notice]=</tt>.
- 1
def notice=(message)
self[:notice] = message
end
- 1
protected
- 1
def now_is_loaded?
@now
end
- 1
private
- 1
def stringify_array(array) # :doc:
array.map do |item|
item.kind_of?(Symbol) ? item.to_s : item
end
end
end
- 39
def self.new(app) app; end
end
- 1
class Request
- 1
prepend Flash::RequestMethods
end
end
# frozen_string_literal: true
require "action_dispatch/http/request"
module ActionDispatch
# This middleware guards from DNS rebinding attacks by explicitly permitting
# the hosts a request can be sent to.
#
# When a request comes to an unauthorized host, the +response_app+
# application will be executed and rendered. If no +response_app+ is given, a
# default one will run, which responds with +403 Forbidden+.
class HostAuthorization
class Permissions # :nodoc:
def initialize(hosts)
@hosts = sanitize_hosts(hosts)
end
def empty?
@hosts.empty?
end
def allows?(host)
@hosts.any? do |allowed|
allowed === host
rescue
# IPAddr#=== raises an error if you give it a hostname instead of
# IP. Treat similar errors as blocked access.
false
end
end
private
def sanitize_hosts(hosts)
Array(hosts).map do |host|
case host
when Regexp then sanitize_regexp(host)
when String then sanitize_string(host)
else host
end
end
end
def sanitize_regexp(host)
/\A#{host}\z/
end
def sanitize_string(host)
if host.start_with?(".")
/\A(.+\.)?#{Regexp.escape(host[1..-1])}\z/
else
host
end
end
end
DEFAULT_RESPONSE_APP = -> env do
request = Request.new(env)
format = request.xhr? ? "text/plain" : "text/html"
template = DebugView.new(host: request.host)
body = template.render(template: "rescues/blocked_host", layout: "rescues/layout")
[403, {
"Content-Type" => "#{format}; charset=#{Response.default_charset}",
"Content-Length" => body.bytesize.to_s,
}, [body]]
end
def initialize(app, hosts, response_app = nil)
@app = app
@permissions = Permissions.new(hosts)
@response_app = response_app || DEFAULT_RESPONSE_APP
end
def call(env)
return @app.call(env) if @permissions.empty?
request = Request.new(env)
if authorized?(request)
mark_as_authorized(request)
@app.call(env)
else
@response_app.call(env)
end
end
private
def authorized?(request)
origin_host = request.get_header("HTTP_HOST").to_s.sub(/:\d+\z/, "")
forwarded_host = request.x_forwarded_host.to_s.split(/,\s?/).last.to_s.sub(/:\d+\z/, "")
@permissions.allows?(origin_host) &&
(forwarded_host.blank? || @permissions.allows?(forwarded_host))
end
def mark_as_authorized(request)
request.set_header("action_dispatch.authorized_host", request.host)
end
end
end
# frozen_string_literal: true
- 1
module ActionDispatch
# When called, this middleware renders an error page. By default if an HTML
# response is expected it will render static error pages from the <tt>/public</tt>
# directory. For example when this middleware receives a 500 response it will
# render the template found in <tt>/public/500.html</tt>.
# If an internationalized locale is set, this middleware will attempt to render
# the template in <tt>/public/500.<locale>.html</tt>. If an internationalized template
# is not found it will fall back on <tt>/public/500.html</tt>.
#
# When a request with a content type other than HTML is made, this middleware
# will attempt to convert error information into the appropriate response type.
- 1
class PublicExceptions
- 1
attr_accessor :public_path
- 1
def initialize(public_path)
- 39
@public_path = public_path
end
- 1
def call(env)
request = ActionDispatch::Request.new(env)
status = request.path_info[1..-1].to_i
begin
content_type = request.formats.first
rescue Mime::Type::InvalidMimeType
content_type = Mime[:text]
end
body = { status: status, error: Rack::Utils::HTTP_STATUS_CODES.fetch(status, Rack::Utils::HTTP_STATUS_CODES[500]) }
render(status, content_type, body)
end
- 1
private
- 1
def render(status, content_type, body)
format = "to_#{content_type.to_sym}" if content_type
if format && body.respond_to?(format)
render_format(status, content_type, body.public_send(format))
else
render_html(status)
end
end
- 1
def render_format(status, content_type, body)
[status, { "Content-Type" => "#{content_type}; charset=#{ActionDispatch::Response.default_charset}",
"Content-Length" => body.bytesize.to_s }, [body]]
end
- 1
def render_html(status)
path = "#{public_path}/#{status}.#{I18n.locale}.html"
path = "#{public_path}/#{status}.html" unless (found = File.exist?(path))
if found || File.exist?(path)
render_format(status, "text/html", File.read(path))
else
[404, { "X-Cascade" => "pass" }, []]
end
end
end
end
# frozen_string_literal: true
module ActionDispatch
# ActionDispatch::Reloader wraps the request with callbacks provided by ActiveSupport::Reloader
# callbacks, intended to assist with code reloading during development.
#
# By default, ActionDispatch::Reloader is included in the middleware stack
# only in the development environment; specifically, when +config.cache_classes+
# is false.
class Reloader < Executor
end
end
# frozen_string_literal: true
require "ipaddr"
module ActionDispatch
# This middleware calculates the IP address of the remote client that is
# making the request. It does this by checking various headers that could
# contain the address, and then picking the last-set address that is not
# on the list of trusted IPs. This follows the precedent set by e.g.
# {the Tomcat server}[https://issues.apache.org/bugzilla/show_bug.cgi?id=50453],
# with {reasoning explained at length}[https://blog.gingerlime.com/2012/rails-ip-spoofing-vulnerabilities-and-protection]
# by @gingerlime. A more detailed explanation of the algorithm is given
# at GetIp#calculate_ip.
#
# Some Rack servers concatenate repeated headers, like {HTTP RFC 2616}[https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2]
# requires. Some Rack servers simply drop preceding headers, and only report
# the value that was {given in the last header}[https://andre.arko.net/2011/12/26/repeated-headers-and-ruby-web-servers].
# If you are behind multiple proxy servers (like NGINX to HAProxy to Unicorn)
# then you should test your Rack server to make sure your data is good.
#
# IF YOU DON'T USE A PROXY, THIS MAKES YOU VULNERABLE TO IP SPOOFING.
# This middleware assumes that there is at least one proxy sitting around
# and setting headers with the client's remote IP address. If you don't use
# a proxy, because you are hosted on e.g. Heroku without SSL, any client can
# claim to have any IP address by setting the X-Forwarded-For header. If you
# care about that, then you need to explicitly drop or ignore those headers
# sometime before this middleware runs.
class RemoteIp
class IpSpoofAttackError < StandardError; end
# The default trusted IPs list simply includes IP addresses that are
# guaranteed by the IP specification to be private addresses. Those will
# not be the ultimate client IP in production, and so are discarded. See
# https://en.wikipedia.org/wiki/Private_network for details.
TRUSTED_PROXIES = [
"127.0.0.0/8", # localhost IPv4 range, per RFC-3330
"::1", # localhost IPv6
"fc00::/7", # private IPv6 range fc00::/7
"10.0.0.0/8", # private IPv4 range 10.x.x.x
"172.16.0.0/12", # private IPv4 range 172.16.0.0 .. 172.31.255.255
"192.168.0.0/16", # private IPv4 range 192.168.x.x
].map { |proxy| IPAddr.new(proxy) }
attr_reader :check_ip, :proxies
# Create a new +RemoteIp+ middleware instance.
#
# The +ip_spoofing_check+ option is on by default. When on, an exception
# is raised if it looks like the client is trying to lie about its own IP
# address. It makes sense to turn off this check on sites aimed at non-IP
# clients (like WAP devices), or behind proxies that set headers in an
# incorrect or confusing way (like AWS ELB).
#
# The +custom_proxies+ argument can take an Array of string, IPAddr, or
# Regexp objects which will be used instead of +TRUSTED_PROXIES+. If a
# single string, IPAddr, or Regexp object is provided, it will be used in
# addition to +TRUSTED_PROXIES+. Any proxy setup will put the value you
# want in the middle (or at the beginning) of the X-Forwarded-For list,
# with your proxy servers after it. If your proxies aren't removed, pass
# them in via the +custom_proxies+ parameter. That way, the middleware will
# ignore those IP addresses, and return the one that you want.
def initialize(app, ip_spoofing_check = true, custom_proxies = nil)
@app = app
@check_ip = ip_spoofing_check
@proxies = if custom_proxies.blank?
TRUSTED_PROXIES
elsif custom_proxies.respond_to?(:any?)
custom_proxies
else
Array(custom_proxies) + TRUSTED_PROXIES
end
end
# Since the IP address may not be needed, we store the object here
# without calculating the IP to keep from slowing down the majority of
# requests. For those requests that do need to know the IP, the
# GetIp#calculate_ip method will calculate the memoized client IP address.
def call(env)
req = ActionDispatch::Request.new env
req.remote_ip = GetIp.new(req, check_ip, proxies)
@app.call(req.env)
end
# The GetIp class exists as a way to defer processing of the request data
# into an actual IP address. If the ActionDispatch::Request#remote_ip method
# is called, this class will calculate the value and then memoize it.
class GetIp
def initialize(req, check_ip, proxies)
@req = req
@check_ip = check_ip
@proxies = proxies
end
# Sort through the various IP address headers, looking for the IP most
# likely to be the address of the actual remote client making this
# request.
#
# REMOTE_ADDR will be correct if the request is made directly against the
# Ruby process, on e.g. Heroku. When the request is proxied by another
# server like HAProxy or NGINX, the IP address that made the original
# request will be put in an X-Forwarded-For header. If there are multiple
# proxies, that header may contain a list of IPs. Other proxy services
# set the Client-Ip header instead, so we check that too.
#
# As discussed in {this post about Rails IP Spoofing}[https://blog.gingerlime.com/2012/rails-ip-spoofing-vulnerabilities-and-protection/],
# while the first IP in the list is likely to be the "originating" IP,
# it could also have been set by the client maliciously.
#
# In order to find the first address that is (probably) accurate, we
# take the list of IPs, remove known and trusted proxies, and then take
# the last address left, which was presumably set by one of those proxies.
def calculate_ip
# Set by the Rack web server, this is a single value.
remote_addr = ips_from(@req.remote_addr).last
# Could be a CSV list and/or repeated headers that were concatenated.
client_ips = ips_from(@req.client_ip).reverse
forwarded_ips = ips_from(@req.x_forwarded_for).reverse
# +Client-Ip+ and +X-Forwarded-For+ should not, generally, both be set.
# If they are both set, it means that either:
#
# 1) This request passed through two proxies with incompatible IP header
# conventions.
# 2) The client passed one of +Client-Ip+ or +X-Forwarded-For+
# (whichever the proxy servers weren't using) themselves.
#
# Either way, there is no way for us to determine which header is the
# right one after the fact. Since we have no idea, if we are concerned
# about IP spoofing we need to give up and explode. (If you're not
# concerned about IP spoofing you can turn the +ip_spoofing_check+
# option off.)
should_check_ip = @check_ip && client_ips.last && forwarded_ips.last
if should_check_ip && !forwarded_ips.include?(client_ips.last)
# We don't know which came from the proxy, and which from the user
raise IpSpoofAttackError, "IP spoofing attack?! " \
"HTTP_CLIENT_IP=#{@req.client_ip.inspect} " \
"HTTP_X_FORWARDED_FOR=#{@req.x_forwarded_for.inspect}"
end
# We assume these things about the IP headers:
#
# - X-Forwarded-For will be a list of IPs, one per proxy, or blank
# - Client-Ip is propagated from the outermost proxy, or is blank
# - REMOTE_ADDR will be the IP that made the request to Rack
ips = [forwarded_ips, client_ips].flatten.compact
# If every single IP option is in the trusted list, return the IP
# that's furthest away
filter_proxies(ips + [remote_addr]).first || ips.last || remote_addr
end
# Memoizes the value returned by #calculate_ip and returns it for
# ActionDispatch::Request to use.
def to_s
@ip ||= calculate_ip
end
private
def ips_from(header) # :doc:
return [] unless header
# Split the comma-separated list into an array of strings.
ips = header.strip.split(/[,\s]+/)
ips.select do |ip|
# Only return IPs that are valid according to the IPAddr#new method.
range = IPAddr.new(ip).to_range
# We want to make sure nobody is sneaking a netmask in.
range.begin == range.end
rescue ArgumentError
nil
end
end
def filter_proxies(ips) # :doc:
ips.reject do |ip|
@proxies.any? { |proxy| proxy === ip }
end
end
end
end
end
# frozen_string_literal: true
require "securerandom"
require "active_support/core_ext/string/access"
module ActionDispatch
# Makes a unique request id available to the +action_dispatch.request_id+ env variable (which is then accessible
# through <tt>ActionDispatch::Request#request_id</tt> or the alias <tt>ActionDispatch::Request#uuid</tt>) and sends
# the same id to the client via the X-Request-Id header.
#
# The unique request id is either based on the X-Request-Id header in the request, which would typically be generated
# by a firewall, load balancer, or the web server, or, if this header is not available, a random uuid. If the
# header is accepted from the outside world, we sanitize it to a max of 255 chars and alphanumeric and dashes only.
#
# The unique request id can be used to trace a request end-to-end and would typically end up being part of log files
# from multiple pieces of the stack.
class RequestId
X_REQUEST_ID = "X-Request-Id" #:nodoc:
def initialize(app)
@app = app
end
def call(env)
req = ActionDispatch::Request.new env
req.request_id = make_request_id(req.x_request_id)
@app.call(env).tap { |_status, headers, _body| headers[X_REQUEST_ID] = req.request_id }
end
private
def make_request_id(request_id)
if request_id.presence
request_id.gsub(/[^\w\-@]/, "").first(255)
else
internal_request_id
end
end
def internal_request_id
SecureRandom.uuid
end
end
end
# frozen_string_literal: true
- 1
require "rack/utils"
- 1
require "rack/request"
- 1
require "rack/session/abstract/id"
- 1
require "action_dispatch/middleware/cookies"
- 1
require "action_dispatch/request/session"
- 1
module ActionDispatch
- 1
module Session
- 1
class SessionRestoreError < StandardError #:nodoc:
- 1
def initialize
super("Session contains objects whose class definition isn't available.\n" \
"Remember to require the classes for all objects kept in the session.\n" \
"(Original exception: #{$!.message} [#{$!.class}])\n")
set_backtrace $!.backtrace
end
end
- 1
module Compatibility
- 1
def initialize(app, options = {})
- 1
options[:key] ||= "_session_id"
- 1
super
end
- 1
def generate_sid
sid = SecureRandom.hex(16)
sid.encode!(Encoding::UTF_8)
sid
end
- 1
private
- 1
def initialize_sid # :doc:
- 1
@default_options.delete(:sidbits)
- 1
@default_options.delete(:secure_random)
end
- 1
def make_request(env)
ActionDispatch::Request.new env
end
end
- 1
module StaleSessionCheck
- 1
def load_session(env)
stale_session_check! { super }
end
- 1
def extract_session_id(env)
stale_session_check! { super }
end
- 1
def stale_session_check!
yield
rescue ArgumentError => argument_error
if argument_error.message =~ %r{undefined class/module ([\w:]*\w)}
begin
# Note that the regexp does not allow $1 to end with a ':'.
$1.constantize
rescue LoadError, NameError
raise ActionDispatch::Session::SessionRestoreError
end
retry
else
raise
end
end
end
- 1
module SessionObject # :nodoc:
- 1
def prepare_session(req)
Request::Session.create(self, req, @default_options)
end
- 1
def loaded_session?(session)
!session.is_a?(Request::Session) || session.loaded?
end
end
- 1
class AbstractStore < Rack::Session::Abstract::Persisted
- 1
include Compatibility
- 1
include StaleSessionCheck
- 1
include SessionObject
- 1
private
- 1
def set_cookie(request, response, cookie)
request.cookie_jar[key] = cookie
end
end
- 1
class AbstractSecureStore < Rack::Session::Abstract::PersistedSecure
- 1
include Compatibility
- 1
include StaleSessionCheck
- 1
include SessionObject
- 1
def generate_sid
Rack::Session::SessionId.new(super)
end
- 1
private
- 1
def set_cookie(request, response, cookie)
request.cookie_jar[key] = cookie
end
end
end
end
# frozen_string_literal: true
require "action_dispatch/middleware/session/abstract_store"
module ActionDispatch
module Session
# A session store that uses an ActiveSupport::Cache::Store to store the sessions. This store is most useful
# if you don't store critical data in your sessions and you don't need them to live for extended periods
# of time.
#
# ==== Options
# * <tt>cache</tt> - The cache to use. If it is not specified, <tt>Rails.cache</tt> will be used.
# * <tt>expire_after</tt> - The length of time a session will be stored before automatically expiring.
# By default, the <tt>:expires_in</tt> option of the cache is used.
class CacheStore < AbstractSecureStore
def initialize(app, options = {})
@cache = options[:cache] || Rails.cache
options[:expire_after] ||= @cache.options[:expires_in]
super
end
# Get a session from the cache.
def find_session(env, sid)
unless sid && (session = get_session_with_fallback(sid))
sid, session = generate_sid, {}
end
[sid, session]
end
# Set a session in the cache.
def write_session(env, sid, session, options)
key = cache_key(sid.private_id)
if session
@cache.write(key, session, expires_in: options[:expire_after])
else
@cache.delete(key)
end
sid
end
# Remove a session from the cache.
def delete_session(env, sid, options)
@cache.delete(cache_key(sid.private_id))
@cache.delete(cache_key(sid.public_id))
generate_sid
end
private
# Turn the session id into a cache key.
def cache_key(id)
"_session_id:#{id}"
end
def get_session_with_fallback(sid)
@cache.read(cache_key(sid.private_id)) || @cache.read(cache_key(sid.public_id))
end
end
end
end
# frozen_string_literal: true
- 1
require "active_support/core_ext/hash/keys"
- 1
require "action_dispatch/middleware/session/abstract_store"
- 1
require "rack/session/cookie"
- 1
module ActionDispatch
- 1
module Session
# This cookie-based session store is the Rails default. It is
# dramatically faster than the alternatives.
#
# Sessions typically contain at most a user_id and flash message; both fit
# within the 4096 bytes cookie size limit. A CookieOverflow exception is raised if
# you attempt to store more than 4096 bytes of data.
#
# The cookie jar used for storage is automatically configured to be the
# best possible option given your application's configuration.
#
# Your cookies will be encrypted using your apps secret_key_base. This
# goes a step further than signed cookies in that encrypted cookies cannot
# be altered or read by users. This is the default starting in Rails 4.
#
# Configure your session store in an initializer:
#
# Rails.application.config.session_store :cookie_store, key: '_your_app_session'
#
# In the development and test environments your application's secret key base is
# generated by Rails and stored in a temporary file in <tt>tmp/development_secret.txt</tt>.
# In all other environments, it is stored encrypted in the
# <tt>config/credentials.yml.enc</tt> file.
#
# If your application was not updated to Rails 5.2 defaults, the secret_key_base
# will be found in the old <tt>config/secrets.yml</tt> file.
#
# Note that changing your secret_key_base will invalidate all existing session.
# Additionally, you should take care to make sure you are not relying on the
# ability to decode signed cookies generated by your app in external
# applications or JavaScript before changing it.
#
# Because CookieStore extends Rack::Session::Abstract::Persisted, many of the
# options described there can be used to customize the session cookie that
# is generated. For example:
#
# Rails.application.config.session_store :cookie_store, expire_after: 14.days
#
# would set the session cookie to expire automatically 14 days after creation.
# Other useful options include <tt>:key</tt>, <tt>:secure</tt> and
# <tt>:httponly</tt>.
- 1
class CookieStore < AbstractSecureStore
- 1
class SessionId < DelegateClass(Rack::Session::SessionId)
- 1
attr_reader :cookie_value
- 1
def initialize(session_id, cookie_value = {})
super(session_id)
@cookie_value = cookie_value
end
end
- 1
def initialize(app, options = {})
- 1
super(app, options.merge!(cookie_only: true))
end
- 1
def delete_session(req, session_id, options)
new_sid = generate_sid unless options[:drop]
# Reset hash and Assign the new session id
req.set_header("action_dispatch.request.unsigned_session_cookie", new_sid ? { "session_id" => new_sid.public_id } : {})
new_sid
end
- 1
def load_session(req)
stale_session_check! do
data = unpacked_cookie_data(req)
data = persistent_session_id!(data)
[Rack::Session::SessionId.new(data["session_id"]), data]
end
end
- 1
private
- 1
def extract_session_id(req)
stale_session_check! do
sid = unpacked_cookie_data(req)["session_id"]
sid && Rack::Session::SessionId.new(sid)
end
end
- 1
def unpacked_cookie_data(req)
req.fetch_header("action_dispatch.request.unsigned_session_cookie") do |k|
v = stale_session_check! do
if data = get_cookie(req)
data.stringify_keys!
end
data || {}
end
req.set_header k, v
end
end
- 1
def persistent_session_id!(data, sid = nil)
data ||= {}
data["session_id"] ||= sid || generate_sid.public_id
data
end
- 1
def write_session(req, sid, session_data, options)
session_data["session_id"] = sid.public_id
SessionId.new(sid, session_data)
end
- 1
def set_cookie(request, session_id, cookie)
cookie_jar(request)[@key] = cookie
end
- 1
def get_cookie(req)
cookie_jar(req)[@key]
end
- 1
def cookie_jar(request)
request.cookie_jar.signed_or_encrypted
end
end
end
end
# frozen_string_literal: true
require "action_dispatch/middleware/session/abstract_store"
begin
require "rack/session/dalli"
rescue LoadError => e
$stderr.puts "You don't have dalli installed in your application. Please add it to your Gemfile and run bundle install"
raise e
end
module ActionDispatch
module Session
# A session store that uses MemCache to implement storage.
#
# ==== Options
# * <tt>expire_after</tt> - The length of time a session will be stored before automatically expiring.
class MemCacheStore < Rack::Session::Dalli
include Compatibility
include StaleSessionCheck
include SessionObject
def initialize(app, options = {})
options[:expire_after] ||= options[:expires]
super
end
end
end
end
# frozen_string_literal: true
- 1
require "action_dispatch/http/request"
- 1
require "action_dispatch/middleware/exception_wrapper"
- 1
module ActionDispatch
# This middleware rescues any exception returned by the application
# and calls an exceptions app that will wrap it in a format for the end user.
#
# The exceptions app should be passed as parameter on initialization
# of ShowExceptions. Every time there is an exception, ShowExceptions will
# store the exception in env["action_dispatch.exception"], rewrite the
# PATH_INFO to the exception status code and call the Rack app.
#
# If the application returns a "X-Cascade" pass response, this middleware
# will send an empty response as result with the correct status code.
# If any exception happens inside the exceptions app, this middleware
# catches the exceptions and returns a FAILSAFE_RESPONSE.
- 1
class ShowExceptions
- 1
FAILSAFE_RESPONSE = [500, { "Content-Type" => "text/plain" },
["500 Internal Server Error\n" \
"If you are the administrator of this website, then please read this web " \
"application's log file and/or the web server's log file to find out what " \
"went wrong."]]
- 1
def initialize(app, exceptions_app)
- 37
@app = app
- 37
@exceptions_app = exceptions_app
end
- 1
def call(env)
request = ActionDispatch::Request.new env
@app.call(env)
rescue Exception => exception
if request.show_exceptions?
render_exception(request, exception)
else
raise exception
end
end
- 1
private
- 1
def render_exception(request, exception)
backtrace_cleaner = request.get_header "action_dispatch.backtrace_cleaner"
wrapper = ExceptionWrapper.new(backtrace_cleaner, exception)
status = wrapper.status_code
request.set_header "action_dispatch.exception", wrapper.unwrapped_exception
request.set_header "action_dispatch.original_path", request.path_info
request.path_info = "/#{status}"
response = @exceptions_app.call(request.env)
response[1]["X-Cascade"] == "pass" ? pass_response(status) : response
rescue Exception => failsafe_error
$stderr.puts "Error during failsafe response: #{failsafe_error}\n #{failsafe_error.backtrace * "\n "}"
FAILSAFE_RESPONSE
end
- 1
def pass_response(status)
[status, { "Content-Type" => "text/html; charset=#{Response.default_charset}", "Content-Length" => "0" }, []]
end
end
end
# frozen_string_literal: true
module ActionDispatch
# This middleware is added to the stack when <tt>config.force_ssl = true</tt>, and is passed
# the options set in +config.ssl_options+. It does three jobs to enforce secure HTTP
# requests:
#
# 1. <b>TLS redirect</b>: Permanently redirects +http://+ requests to +https://+
# with the same URL host, path, etc. Enabled by default. Set +config.ssl_options+
# to modify the destination URL
# (e.g. <tt>redirect: { host: "secure.widgets.com", port: 8080 }</tt>), or set
# <tt>redirect: false</tt> to disable this feature.
#
# Requests can opt-out of redirection with +exclude+:
#
# config.ssl_options = { redirect: { exclude: -> request { /healthcheck/.match?(request.path) } } }
#
# Cookies will not be flagged as secure for excluded requests.
#
# 2. <b>Secure cookies</b>: Sets the +secure+ flag on cookies to tell browsers they
# must not be sent along with +http://+ requests. Enabled by default. Set
# +config.ssl_options+ with <tt>secure_cookies: false</tt> to disable this feature.
#
# 3. <b>HTTP Strict Transport Security (HSTS)</b>: Tells the browser to remember
# this site as TLS-only and automatically redirect non-TLS requests.
# Enabled by default. Configure +config.ssl_options+ with <tt>hsts: false</tt> to disable.
#
# Set +config.ssl_options+ with <tt>hsts: { ... }</tt> to configure HSTS:
#
# * +expires+: How long, in seconds, these settings will stick. The minimum
# required to qualify for browser preload lists is 1 year. Defaults to
# 2 years (recommended).
#
# * +subdomains+: Set to +true+ to tell the browser to apply these settings
# to all subdomains. This protects your cookies from interception by a
# vulnerable site on a subdomain. Defaults to +true+.
#
# * +preload+: Advertise that this site may be included in browsers'
# preloaded HSTS lists. HSTS protects your site on every visit <i>except the
# first visit</i> since it hasn't seen your HSTS header yet. To close this
# gap, browser vendors include a baked-in list of HSTS-enabled sites.
# Go to https://hstspreload.org to submit your site for inclusion.
# Defaults to +false+.
#
# To turn off HSTS, omitting the header is not enough. Browsers will remember the
# original HSTS directive until it expires. Instead, use the header to tell browsers to
# expire HSTS immediately. Setting <tt>hsts: false</tt> is a shortcut for
# <tt>hsts: { expires: 0 }</tt>.
class SSL
# :stopdoc:
# Default to 2 years as recommended on hstspreload.org.
HSTS_EXPIRES_IN = 63072000
def self.default_hsts_options
{ expires: HSTS_EXPIRES_IN, subdomains: true, preload: false }
end
def initialize(app, redirect: {}, hsts: {}, secure_cookies: true, ssl_default_redirect_status: nil)
@app = app
@redirect = redirect
@exclude = @redirect && @redirect[:exclude] || proc { !@redirect }
@secure_cookies = secure_cookies
@hsts_header = build_hsts_header(normalize_hsts_options(hsts))
@ssl_default_redirect_status = ssl_default_redirect_status
end
def call(env)
request = Request.new env
if request.ssl?
@app.call(env).tap do |status, headers, body|
set_hsts_header! headers
flag_cookies_as_secure! headers if @secure_cookies && !@exclude.call(request)
end
else
return redirect_to_https request unless @exclude.call(request)
@app.call(env)
end
end
private
def set_hsts_header!(headers)
headers["Strict-Transport-Security"] ||= @hsts_header
end
def normalize_hsts_options(options)
case options
# Explicitly disabling HSTS clears the existing setting from browsers
# by setting expiry to 0.
when false
self.class.default_hsts_options.merge(expires: 0)
# Default to enabled, with default options.
when nil, true
self.class.default_hsts_options
else
self.class.default_hsts_options.merge(options)
end
end
# https://tools.ietf.org/html/rfc6797#section-6.1
def build_hsts_header(hsts)
value = +"max-age=#{hsts[:expires].to_i}"
value << "; includeSubDomains" if hsts[:subdomains]
value << "; preload" if hsts[:preload]
value
end
def flag_cookies_as_secure!(headers)
if cookies = headers["Set-Cookie"]
cookies = cookies.split("\n")
headers["Set-Cookie"] = cookies.map { |cookie|
if !/;\s*secure\s*(;|$)/i.match?(cookie)
"#{cookie}; secure"
else
cookie
end
}.join("\n")
end
end
def redirect_to_https(request)
[ @redirect.fetch(:status, redirection_status(request)),
{ "Content-Type" => "text/html",
"Location" => https_location_for(request) },
(@redirect[:body] || []) ]
end
def redirection_status(request)
if request.get? || request.head?
301 # Issue a permanent redirect via a GET request.
elsif @ssl_default_redirect_status
@ssl_default_redirect_status
else
307 # Issue a fresh request redirect to preserve the HTTP method.
end
end
def https_location_for(request)
host = @redirect[:host] || request.host
port = @redirect[:port] || request.port
location = +"https://#{host}"
location << ":#{port}" if port != 80 && port != 443
location << request.fullpath
location
end
end
end
# frozen_string_literal: true
- 1
require "active_support/inflector/methods"
- 1
require "active_support/dependencies"
- 1
module ActionDispatch
- 1
class MiddlewareStack
- 1
class Middleware
- 1
attr_reader :args, :block, :klass
- 1
def initialize(klass, args, block)
- 316
@klass = klass
- 316
@args = args
- 316
@block = block
end
- 1
def name; klass.name; end
- 1
def ==(middleware)
case middleware
when Middleware
klass == middleware.klass
when Class
klass == middleware
end
end
- 1
def inspect
if klass.is_a?(Class)
klass.to_s
else
klass.class.to_s
end
end
- 1
def build(app)
- 308
klass.new(app, *args, &block)
end
- 1
def build_instrumented(app)
InstrumentationProxy.new(build(app), inspect)
end
end
# This class is used to instrument the execution of a single middleware.
# It proxies the `call` method transparently and instruments the method
# call.
- 1
class InstrumentationProxy
- 1
EVENT_NAME = "process_middleware.action_dispatch"
- 1
def initialize(middleware, class_name)
@middleware = middleware
@payload = {
middleware: class_name,
}
end
- 1
def call(env)
ActiveSupport::Notifications.instrument(EVENT_NAME, @payload) do
@middleware.call(env)
end
end
end
- 1
include Enumerable
- 1
attr_accessor :middlewares
- 1
def initialize(*args)
- 40
@middlewares = []
- 40
yield(self) if block_given?
end
- 1
def each
- 1
@middlewares.each { |x| yield x }
end
- 1
def size
middlewares.size
end
- 1
def last
middlewares.last
end
- 1
def [](i)
middlewares[i]
end
- 1
def unshift(klass, *args, &block)
middlewares.unshift(build_middleware(klass, args, block))
end
- 1
ruby2_keywords(:unshift) if respond_to?(:ruby2_keywords, true)
- 1
def initialize_copy(other)
- 301
self.middlewares = other.middlewares.dup
end
- 1
def insert(index, klass, *args, &block)
- 2
index = assert_index(index, :before)
- 2
middlewares.insert(index, build_middleware(klass, args, block))
end
- 1
ruby2_keywords(:insert) if respond_to?(:ruby2_keywords, true)
- 1
alias_method :insert_before, :insert
- 1
def insert_after(index, *args, &block)
index = assert_index(index, :after)
insert(index + 1, *args, &block)
end
- 1
ruby2_keywords(:insert_after) if respond_to?(:ruby2_keywords, true)
- 1
def swap(target, *args, &block)
index = assert_index(target, :before)
insert(index, *args, &block)
middlewares.delete_at(index + 1)
end
- 1
ruby2_keywords(:swap) if respond_to?(:ruby2_keywords, true)
- 1
def delete(target)
- 12
middlewares.delete_if { |m| m.klass == target }
end
- 1
def move(target, source)
source_index = assert_index(source, :before)
source_middleware = middlewares.delete_at(source_index)
target_index = assert_index(target, :before)
middlewares.insert(target_index, source_middleware)
end
- 1
alias_method :move_before, :move
- 1
def move_after(target, source)
source_index = assert_index(source, :after)
source_middleware = middlewares.delete_at(source_index)
target_index = assert_index(target, :after)
middlewares.insert(target_index + 1, source_middleware)
end
- 1
def use(klass, *args, &block)
- 314
middlewares.push(build_middleware(klass, args, block))
end
- 1
ruby2_keywords(:use) if respond_to?(:ruby2_keywords, true)
- 1
def build(app = nil, &block)
- 39
instrumenting = ActiveSupport::Notifications.notifier.listening?(InstrumentationProxy::EVENT_NAME)
- 39
middlewares.freeze.reverse.inject(app || block) do |a, e|
- 308
if instrumenting
e.build_instrumented(a)
else
- 308
e.build(a)
end
end
end
- 1
private
- 1
def assert_index(index, where)
- 5
i = index.is_a?(Integer) ? index : middlewares.index { |m| m.klass == index }
- 2
raise "No such middleware to insert #{where}: #{index.inspect}" unless i
- 2
i
end
- 1
def build_middleware(klass, args, block)
- 309
Middleware.new(klass, args, block)
end
end
end
# frozen_string_literal: true
require "rack/utils"
require "active_support/core_ext/uri"
module ActionDispatch
# This middleware serves static files from disk, if available.
# If no file is found, it hands off to the main app.
#
# In Rails apps, this middleware is configured to serve assets from
# the +public/+ directory.
#
# Only GET and HEAD requests are served. POST and other HTTP methods
# are handed off to the main app.
#
# Only files in the root directory are served; path traversal is denied.
class Static
def initialize(app, path, index: "index", headers: {})
@app = app
@file_handler = FileHandler.new(path, index: index, headers: headers)
end
def call(env)
@file_handler.attempt(env) || @app.call(env)
end
end
# This endpoint serves static files from disk using Rack::File.
#
# URL paths are matched with static files according to expected
# conventions: +path+, +path+.html, +path+/index.html.
#
# Precompressed versions of these files are checked first. Brotli (.br)
# and gzip (.gz) files are supported. If +path+.br exists, this
# endpoint returns that file with a +Content-Encoding: br+ header.
#
# If no matching file is found, this endpoint responds 404 Not Found.
#
# Pass the +root+ directory to search for matching files, an optional
# +index: "index"+ to change the default +path+/index.html, and optional
# additional response headers.
class FileHandler
# Accept-Encoding value -> file extension
PRECOMPRESSED = {
"br" => ".br",
"gzip" => ".gz",
"identity" => nil
}
def initialize(root, index: "index", headers: {}, precompressed: %i[ br gzip ], compressible_content_types: /\A(?:text\/|application\/javascript)/)
@root = root.chomp("/").b
@index = index
@precompressed = Array(precompressed).map(&:to_s) | %w[ identity ]
@compressible_content_types = compressible_content_types
@file_server = ::Rack::File.new(@root, headers)
end
def call(env)
attempt(env) || @file_server.call(env)
end
def attempt(env)
request = Rack::Request.new env
if request.get? || request.head?
if found = find_file(request.path_info, accept_encoding: request.accept_encoding)
serve request, *found
end
end
end
private
def serve(request, filepath, content_headers)
original, request.path_info =
request.path_info, ::Rack::Utils.escape_path(filepath).b
@file_server.call(request.env).tap do |status, headers, body|
# Omit Content-Encoding/Type/etc headers for 304 Not Modified
if status != 304
headers.update(content_headers)
end
end
ensure
request.path_info = original
end
# Match a URI path to a static file to be served.
#
# Used by the +Static+ class to negotiate a servable file in the
# +public/+ directory (see Static#call).
#
# Checks for +path+, +path+.html, and +path+/index.html files,
# in that order, including .br and .gzip compressed extensions.
#
# If a matching file is found, the path and necessary response headers
# (Content-Type, Content-Encoding) are returned.
def find_file(path_info, accept_encoding:)
each_candidate_filepath(path_info) do |filepath, content_type|
if response = try_files(filepath, content_type, accept_encoding: accept_encoding)
return response
end
end
end
def try_files(filepath, content_type, accept_encoding:)
headers = { "Content-Type" => content_type }
if compressible? content_type
try_precompressed_files filepath, headers, accept_encoding: accept_encoding
elsif file_readable? filepath
[ filepath, headers ]
end
end
def try_precompressed_files(filepath, headers, accept_encoding:)
each_precompressed_filepath(filepath) do |content_encoding, precompressed_filepath|
if file_readable? precompressed_filepath
# Identity encoding is default, so we skip Accept-Encoding
# negotiation and needn't set Content-Encoding.
#
# Vary header is expected when we've found other available
# encodings that Accept-Encoding ruled out.
if content_encoding == "identity"
return precompressed_filepath, headers
else
headers["Vary"] = "Accept-Encoding"
if accept_encoding.any? { |enc, _| /\b#{content_encoding}\b/i.match?(enc) }
headers["Content-Encoding"] = content_encoding
return precompressed_filepath, headers
end
end
end
end
end
def file_readable?(path)
file_stat = File.stat(File.join(@root, path.b))
rescue SystemCallError
false
else
file_stat.file? && file_stat.readable?
end
def compressible?(content_type)
@compressible_content_types.match?(content_type)
end
def each_precompressed_filepath(filepath)
@precompressed.each do |content_encoding|
precompressed_ext = PRECOMPRESSED.fetch(content_encoding)
yield content_encoding, "#{filepath}#{precompressed_ext}"
end
nil
end
def each_candidate_filepath(path_info)
return unless path = clean_path(path_info)
ext = ::File.extname(path)
content_type = ::Rack::Mime.mime_type(ext, nil)
yield path, content_type || "text/plain"
# Tack on .html and /index.html only for paths that don't have
# an explicit, resolvable file extension. No need to check
# for foo.js.html and foo.js/index.html.
unless content_type
default_ext = ::ActionController::Base.default_static_extension
if ext != default_ext
default_content_type = ::Rack::Mime.mime_type(default_ext, "text/plain")
yield "#{path}#{default_ext}", default_content_type
yield "#{path}/#{@index}#{default_ext}", default_content_type
end
end
nil
end
def clean_path(path_info)
path = ::Rack::Utils.unescape_path path_info.chomp("/")
if ::Rack::Utils.valid_path? path
::Rack::Utils.clean_path_info path
end
end
end
end
# frozen_string_literal: true
require "action_dispatch"
require "active_support/messages/rotation_configuration"
module ActionDispatch
class Railtie < Rails::Railtie # :nodoc:
config.action_dispatch = ActiveSupport::OrderedOptions.new
config.action_dispatch.x_sendfile_header = nil
config.action_dispatch.ip_spoofing_check = true
config.action_dispatch.show_exceptions = true
config.action_dispatch.tld_length = 1
config.action_dispatch.ignore_accept_header = false
config.action_dispatch.rescue_templates = {}
config.action_dispatch.rescue_responses = {}
config.action_dispatch.default_charset = nil
config.action_dispatch.rack_cache = false
config.action_dispatch.http_auth_salt = "http authentication"
config.action_dispatch.signed_cookie_salt = "signed cookie"
config.action_dispatch.encrypted_cookie_salt = "encrypted cookie"
config.action_dispatch.encrypted_signed_cookie_salt = "signed encrypted cookie"
config.action_dispatch.authenticated_encrypted_cookie_salt = "authenticated encrypted cookie"
config.action_dispatch.use_authenticated_cookie_encryption = false
config.action_dispatch.use_cookies_with_metadata = false
config.action_dispatch.perform_deep_munge = true
config.action_dispatch.return_only_media_type_on_content_type = true
config.action_dispatch.default_headers = {
"X-Frame-Options" => "SAMEORIGIN",
"X-XSS-Protection" => "1; mode=block",
"X-Content-Type-Options" => "nosniff",
"X-Download-Options" => "noopen",
"X-Permitted-Cross-Domain-Policies" => "none",
"Referrer-Policy" => "strict-origin-when-cross-origin"
}
config.action_dispatch.cookies_rotations = ActiveSupport::Messages::RotationConfiguration.new
config.eager_load_namespaces << ActionDispatch
initializer "action_dispatch.configure" do |app|
ActionDispatch::Http::URL.secure_protocol = app.config.force_ssl
ActionDispatch::Http::URL.tld_length = app.config.action_dispatch.tld_length
ActionDispatch::Request.ignore_accept_header = app.config.action_dispatch.ignore_accept_header
ActionDispatch::Request::Utils.perform_deep_munge = app.config.action_dispatch.perform_deep_munge
ActiveSupport.on_load(:action_dispatch_response) do
self.default_charset = app.config.action_dispatch.default_charset || app.config.encoding
self.default_headers = app.config.action_dispatch.default_headers
self.return_only_media_type_on_content_type = app.config.action_dispatch.return_only_media_type_on_content_type
end
ActionDispatch::ExceptionWrapper.rescue_responses.merge!(config.action_dispatch.rescue_responses)
ActionDispatch::ExceptionWrapper.rescue_templates.merge!(config.action_dispatch.rescue_templates)
config.action_dispatch.always_write_cookie = Rails.env.development? if config.action_dispatch.always_write_cookie.nil?
ActionDispatch::Cookies::CookieJar.always_write_cookie = config.action_dispatch.always_write_cookie
ActionDispatch.test_app = app
end
end
end
# frozen_string_literal: true
- 1
require "rack/session/abstract/id"
- 1
module ActionDispatch
- 1
class Request
# Session is responsible for lazily loading the session from store.
- 1
class Session # :nodoc:
- 1
ENV_SESSION_KEY = Rack::RACK_SESSION # :nodoc:
- 1
ENV_SESSION_OPTIONS_KEY = Rack::RACK_SESSION_OPTIONS # :nodoc:
# Singleton object used to determine if an optional param wasn't specified.
- 1
Unspecified = Object.new
# Creates a session hash, merging the properties of the previous session if any.
- 1
def self.create(store, req, default_options)
session_was = find req
session = Request::Session.new(store, req)
session.merge! session_was if session_was
set(req, session)
Options.set(req, Request::Session::Options.new(store, default_options))
session
end
- 1
def self.find(req)
req.get_header ENV_SESSION_KEY
end
- 1
def self.set(req, session)
req.set_header ENV_SESSION_KEY, session
end
- 1
class Options #:nodoc:
- 1
def self.set(req, options)
req.set_header ENV_SESSION_OPTIONS_KEY, options
end
- 1
def self.find(req)
req.get_header ENV_SESSION_OPTIONS_KEY
end
- 1
def initialize(by, default_options)
@by = by
@delegate = default_options.dup
end
- 1
def [](key)
@delegate[key]
end
- 1
def id(req)
@delegate.fetch(:id) {
@by.send(:extract_session_id, req)
}
end
- 1
def []=(k, v); @delegate[k] = v; end
- 1
def to_hash; @delegate.dup; end
- 1
def values_at(*args); @delegate.values_at(*args); end
end
- 1
def initialize(by, req)
@by = by
@req = req
@delegate = {}
@loaded = false
@exists = nil # We haven't checked yet.
end
- 1
def id
options.id(@req)
end
- 1
def options
Options.find @req
end
- 1
def destroy
clear
options = self.options || {}
@by.send(:delete_session, @req, options.id(@req), options)
# Load the new sid to be written with the response.
@loaded = false
load_for_write!
end
# Returns value of the key stored in the session or
# +nil+ if the given key is not found in the session.
- 1
def [](key)
load_for_read!
key = key.to_s
if key == "session_id"
id&.public_id
else
@delegate[key]
end
end
# Returns the nested value specified by the sequence of keys, returning
# +nil+ if any intermediate step is +nil+.
- 1
def dig(*keys)
load_for_read!
keys = keys.map.with_index { |key, i| i.zero? ? key.to_s : key }
@delegate.dig(*keys)
end
# Returns true if the session has the given key or false.
- 1
def has_key?(key)
load_for_read!
@delegate.key?(key.to_s)
end
- 1
alias :key? :has_key?
- 1
alias :include? :has_key?
# Returns keys of the session as Array.
- 1
def keys
load_for_read!
@delegate.keys
end
# Returns values of the session as Array.
- 1
def values
load_for_read!
@delegate.values
end
# Writes given value to given key of the session.
- 1
def []=(key, value)
load_for_write!
@delegate[key.to_s] = value
end
# Clears the session.
- 1
def clear
load_for_write!
@delegate.clear
end
# Returns the session as Hash.
- 1
def to_hash
load_for_read!
@delegate.dup.delete_if { |_, v| v.nil? }
end
- 1
alias :to_h :to_hash
# Updates the session with given Hash.
#
# session.to_hash
# # => {"session_id"=>"e29b9ea315edf98aad94cc78c34cc9b2"}
#
# session.update({ "foo" => "bar" })
# # => {"session_id"=>"e29b9ea315edf98aad94cc78c34cc9b2", "foo" => "bar"}
#
# session.to_hash
# # => {"session_id"=>"e29b9ea315edf98aad94cc78c34cc9b2", "foo" => "bar"}
- 1
def update(hash)
load_for_write!
@delegate.update hash.stringify_keys
end
# Deletes given key from the session.
- 1
def delete(key)
load_for_write!
@delegate.delete key.to_s
end
# Returns value of the given key from the session, or raises +KeyError+
# if can't find the given key and no default value is set.
# Returns default value if specified.
#
# session.fetch(:foo)
# # => KeyError: key not found: "foo"
#
# session.fetch(:foo, :bar)
# # => :bar
#
# session.fetch(:foo) do
# :bar
# end
# # => :bar
- 1
def fetch(key, default = Unspecified, &block)
load_for_read!
if default == Unspecified
@delegate.fetch(key.to_s, &block)
else
@delegate.fetch(key.to_s, default, &block)
end
end
- 1
def inspect
if loaded?
super
else
"#<#{self.class}:0x#{(object_id << 1).to_s(16)} not yet loaded>"
end
end
- 1
def exists?
return @exists unless @exists.nil?
@exists = @by.send(:session_exists?, @req)
end
- 1
def loaded?
@loaded
end
- 1
def empty?
load_for_read!
@delegate.empty?
end
- 1
def merge!(other)
load_for_write!
@delegate.merge!(other)
end
- 1
def each(&block)
to_hash.each(&block)
end
- 1
private
- 1
def load_for_read!
load! if !loaded? && exists?
end
- 1
def load_for_write!
load! unless loaded?
end
- 1
def load!
id, session = @by.load_session @req
options[:id] = id
@delegate.replace(session.stringify_keys)
@loaded = true
end
end
end
end
# frozen_string_literal: true
require "active_support/core_ext/hash/indifferent_access"
module ActionDispatch
class Request
class Utils # :nodoc:
mattr_accessor :perform_deep_munge, default: true
def self.each_param_value(params, &block)
case params
when Array
params.each { |element| each_param_value(element, &block) }
when Hash
params.each_value { |value| each_param_value(value, &block) }
when String
block.call params
end
end
def self.normalize_encode_params(params)
if perform_deep_munge
NoNilParamEncoder.normalize_encode_params params
else
ParamEncoder.normalize_encode_params params
end
end
def self.check_param_encoding(params)
case params
when Array
params.each { |element| check_param_encoding(element) }
when Hash
params.each_value { |value| check_param_encoding(value) }
when String
unless params.valid_encoding?
# Raise Rack::Utils::InvalidParameterError for consistency with Rack.
# ActionDispatch::Request#GET will re-raise as a BadRequest error.
raise Rack::Utils::InvalidParameterError, "Invalid encoding for parameter: #{params.scrub}"
end
end
end
class ParamEncoder # :nodoc:
# Convert nested Hash to HashWithIndifferentAccess.
def self.normalize_encode_params(params)
case params
when Array
handle_array params
when Hash
if params.has_key?(:tempfile)
ActionDispatch::Http::UploadedFile.new(params)
else
params.transform_values do |val|
normalize_encode_params(val)
end.with_indifferent_access
end
else
params
end
end
def self.handle_array(params)
params.map! { |el| normalize_encode_params(el) }
end
end
# Remove nils from the params hash.
class NoNilParamEncoder < ParamEncoder # :nodoc:
def self.handle_array(params)
list = super
list.compact!
list
end
end
end
end
end
# frozen_string_literal: true
- 1
require "active_support/core_ext/string/filters"
- 1
module ActionDispatch
# The routing module provides URL rewriting in native Ruby. It's a way to
# redirect incoming requests to controllers and actions. This replaces
# mod_rewrite rules. Best of all, Rails' \Routing works with any web server.
# Routes are defined in <tt>config/routes.rb</tt>.
#
# Think of creating routes as drawing a map for your requests. The map tells
# them where to go based on some predefined pattern:
#
# Rails.application.routes.draw do
# Pattern 1 tells some request to go to one place
# Pattern 2 tell them to go to another
# ...
# end
#
# The following symbols are special:
#
# :controller maps to your controller name
# :action maps to an action with your controllers
#
# Other names simply map to a parameter as in the case of <tt>:id</tt>.
#
# == Resources
#
# Resource routing allows you to quickly declare all of the common routes
# for a given resourceful controller. Instead of declaring separate routes
# for your +index+, +show+, +new+, +edit+, +create+, +update+ and +destroy+
# actions, a resourceful route declares them in a single line of code:
#
# resources :photos
#
# Sometimes, you have a resource that clients always look up without
# referencing an ID. A common example, /profile always shows the profile of
# the currently logged in user. In this case, you can use a singular resource
# to map /profile (rather than /profile/:id) to the show action.
#
# resource :profile
#
# It's common to have resources that are logically children of other
# resources:
#
# resources :magazines do
# resources :ads
# end
#
# You may wish to organize groups of controllers under a namespace. Most
# commonly, you might group a number of administrative controllers under
# an +admin+ namespace. You would place these controllers under the
# <tt>app/controllers/admin</tt> directory, and you can group them together
# in your router:
#
# namespace "admin" do
# resources :posts, :comments
# end
#
# Alternatively, you can add prefixes to your path without using a separate
# directory by using +scope+. +scope+ takes additional options which
# apply to all enclosed routes.
#
# scope path: "/cpanel", as: 'admin' do
# resources :posts, :comments
# end
#
# For more, see <tt>Routing::Mapper::Resources#resources</tt>,
# <tt>Routing::Mapper::Scoping#namespace</tt>, and
# <tt>Routing::Mapper::Scoping#scope</tt>.
#
# == Non-resourceful routes
#
# For routes that don't fit the <tt>resources</tt> mold, you can use the HTTP helper
# methods <tt>get</tt>, <tt>post</tt>, <tt>patch</tt>, <tt>put</tt> and <tt>delete</tt>.
#
# get 'post/:id', to: 'posts#show'
# post 'post/:id', to: 'posts#create_comment'
#
# Now, if you POST to <tt>/posts/:id</tt>, it will route to the <tt>create_comment</tt> action. A GET on the same
# URL will route to the <tt>show</tt> action.
#
# If your route needs to respond to more than one HTTP method (or all methods) then using the
# <tt>:via</tt> option on <tt>match</tt> is preferable.
#
# match 'post/:id', to: 'posts#show', via: [:get, :post]
#
# == Named routes
#
# Routes can be named by passing an <tt>:as</tt> option,
# allowing for easy reference within your source as +name_of_route_url+
# for the full URL and +name_of_route_path+ for the URI path.
#
# Example:
#
# # In config/routes.rb
# get '/login', to: 'accounts#login', as: 'login'
#
# # With render, redirect_to, tests, etc.
# redirect_to login_url
#
# Arguments can be passed as well.
#
# redirect_to show_item_path(id: 25)
#
# Use <tt>root</tt> as a shorthand to name a route for the root path "/".
#
# # In config/routes.rb
# root to: 'blogs#index'
#
# # would recognize http://www.example.com/ as
# params = { controller: 'blogs', action: 'index' }
#
# # and provide these named routes
# root_url # => 'http://www.example.com/'
# root_path # => '/'
#
# Note: when using +controller+, the route is simply named after the
# method you call on the block parameter rather than map.
#
# # In config/routes.rb
# controller :blog do
# get 'blog/show', to: :list
# get 'blog/delete', to: :delete
# get 'blog/edit', to: :edit
# end
#
# # provides named routes for show, delete, and edit
# link_to @article.title, blog_show_path(id: @article.id)
#
# == Pretty URLs
#
# Routes can generate pretty URLs. For example:
#
# get '/articles/:year/:month/:day', to: 'articles#find_by_id', constraints: {
# year: /\d{4}/,
# month: /\d{1,2}/,
# day: /\d{1,2}/
# }
#
# Using the route above, the URL "http://localhost:3000/articles/2005/11/06"
# maps to
#
# params = {year: '2005', month: '11', day: '06'}
#
# == Regular Expressions and parameters
# You can specify a regular expression to define a format for a parameter.
#
# controller 'geocode' do
# get 'geocode/:postalcode', to: :show, constraints: {
# postalcode: /\d{5}(-\d{4})?/
# }
# end
#
# Constraints can include the 'ignorecase' and 'extended syntax' regular
# expression modifiers:
#
# controller 'geocode' do
# get 'geocode/:postalcode', to: :show, constraints: {
# postalcode: /hx\d\d\s\d[a-z]{2}/i
# }
# end
#
# controller 'geocode' do
# get 'geocode/:postalcode', to: :show, constraints: {
# postalcode: /# Postalcode format
# \d{5} #Prefix
# (-\d{4})? #Suffix
# /x
# }
# end
#
# Using the multiline modifier will raise an +ArgumentError+.
# Encoding regular expression modifiers are silently ignored. The
# match will always use the default encoding or ASCII.
#
# == External redirects
#
# You can redirect any path to another path using the redirect helper in your router:
#
# get "/stories", to: redirect("/posts")
#
# == Unicode character routes
#
# You can specify unicode character routes in your router:
#
# get "こんにちは", to: "welcome#index"
#
# == Routing to Rack Applications
#
# Instead of a String, like <tt>posts#index</tt>, which corresponds to the
# index action in the PostsController, you can specify any Rack application
# as the endpoint for a matcher:
#
# get "/application.js", to: Sprockets
#
# == Reloading routes
#
# You can reload routes if you feel you must:
#
# Rails.application.reload_routes!
#
# This will clear all named routes and reload config/routes.rb if the file has been modified from
# last load. To absolutely force reloading, use <tt>reload!</tt>.
#
# == Testing Routes
#
# The two main methods for testing your routes:
#
# === +assert_routing+
#
# def test_movie_route_properly_splits
# opts = {controller: "plugin", action: "checkout", id: "2"}
# assert_routing "plugin/checkout/2", opts
# end
#
# +assert_routing+ lets you test whether or not the route properly resolves into options.
#
# === +assert_recognizes+
#
# def test_route_has_options
# opts = {controller: "plugin", action: "show", id: "12"}
# assert_recognizes opts, "/plugins/show/12"
# end
#
# Note the subtle difference between the two: +assert_routing+ tests that
# a URL fits options while +assert_recognizes+ tests that a URL
# breaks into parameters properly.
#
# In tests you can simply pass the URL or named route to +get+ or +post+.
#
# def send_to_jail
# get '/jail'
# assert_response :success
# end
#
# def goes_to_login
# get login_url
# #...
# end
#
# == View a list of all your routes
#
# rails routes
#
# Target a specific controller with <tt>-c</tt>, or grep routes
# using <tt>-g</tt>. Useful in conjunction with <tt>--expanded</tt>
# which displays routes vertically.
- 1
module Routing
- 1
extend ActiveSupport::Autoload
- 1
autoload :Mapper
- 1
autoload :RouteSet
- 1
autoload :RoutesProxy
- 1
autoload :UrlFor
- 1
autoload :PolymorphicRoutes
- 1
SEPARATORS = %w( / . ? ) #:nodoc:
- 1
HTTP_METHODS = [:get, :head, :post, :patch, :put, :delete, :options] #:nodoc:
end
end
# frozen_string_literal: true
- 1
module ActionDispatch
- 1
module Routing
- 1
class Endpoint # :nodoc:
- 1
def dispatcher?; false; end
- 1
def redirect?; false; end
- 1
def matches?(req); true; end
- 1
def app; self; end
- 1
def rack_app; app; end
- 1
def engine?
rack_app.is_a?(Class) && rack_app < Rails::Engine
end
end
end
end
# frozen_string_literal: true
- 1
require "delegate"
- 1
require "io/console/size"
- 1
module ActionDispatch
- 1
module Routing
- 1
class RouteWrapper < SimpleDelegator
- 1
def endpoint
app.dispatcher? ? "#{controller}##{action}" : rack_app.inspect
end
- 1
def constraints
requirements.except(:controller, :action)
end
- 1
def rack_app
app.rack_app
end
- 1
def path
super.spec.to_s
end
- 1
def name
super.to_s
end
- 1
def reqs
@reqs ||= begin
reqs = endpoint
reqs += " #{constraints}" unless constraints.empty?
reqs
end
end
- 1
def controller
parts.include?(:controller) ? ":controller" : requirements[:controller]
end
- 1
def action
parts.include?(:action) ? ":action" : requirements[:action]
end
- 1
def internal?
internal
end
- 1
def engine?
app.engine?
end
end
##
# This class is just used for displaying route information when someone
# executes `bin/rails routes` or looks at the RoutingError page.
# People should not use this class.
- 1
class RoutesInspector # :nodoc:
- 1
def initialize(routes)
@engines = {}
@routes = routes
end
- 1
def format(formatter, filter = {})
routes_to_display = filter_routes(normalize_filter(filter))
routes = collect_routes(routes_to_display)
if routes.none?
formatter.no_routes(collect_routes(@routes), filter)
return formatter.result
end
formatter.header routes
formatter.section routes
@engines.each do |name, engine_routes|
formatter.section_title "Routes for #{name}"
formatter.section engine_routes
end
formatter.result
end
- 1
private
- 1
def normalize_filter(filter)
if filter[:controller]
{ controller: /#{filter[:controller].underscore.sub(/_?controller\z/, "")}/ }
elsif filter[:grep]
{ controller: /#{filter[:grep]}/, action: /#{filter[:grep]}/,
verb: /#{filter[:grep]}/, name: /#{filter[:grep]}/, path: /#{filter[:grep]}/ }
end
end
- 1
def filter_routes(filter)
if filter
@routes.select do |route|
route_wrapper = RouteWrapper.new(route)
filter.any? { |default, value| value.match?(route_wrapper.send(default)) }
end
else
@routes
end
end
- 1
def collect_routes(routes)
routes.collect do |route|
RouteWrapper.new(route)
end.reject(&:internal?).collect do |route|
collect_engine_routes(route)
{ name: route.name,
verb: route.verb,
path: route.path,
reqs: route.reqs }
end
end
- 1
def collect_engine_routes(route)
name = route.endpoint
return unless route.engine?
return if @engines[name]
routes = route.rack_app.routes
if routes.is_a?(ActionDispatch::Routing::RouteSet)
@engines[name] = collect_routes(routes.routes)
end
end
end
- 1
module ConsoleFormatter
- 1
class Base
- 1
def initialize
@buffer = []
end
- 1
def result
@buffer.join("\n")
end
- 1
def section_title(title)
end
- 1
def section(routes)
end
- 1
def header(routes)
end
- 1
def no_routes(routes, filter)
@buffer <<
if routes.none?
<<~MESSAGE
You don't have any routes defined!
Please add some routes in config/routes.rb.
MESSAGE
elsif filter.key?(:controller)
"No routes were found for this controller."
elsif filter.key?(:grep)
"No routes were found for this grep pattern."
end
@buffer << "For more information about routes, see the Rails guide: https://guides.rubyonrails.org/routing.html."
end
end
- 1
class Sheet < Base
- 1
def section_title(title)
@buffer << "\n#{title}:"
end
- 1
def section(routes)
@buffer << draw_section(routes)
end
- 1
def header(routes)
@buffer << draw_header(routes)
end
- 1
private
- 1
def draw_section(routes)
header_lengths = ["Prefix", "Verb", "URI Pattern"].map(&:length)
name_width, verb_width, path_width = widths(routes).zip(header_lengths).map(&:max)
routes.map do |r|
"#{r[:name].rjust(name_width)} #{r[:verb].ljust(verb_width)} #{r[:path].ljust(path_width)} #{r[:reqs]}"
end
end
- 1
def draw_header(routes)
name_width, verb_width, path_width = widths(routes)
"#{"Prefix".rjust(name_width)} #{"Verb".ljust(verb_width)} #{"URI Pattern".ljust(path_width)} Controller#Action"
end
- 1
def widths(routes)
[routes.map { |r| r[:name].length }.max || 0,
routes.map { |r| r[:verb].length }.max || 0,
routes.map { |r| r[:path].length }.max || 0]
end
end
- 1
class Expanded < Base
- 1
def initialize(width: IO.console_size[1])
@width = width
super()
end
- 1
def section_title(title)
@buffer << "\n#{"[ #{title} ]"}"
end
- 1
def section(routes)
@buffer << draw_expanded_section(routes)
end
- 1
private
- 1
def draw_expanded_section(routes)
routes.map.each_with_index do |r, i|
<<~MESSAGE.chomp
#{route_header(index: i + 1)}
Prefix | #{r[:name]}
Verb | #{r[:verb]}
URI | #{r[:path]}
Controller#Action | #{r[:reqs]}
MESSAGE
end
end
- 1
def route_header(index:)
"--[ Route #{index} ]".ljust(@width, "-")
end
end
end
- 1
class HtmlTableFormatter
- 1
def initialize(view)
@view = view
@buffer = []
end
- 1
def section_title(title)
@buffer << %(<tr><th colspan="4">#{title}</th></tr>)
end
- 1
def section(routes)
@buffer << @view.render(partial: "routes/route", collection: routes)
end
# The header is part of the HTML page, so we don't construct it here.
- 1
def header(routes)
end
- 1
def no_routes(*)
@buffer << <<~MESSAGE
<p>You don't have any routes defined!</p>
<ul>
<li>Please add some routes in <tt>config/routes.rb</tt>.</li>
<li>
For more information about routes, please see the Rails guide
<a href="https://guides.rubyonrails.org/routing.html">Rails Routing from the Outside In</a>.
</li>
</ul>
MESSAGE
end
- 1
def result
@view.raw @view.render(layout: "routes/table") {
@view.raw @buffer.join("\n")
}
end
end
end
end
# frozen_string_literal: true
- 1
require "active_support/core_ext/hash/slice"
- 1
require "active_support/core_ext/enumerable"
- 1
require "active_support/core_ext/array/extract_options"
- 1
require "active_support/core_ext/regexp"
- 1
require "active_support/core_ext/symbol/starts_ends_with"
- 1
require "action_dispatch/routing/redirection"
- 1
require "action_dispatch/routing/endpoint"
- 1
module ActionDispatch
- 1
module Routing
- 1
class Mapper
- 1
URL_OPTIONS = [:protocol, :subdomain, :domain, :host, :port]
- 1
class Constraints < Routing::Endpoint #:nodoc:
- 1
attr_reader :app, :constraints
- 1
SERVE = ->(app, req) { app.serve req }
- 1
CALL = ->(app, req) { app.call req.env }
- 1
def initialize(app, constraints, strategy)
# Unwrap Constraints objects. I don't actually think it's possible
# to pass a Constraints object to this constructor, but there were
# multiple places that kept testing children of this object. I
# *think* they were just being defensive, but I have no idea.
- 91
if app.is_a?(self.class)
constraints += app.constraints
app = app.app
end
- 91
@strategy = strategy
- 91
@app, @constraints, = app, constraints
end
- 1
def dispatcher?; @strategy == SERVE; end
- 1
def matches?(req)
@constraints.all? do |constraint|
(constraint.respond_to?(:matches?) && constraint.matches?(req)) ||
(constraint.respond_to?(:call) && constraint.call(*constraint_args(constraint, req)))
end
end
- 1
def serve(req)
return [ 404, { "X-Cascade" => "pass" }, [] ] unless matches?(req)
@strategy.call @app, req
end
- 1
private
- 1
def constraint_args(constraint, request)
arity = if constraint.respond_to?(:arity)
constraint.arity
else
constraint.method(:call).arity
end
if arity < 1
[]
elsif arity == 1
[request]
else
[request.path_parameters, request]
end
end
end
- 1
class Mapping #:nodoc:
- 1
ANCHOR_CHARACTERS_REGEX = %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z}
- 1
OPTIONAL_FORMAT_REGEX = %r{(?:\(\.:format\)+|\.:format|/)\Z}
- 1
attr_reader :requirements, :defaults, :to, :default_controller,
:default_action, :required_defaults, :ast, :scope_options
- 1
def self.build(scope, set, ast, controller, default_action, to, via, formatted, options_constraints, anchor, options)
- 326
scope_params = {
blocks: scope[:blocks] || [],
constraints: scope[:constraints] || {},
- 326
defaults: (scope[:defaults] || {}).dup,
module: scope[:module],
options: scope[:options] || {}
}
- 326
new set: set, ast: ast, controller: controller, default_action: default_action,
to: to, formatted: formatted, via: via, options_constraints: options_constraints,
anchor: anchor, scope_params: scope_params, options: scope_params[:options].merge(options)
end
- 1
def self.check_via(via)
- 326
if via.empty?
msg = "You should not use the `match` method in your router without specifying an HTTP method.\n" \
"If you want to expose your action to both GET and POST, add `via: [:get, :post]` option.\n" \
"If you want to expose your action to GET, use `get` in the router:\n" \
" Instead of: match \"controller#action\"\n" \
" Do: get \"controller#action\""
raise ArgumentError, msg
end
- 326
via
end
- 1
def self.normalize_path(path, format)
- 326
path = Mapper.normalize_path(path)
- 326
if format == true
- 1
"#{path}.:format"
- 325
elsif optional_format?(path, format)
- 292
"#{path}(.:format)"
else
- 33
path
end
end
- 1
def self.optional_format?(path, format)
- 325
format != false && !path.match?(OPTIONAL_FORMAT_REGEX)
end
- 1
def initialize(set:, ast:, controller:, default_action:, to:, formatted:, via:, options_constraints:, anchor:, scope_params:, options:)
- 326
@defaults = scope_params[:defaults]
- 326
@set = set
- 326
@to = intern(to)
- 326
@default_controller = intern(controller)
- 326
@default_action = intern(default_action)
- 326
@ast = ast
- 326
@anchor = anchor
- 326
@via = via
- 326
@internal = options.delete(:internal)
- 326
@scope_options = scope_params[:options]
- 326
path_params = []
- 326
wildcard_options = {}
- 326
ast.each do |node|
- 3827
if node.symbol?
- 508
path_params << node.to_sym
- 3319
elsif formatted != false && node.star?
# Add a constraint for wildcard route to make it non-greedy and match the
# optional format part of the route by default.
- 3
wildcard_options[node.name.to_sym] ||= /.+?/
- 3316
elsif node.cat?
- 1594
alter_regex_for_custom_routes(node)
end
end
- 326
options = wildcard_options.merge!(options)
- 326
options = normalize_options!(options, path_params, scope_params[:module])
- 326
split_options = constraints(options, path_params)
- 326
constraints = scope_params[:constraints].merge Hash[split_options[:constraints] || []]
- 326
if options_constraints.is_a?(Hash)
- 321
@defaults = Hash[options_constraints.find_all { |key, default|
- 15
URL_OPTIONS.include?(key) && (String === default || Integer === default)
}].merge @defaults
- 321
@blocks = scope_params[:blocks]
- 321
constraints.merge! options_constraints
else
- 5
@blocks = blocks(options_constraints)
end
- 326
requirements, conditions = split_constraints path_params, constraints
- 326
verify_regexp_requirements requirements.map(&:last).grep(Regexp)
- 326
formats = normalize_format(formatted)
- 326
@requirements = formats[:requirements].merge Hash[requirements]
- 326
@conditions = Hash[conditions]
- 326
@defaults = formats[:defaults].merge(@defaults).merge(normalize_defaults(options))
- 326
if path_params.include?(:action) && !@requirements.key?(:action)
- 6
@defaults[:action] ||= "index"
end
- 326
@required_defaults = (split_options[:required_defaults] || []).map(&:first)
end
- 1
def make_route(name, precedence)
- 326
Journey::Route.new(name: name, app: application, path: path, constraints: conditions,
required_defaults: required_defaults, defaults: defaults,
request_method_match: request_method, precedence: precedence,
scope_options: scope_options, internal: @internal)
end
- 1
def application
- 326
app(@blocks)
end
- 1
JOINED_SEPARATORS = SEPARATORS.join # :nodoc:
- 1
def path
- 326
Journey::Path::Pattern.new(@ast, requirements, JOINED_SEPARATORS, @anchor)
end
- 1
def conditions
- 326
build_conditions @conditions, @set.request_class
end
- 1
def build_conditions(current_conditions, request_class)
- 326
conditions = current_conditions.dup
- 326
conditions.keep_if do |k, _|
- 10
request_class.public_method_defined?(k)
end
end
- 1
private :build_conditions
- 1
def request_method
- 652
@via.map { |x| Journey::Route.verb_matcher(x) }
end
- 1
private :request_method
- 1
private
# Find all the symbol nodes that are adjacent to literal nodes and alter
# the regexp so that Journey will partition them into custom routes.
- 1
def alter_regex_for_custom_routes(node)
- 1594
if node.left.literal? && node.right.symbol?
- 2
symbol = node.right
- 1592
elsif node.left.literal? && node.right.cat? && node.right.left.symbol?
- 2
symbol = node.right.left
- 1590
elsif node.left.symbol? && node.right.literal?
- 2
symbol = node.left
- 1588
elsif node.left.symbol? && node.right.cat? && node.right.left.literal?
- 2
symbol = node.left
end
- 1594
if symbol
- 8
symbol.regexp = /(?:#{Regexp.union(symbol.regexp, '-')})+/
end
end
- 1
def intern(object)
- 978
object.is_a?(String) ? -object : object
end
- 1
def normalize_options!(options, path_params, modyoule)
- 326
if path_params.include?(:controller)
- 5
raise ArgumentError, ":controller segment is not allowed within a namespace block" if modyoule
# Add a default constraint for :controller path segments that matches namespaced
# controllers with default routes like :controller/:action/:id(.:format), e.g:
# GET /admin/products/show/1
# => { controller: 'admin/products', action: 'show', id: '1' }
- 5
options[:controller] ||= /.+?/
end
- 326
if to.respond_to?(:action) || to.respond_to?(:call)
- 87
options
else
- 239
to_endpoint = split_to to
- 239
controller = to_endpoint[0] || default_controller
- 239
action = to_endpoint[1] || default_action
- 239
controller = add_controller_module(controller, modyoule)
- 239
options.merge! check_controller_and_action(path_params, controller, action)
end
end
- 1
def split_constraints(path_params, constraints)
- 326
constraints.partition do |key, requirement|
- 29
path_params.include?(key) || key == :controller
end
end
- 1
def normalize_format(formatted)
- 326
case formatted
when true
- 1
{ requirements: { format: /.+/ },
defaults: {} }
when Regexp
{ requirements: { format: formatted },
defaults: { format: nil } }
when String
- 1
{ requirements: { format: Regexp.compile(formatted) },
defaults: { format: formatted } }
else
- 324
{ requirements: {}, defaults: {} }
end
end
- 1
def verify_regexp_requirements(requirements)
- 326
requirements.each do |requirement|
- 17
if ANCHOR_CHARACTERS_REGEX.match?(requirement.source)
raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}"
end
- 17
if requirement.multiline?
raise ArgumentError, "Regexp multiline option is not allowed in routing requirements: #{requirement.inspect}"
end
end
end
- 1
def normalize_defaults(options)
- 816
Hash[options.reject { |_, default| Regexp === default }]
end
- 1
def app(blocks)
- 326
if to.respond_to?(:action)
Routing::RouteSet::StaticDispatcher.new to
- 326
elsif to.respond_to?(:call)
- 87
Constraints.new(to, blocks, Constraints::CALL)
- 239
elsif blocks.any?
- 4
Constraints.new(dispatcher(defaults.key?(:controller)), blocks, Constraints::SERVE)
else
- 235
dispatcher(defaults.key?(:controller))
end
end
- 1
def check_controller_and_action(path_params, controller, action)
- 239
hash = check_part(:controller, controller, path_params, {}) do |part|
- 234
translate_controller(part) {
message = +"'#{part}' is not a supported controller name. This can lead to potential routing problems."
message << " See https://guides.rubyonrails.org/routing.html#specifying-a-controller-to-use"
raise ArgumentError, message
}
end
- 239
check_part(:action, action, path_params, hash) { |part|
- 234
part.is_a?(Regexp) ? part : part.to_s
}
end
- 1
def check_part(name, part, path_params, hash)
- 478
if part
- 468
hash[name] = yield(part)
else
- 10
unless path_params.include?(name)
message = "Missing :#{name} key on routes definition, please check your routes."
raise ArgumentError, message
end
end
- 478
hash
end
- 1
def split_to(to)
- 239
if /#/.match?(to)
- 75
to.split("#").map!(&:-@)
else
- 164
[]
end
end
- 1
def add_controller_module(controller, modyoule)
- 239
if modyoule && !controller.is_a?(Regexp)
- 36
if controller&.start_with?("/")
-controller[1..-1]
else
- 36
-[modyoule, controller].compact.join("/")
end
else
- 203
controller
end
end
- 1
def translate_controller(controller)
- 234
return controller if Regexp === controller
- 234
return controller.to_s if /\A[a-z_0-9][a-z_0-9\/]*\z/.match?(controller)
yield
end
- 1
def blocks(callable_constraint)
- 5
unless callable_constraint.respond_to?(:call) || callable_constraint.respond_to?(:matches?)
raise ArgumentError, "Invalid constraint: #{callable_constraint.inspect} must respond to :call or :matches?"
end
- 5
[callable_constraint]
end
- 1
def constraints(options, path_params)
- 326
options.group_by do |key, option|
- 490
if Regexp === option
- 13
:constraints
else
- 477
if path_params.include?(key)
- 1
:path_params
else
- 476
:required_defaults
end
end
end
end
- 1
def dispatcher(raise_on_name_error)
- 239
Routing::RouteSet::Dispatcher.new raise_on_name_error
end
end
# Invokes Journey::Router::Utils.normalize_path, then ensures that
# /(:locale) becomes (/:locale). Except for root cases, where the
# former is the correct one.
- 1
def self.normalize_path(path)
- 574
path = Journey::Router::Utils.normalize_path(path)
# the path for a root URL at this point can be something like
# "/(/:locale)(/:platform)/(:browser)", and we would want
# "/(:locale)(/:platform)(/:browser)"
# reverse "/(", "/((" etc to "(/", "((/" etc
- 574
path.gsub!(%r{/(\(+)/?}, '\1/')
# if a path is all optional segments, change the leading "(/" back to
# "/(" so it evaluates to "/" when interpreted with no options.
# Unless, however, at least one secondary segment consists of a static
# part, ex. "(/:locale)(/pages/:page)"
- 574
path.sub!(%r{^(\(+)/}, '/\1') if %r{^(\(+[^)]+\))(\(+/:[^)]+\))*$}.match?(path)
- 574
path
end
- 1
def self.normalize_name(name)
- 158
normalize_path(name)[1..-1].tr("/", "_")
end
- 1
module Base
# Matches a URL pattern to one or more routes.
#
# You should not use the +match+ method in your router
# without specifying an HTTP method.
#
# If you want to expose your action to both GET and POST, use:
#
# # sets :controller, :action and :id in params
# match ':controller/:action/:id', via: [:get, :post]
#
# Note that +:controller+, +:action+ and +:id+ are interpreted as URL
# query parameters and thus available through +params+ in an action.
#
# If you want to expose your action to GET, use +get+ in the router:
#
# Instead of:
#
# match ":controller/:action/:id"
#
# Do:
#
# get ":controller/:action/:id"
#
# Two of these symbols are special, +:controller+ maps to the controller
# and +:action+ to the controller's action. A pattern can also map
# wildcard segments (globs) to params:
#
# get 'songs/*category/:title', to: 'songs#show'
#
# # 'songs/rock/classic/stairway-to-heaven' sets
# # params[:category] = 'rock/classic'
# # params[:title] = 'stairway-to-heaven'
#
# To match a wildcard parameter, it must have a name assigned to it.
# Without a variable name to attach the glob parameter to, the route
# can't be parsed.
#
# When a pattern points to an internal route, the route's +:action+ and
# +:controller+ should be set in options or hash shorthand. Examples:
#
# match 'photos/:id' => 'photos#show', via: :get
# match 'photos/:id', to: 'photos#show', via: :get
# match 'photos/:id', controller: 'photos', action: 'show', via: :get
#
# A pattern can also point to a +Rack+ endpoint i.e. anything that
# responds to +call+:
#
# match 'photos/:id', to: -> (hash) { [200, {}, ["Coming soon"]] }, via: :get
# match 'photos/:id', to: PhotoRackApp, via: :get
# # Yes, controller actions are just rack endpoints
# match 'photos/:id', to: PhotosController.action(:show), via: :get
#
# Because requesting various HTTP verbs with a single action has security
# implications, you must either specify the actions in
# the via options or use one of the HttpHelpers[rdoc-ref:HttpHelpers]
# instead +match+
#
# === Options
#
# Any options not seen here are passed on as params with the URL.
#
# [:controller]
# The route's controller.
#
# [:action]
# The route's action.
#
# [:param]
# Overrides the default resource identifier +:id+ (name of the
# dynamic segment used to generate the routes).
# You can access that segment from your controller using
# <tt>params[<:param>]</tt>.
# In your router:
#
# resources :users, param: :name
#
# The +users+ resource here will have the following routes generated for it:
#
# GET /users(.:format)
# POST /users(.:format)
# GET /users/new(.:format)
# GET /users/:name/edit(.:format)
# GET /users/:name(.:format)
# PATCH/PUT /users/:name(.:format)
# DELETE /users/:name(.:format)
#
# You can override <tt>ActiveRecord::Base#to_param</tt> of a related
# model to construct a URL:
#
# class User < ActiveRecord::Base
# def to_param
# name
# end
# end
#
# user = User.find_by(name: 'Phusion')
# user_path(user) # => "/users/Phusion"
#
# [:path]
# The path prefix for the routes.
#
# [:module]
# The namespace for :controller.
#
# match 'path', to: 'c#a', module: 'sekret', controller: 'posts', via: :get
# # => Sekret::PostsController
#
# See <tt>Scoping#namespace</tt> for its scope equivalent.
#
# [:as]
# The name used to generate routing helpers.
#
# [:via]
# Allowed HTTP verb(s) for route.
#
# match 'path', to: 'c#a', via: :get
# match 'path', to: 'c#a', via: [:get, :post]
# match 'path', to: 'c#a', via: :all
#
# [:to]
# Points to a +Rack+ endpoint. Can be an object that responds to
# +call+ or a string representing a controller's action.
#
# match 'path', to: 'controller#action', via: :get
# match 'path', to: -> (env) { [200, {}, ["Success!"]] }, via: :get
# match 'path', to: RackApp, via: :get
#
# [:on]
# Shorthand for wrapping routes in a specific RESTful context. Valid
# values are +:member+, +:collection+, and +:new+. Only use within
# <tt>resource(s)</tt> block. For example:
#
# resource :bar do
# match 'foo', to: 'c#a', on: :member, via: [:get, :post]
# end
#
# Is equivalent to:
#
# resource :bar do
# member do
# match 'foo', to: 'c#a', via: [:get, :post]
# end
# end
#
# [:constraints]
# Constrains parameters with a hash of regular expressions
# or an object that responds to <tt>matches?</tt>. In addition, constraints
# other than path can also be specified with any object
# that responds to <tt>===</tt> (e.g. String, Array, Range, etc.).
#
# match 'path/:id', constraints: { id: /[A-Z]\d{5}/ }, via: :get
#
# match 'json_only', constraints: { format: 'json' }, via: :get
#
# class PermitList
# def matches?(request) request.remote_ip == '1.2.3.4' end
# end
# match 'path', to: 'c#a', constraints: PermitList.new, via: :get
#
# See <tt>Scoping#constraints</tt> for more examples with its scope
# equivalent.
#
# [:defaults]
# Sets defaults for parameters
#
# # Sets params[:format] to 'jpg' by default
# match 'path', to: 'c#a', defaults: { format: 'jpg' }, via: :get
#
# See <tt>Scoping#defaults</tt> for its scope equivalent.
#
# [:anchor]
# Boolean to anchor a <tt>match</tt> pattern. Default is true. When set to
# false, the pattern matches any request prefixed with the given path.
#
# # Matches any request starting with 'path'
# match 'path', to: 'c#a', anchor: false, via: :get
#
# [:format]
# Allows you to specify the default value for optional +format+
# segment or disable it by supplying +false+.
- 1
def match(path, options = nil)
end
# Mount a Rack-based application to be used within the application.
#
# mount SomeRackApp, at: "some_route"
#
# Alternatively:
#
# mount(SomeRackApp => "some_route")
#
# For options, see +match+, as +mount+ uses it internally.
#
# All mounted applications come with routing helpers to access them.
# These are named after the class specified, so for the above example
# the helper is either +some_rack_app_path+ or +some_rack_app_url+.
# To customize this helper's name, use the +:as+ option:
#
# mount(SomeRackApp => "some_route", as: "exciting")
#
# This will generate the +exciting_path+ and +exciting_url+ helpers
# which can be used to navigate to this mounted app.
- 1
def mount(app, options = nil)
- 14
if options
- 10
path = options.delete(:at)
- 4
elsif Hash === app
- 4
options = app
- 8
app, path = options.find { |k, _| k.respond_to?(:call) }
- 4
options.delete(app) if app
end
- 14
raise ArgumentError, "A rack application must be specified" unless app.respond_to?(:call)
- 14
raise ArgumentError, <<~MSG unless path
Must be called with mount point
mount SomeRackApp, at: "some_route"
or
mount(SomeRackApp => "some_route")
MSG
- 14
rails_app = rails_app? app
- 14
options[:as] ||= app_name(app, rails_app)
- 14
target_as = name_for_action(options[:as], path)
- 14
options[:via] ||= :all
- 14
match(path, options.merge(to: app, anchor: false, format: false))
- 14
define_generate_prefix(app, target_as) if rails_app
- 14
self
end
- 1
def default_url_options=(options)
- 3
@set.default_url_options = options
end
- 1
alias_method :default_url_options, :default_url_options=
- 1
def with_default_scope(scope, &block)
- 2
scope(scope) do
- 2
instance_exec(&block)
end
end
# Query if the following named route was already defined.
- 1
def has_named_route?(name)
- 233
@set.named_routes.key?(name)
end
- 1
private
- 1
def rails_app?(app)
- 14
app.is_a?(Class) && app < Rails::Railtie
end
- 1
def app_name(app, rails_app)
- 8
if rails_app
app.railtie_name
- 8
elsif app.is_a?(Class)
- 2
class_name = app.name
- 2
ActiveSupport::Inflector.underscore(class_name).tr("/", "_")
end
end
- 1
def define_generate_prefix(app, name)
- 3
_route = @set.named_routes.get name
- 3
_routes = @set
- 3
_url_helpers = @set.url_helpers
- 3
script_namer = ->(options) do
prefix_options = options.slice(*_route.segment_keys)
prefix_options[:relative_url_root] = ""
if options[:_recall]
prefix_options.reverse_merge!(options[:_recall].slice(*_route.segment_keys))
end
# We must actually delete prefix segment keys to avoid passing them to next url_for.
_route.segment_keys.each { |k| options.delete(k) }
_url_helpers.send("#{name}_path", prefix_options)
end
- 3
app.routes.define_mounted_helper(name, script_namer)
- 3
app.routes.extend Module.new {
- 3
def optimize_routes_generation?; false; end
- 3
define_method :find_script_name do |options|
if options.key? :script_name
super(options)
else
script_namer.call(options)
end
end
}
end
end
- 1
module HttpHelpers
# Define a route that only recognizes HTTP GET.
# For supported arguments, see match[rdoc-ref:Base#match]
#
# get 'bacon', to: 'food#bacon'
- 1
def get(*args, &block)
- 228
map_method(:get, args, &block)
end
# Define a route that only recognizes HTTP POST.
# For supported arguments, see match[rdoc-ref:Base#match]
#
# post 'bacon', to: 'food#bacon'
- 1
def post(*args, &block)
- 21
map_method(:post, args, &block)
end
# Define a route that only recognizes HTTP PATCH.
# For supported arguments, see match[rdoc-ref:Base#match]
#
# patch 'bacon', to: 'food#bacon'
- 1
def patch(*args, &block)
- 20
map_method(:patch, args, &block)
end
# Define a route that only recognizes HTTP PUT.
# For supported arguments, see match[rdoc-ref:Base#match]
#
# put 'bacon', to: 'food#bacon'
- 1
def put(*args, &block)
- 20
map_method(:put, args, &block)
end
# Define a route that only recognizes HTTP DELETE.
# For supported arguments, see match[rdoc-ref:Base#match]
#
# delete 'broccoli', to: 'food#broccoli'
- 1
def delete(*args, &block)
- 19
map_method(:delete, args, &block)
end
# Define a route that only recognizes HTTP OPTIONS.
# For supported arguments, see match[rdoc-ref:Base#match]
#
# options 'carrots', to: 'food#carrots'
- 1
def options(*args, &block)
map_method(:options, args, &block)
end
- 1
private
- 1
def map_method(method, args, &block)
- 308
options = args.extract_options!
- 308
options[:via] = method
- 308
match(*args, options, &block)
- 308
self
end
end
# You may wish to organize groups of controllers under a namespace.
# Most commonly, you might group a number of administrative controllers
# under an +admin+ namespace. You would place these controllers under
# the <tt>app/controllers/admin</tt> directory, and you can group them
# together in your router:
#
# namespace "admin" do
# resources :posts, :comments
# end
#
# This will create a number of routes for each of the posts and comments
# controller. For <tt>Admin::PostsController</tt>, Rails will create:
#
# GET /admin/posts
# GET /admin/posts/new
# POST /admin/posts
# GET /admin/posts/1
# GET /admin/posts/1/edit
# PATCH/PUT /admin/posts/1
# DELETE /admin/posts/1
#
# If you want to route /posts (without the prefix /admin) to
# <tt>Admin::PostsController</tt>, you could use
#
# scope module: "admin" do
# resources :posts
# end
#
# or, for a single case
#
# resources :posts, module: "admin"
#
# If you want to route /admin/posts to +PostsController+
# (without the <tt>Admin::</tt> module prefix), you could use
#
# scope "/admin" do
# resources :posts
# end
#
# or, for a single case
#
# resources :posts, path: "/admin/posts"
#
# In each of these cases, the named routes remain the same as if you did
# not use scope. In the last case, the following paths map to
# +PostsController+:
#
# GET /admin/posts
# GET /admin/posts/new
# POST /admin/posts
# GET /admin/posts/1
# GET /admin/posts/1/edit
# PATCH/PUT /admin/posts/1
# DELETE /admin/posts/1
- 1
module Scoping
# Scopes a set of routes to the given default options.
#
# Take the following route definition as an example:
#
# scope path: ":account_id", as: "account" do
# resources :projects
# end
#
# This generates helpers such as +account_projects_path+, just like +resources+ does.
# The difference here being that the routes generated are like /:account_id/projects,
# rather than /accounts/:account_id/projects.
#
# === Options
#
# Takes same options as <tt>Base#match</tt> and <tt>Resources#resources</tt>.
#
# # route /posts (without the prefix /admin) to <tt>Admin::PostsController</tt>
# scope module: "admin" do
# resources :posts
# end
#
# # prefix the posts resource's requests with '/admin'
# scope path: "/admin" do
# resources :posts
# end
#
# # prefix the routing helper name: +sekret_posts_path+ instead of +posts_path+
# scope as: "sekret" do
# resources :posts
# end
- 1
def scope(*args)
- 34
options = args.extract_options!.dup
- 34
scope = {}
- 34
options[:path] = args.flatten.join("/") if args.any?
- 34
options[:constraints] ||= {}
- 34
unless nested_scope?
- 24
options[:shallow_path] ||= options[:path] if options.key?(:path)
- 24
options[:shallow_prefix] ||= options[:as] if options.key?(:as)
end
- 34
if options[:constraints].is_a?(Hash)
- 34
defaults = options[:constraints].select do |k, v|
- 2
URL_OPTIONS.include?(k) && (v.is_a?(String) || v.is_a?(Integer))
end
- 34
options[:defaults] = defaults.merge(options[:defaults] || {})
else
block, options[:constraints] = options[:constraints], {}
end
- 34
if options.key?(:only) || options.key?(:except)
scope[:action_options] = { only: options.delete(:only),
except: options.delete(:except) }
end
- 34
if options.key? :anchor
raise ArgumentError, "anchor is ignored unless passed to `match`"
end
- 34
@scope.options.each do |option|
- 544
if option == :blocks
- 34
value = block
- 510
elsif option == :options
- 34
value = options
else
- 838
value = options.delete(option) { POISON }
end
- 544
unless POISON == value
- 182
scope[option] = send("merge_#{option}_scope", @scope[option], value)
end
end
- 34
@scope = @scope.new scope
- 34
yield
- 34
self
ensure
- 34
@scope = @scope.parent
end
- 1
POISON = Object.new # :nodoc:
# Scopes routes to a specific controller
#
# controller "food" do
# match "bacon", action: :bacon, via: :get
# end
- 1
def controller(controller)
- 21
@scope = @scope.new(controller: controller)
- 21
yield
ensure
- 21
@scope = @scope.parent
end
# Scopes routes to a specific namespace. For example:
#
# namespace :admin do
# resources :posts
# end
#
# This generates the following routes:
#
# admin_posts GET /admin/posts(.:format) admin/posts#index
# admin_posts POST /admin/posts(.:format) admin/posts#create
# new_admin_post GET /admin/posts/new(.:format) admin/posts#new
# edit_admin_post GET /admin/posts/:id/edit(.:format) admin/posts#edit
# admin_post GET /admin/posts/:id(.:format) admin/posts#show
# admin_post PATCH/PUT /admin/posts/:id(.:format) admin/posts#update
# admin_post DELETE /admin/posts/:id(.:format) admin/posts#destroy
#
# === Options
#
# The +:path+, +:as+, +:module+, +:shallow_path+ and +:shallow_prefix+
# options all default to the name of the namespace.
#
# For options, see <tt>Base#match</tt>. For +:shallow_path+ option, see
# <tt>Resources#resources</tt>.
#
# # accessible through /sekret/posts rather than /admin/posts
# namespace :admin, path: "sekret" do
# resources :posts
# end
#
# # maps to <tt>Sekret::PostsController</tt> rather than <tt>Admin::PostsController</tt>
# namespace :admin, module: "sekret" do
# resources :posts
# end
#
# # generates +sekret_posts_path+ rather than +admin_posts_path+
# namespace :admin, as: "sekret" do
# resources :posts
# end
- 1
def namespace(path, options = {})
- 3
path = path.to_s
- 3
defaults = {
module: path,
as: options.fetch(:as, path),
shallow_path: options.fetch(:path, path),
shallow_prefix: options.fetch(:as, path)
}
- 6
path_scope(options.delete(:path) { path }) do
- 6
scope(defaults.merge!(options)) { yield }
end
end
# === Parameter Restriction
# Allows you to constrain the nested routes based on a set of rules.
# For instance, in order to change the routes to allow for a dot character in the +id+ parameter:
#
# constraints(id: /\d+\.\d+/) do
# resources :posts
# end
#
# Now routes such as +/posts/1+ will no longer be valid, but +/posts/1.1+ will be.
# The +id+ parameter must match the constraint passed in for this example.
#
# You may use this to also restrict other parameters:
#
# resources :posts do
# constraints(post_id: /\d+\.\d+/) do
# resources :comments
# end
# end
#
# === Restricting based on IP
#
# Routes can also be constrained to an IP or a certain range of IP addresses:
#
# constraints(ip: /192\.168\.\d+\.\d+/) do
# resources :posts
# end
#
# Any user connecting from the 192.168.* range will be able to see this resource,
# where as any user connecting outside of this range will be told there is no such route.
#
# === Dynamic request matching
#
# Requests to routes can be constrained based on specific criteria:
#
# constraints(-> (req) { /iPhone/.match?(req.env["HTTP_USER_AGENT"]) }) do
# resources :iphones
# end
#
# You are able to move this logic out into a class if it is too complex for routes.
# This class must have a +matches?+ method defined on it which either returns +true+
# if the user should be given access to that route, or +false+ if the user should not.
#
# class Iphone
# def self.matches?(request)
# /iPhone/.match?(request.env["HTTP_USER_AGENT"])
# end
# end
#
# An expected place for this code would be +lib/constraints+.
#
# This class is then used like this:
#
# constraints(Iphone) do
# resources :iphones
# end
- 1
def constraints(constraints = {})
- 2
scope(constraints: constraints) { yield }
end
# Allows you to set default parameters for a route, such as this:
# defaults id: 'home' do
# match 'scoped_pages/(:id)', to: 'pages#show'
# end
# Using this, the +:id+ parameter here will default to 'home'.
- 1
def defaults(defaults = {})
@scope = @scope.new(defaults: merge_defaults_scope(@scope[:defaults], defaults))
yield
ensure
@scope = @scope.parent
end
- 1
private
- 1
def merge_path_scope(parent, child)
- 81
Mapper.normalize_path("#{parent}/#{child}")
end
- 1
def merge_shallow_path_scope(parent, child)
- 9
Mapper.normalize_path("#{parent}/#{child}")
end
- 1
def merge_as_scope(parent, child)
- 13
parent ? "#{parent}_#{child}" : child
end
- 1
def merge_shallow_prefix_scope(parent, child)
- 3
parent ? "#{parent}_#{child}" : child
end
- 1
def merge_module_scope(parent, child)
- 14
parent ? "#{parent}/#{child}" : child
end
- 1
def merge_controller_scope(parent, child)
child
end
- 1
def merge_action_scope(parent, child)
child
end
- 1
def merge_via_scope(parent, child)
child
end
- 1
def merge_format_scope(parent, child)
- 1
child
end
- 1
def merge_path_names_scope(parent, child)
merge_options_scope(parent, child)
end
- 1
def merge_constraints_scope(parent, child)
- 34
merge_options_scope(parent, child)
end
- 1
def merge_defaults_scope(parent, child)
- 34
merge_options_scope(parent, child)
end
- 1
def merge_blocks_scope(parent, child)
- 34
merged = parent ? parent.dup : []
- 34
merged << child if child
- 34
merged
end
- 1
def merge_options_scope(parent, child)
- 102
(parent || {}).merge(child)
end
- 1
def merge_shallow_scope(parent, child)
child ? true : false
end
- 1
def merge_to_scope(parent, child)
child
end
end
# Resource routing allows you to quickly declare all of the common routes
# for a given resourceful controller. Instead of declaring separate routes
# for your +index+, +show+, +new+, +edit+, +create+, +update+ and +destroy+
# actions, a resourceful route declares them in a single line of code:
#
# resources :photos
#
# Sometimes, you have a resource that clients always look up without
# referencing an ID. A common example, /profile always shows the profile of
# the currently logged in user. In this case, you can use a singular resource
# to map /profile (rather than /profile/:id) to the show action.
#
# resource :profile
#
# It's common to have resources that are logically children of other
# resources:
#
# resources :magazines do
# resources :ads
# end
#
# You may wish to organize groups of controllers under a namespace. Most
# commonly, you might group a number of administrative controllers under
# an +admin+ namespace. You would place these controllers under the
# <tt>app/controllers/admin</tt> directory, and you can group them together
# in your router:
#
# namespace "admin" do
# resources :posts, :comments
# end
#
# By default the +:id+ parameter doesn't accept dots. If you need to
# use dots as part of the +:id+ parameter add a constraint which
# overrides this restriction, e.g:
#
# resources :articles, id: /[^\/]+/
#
# This allows any character other than a slash as part of your +:id+.
#
- 1
module Resources
# CANONICAL_ACTIONS holds all actions that does not need a prefix or
# a path appended since they fit properly in their scope level.
- 1
VALID_ON_OPTIONS = [:new, :collection, :member]
- 1
RESOURCE_OPTIONS = [:as, :controller, :path, :only, :except, :param, :concerns]
- 1
CANONICAL_ACTIONS = %w(index create new show update destroy)
- 1
class Resource #:nodoc:
- 1
attr_reader :controller, :path, :param
- 1
def initialize(entities, api_only, shallow, options = {})
- 21
if options[:param].to_s.include?(":")
raise ArgumentError, ":param option can't contain colons"
end
- 21
@name = entities.to_s
- 21
@path = (options[:path] || @name).to_s
- 21
@controller = (options[:controller] || @name).to_s
- 21
@as = options[:as]
- 21
@param = (options[:param] || :id).to_sym
- 21
@options = options
- 21
@shallow = shallow
- 21
@api_only = api_only
- 21
@only = options.delete :only
- 21
@except = options.delete :except
end
- 1
def default_actions
- 126
if @api_only
[:index, :create, :show, :update, :destroy]
else
- 126
[:index, :create, :new, :show, :update, :destroy, :edit]
end
end
- 1
def actions
- 145
if @except
- 7
available_actions - Array(@except).map(&:to_sym)
else
- 138
available_actions
end
end
- 1
def available_actions
- 145
if @only
- 7
Array(@only).map(&:to_sym)
else
- 138
default_actions
end
end
- 1
def name
- 42
@as || @name
end
- 1
def plural
- 292
@plural ||= name.to_s
end
- 1
def singular
- 304
@singular ||= name.to_s.singularize
end
- 1
alias :member_name :singular
# Checks for uncountable plurals, and appends "_index" if the plural
# and singular form are the same.
- 1
def collection_name
- 146
singular == plural ? "#{plural}_index" : plural
end
- 1
def resource_scope
- 21
controller
end
- 1
alias :collection_scope :path
- 1
def member_scope
- 19
"#{path}/:#{param}"
end
- 1
alias :shallow_scope :member_scope
- 1
def new_scope(new_path)
- 20
"#{path}/#{new_path}"
end
- 1
def nested_param
- 6
:"#{singular}_#{param}"
end
- 1
def nested_scope
- 6
"#{path}/:#{nested_param}"
end
- 1
def shallow?
@shallow
end
- 26
def singleton?; false; end
end
- 1
class SingletonResource < Resource #:nodoc:
- 1
def initialize(entities, api_only, shallow, options)
- 2
super
- 2
@as = nil
- 2
@controller = (options[:controller] || plural).to_s
- 2
@as = options[:as]
end
- 1
def default_actions
- 12
if @api_only
[:show, :create, :update, :destroy]
else
- 12
[:show, :create, :update, :destroy, :new, :edit]
end
end
- 1
def plural
- 2
@plural ||= name.to_s.pluralize
end
- 1
def singular
- 32
@singular ||= name.to_s
end
- 1
alias :member_name :singular
- 1
alias :collection_name :singular
- 1
alias :member_scope :path
- 1
alias :nested_scope :path
- 7
def singleton?; true; end
end
- 1
def resources_path_names(options)
@scope[:path_names].merge!(options)
end
# Sometimes, you have a resource that clients always look up without
# referencing an ID. A common example, /profile always shows the
# profile of the currently logged in user. In this case, you can use
# a singular resource to map /profile (rather than /profile/:id) to
# the show action:
#
# resource :profile
#
# This creates six different routes in your application, all mapping to
# the +Profiles+ controller (note that the controller is named after
# the plural):
#
# GET /profile/new
# GET /profile
# GET /profile/edit
# PATCH/PUT /profile
# DELETE /profile
# POST /profile
#
# === Options
# Takes same options as resources[rdoc-ref:#resources]
- 1
def resource(*resources, &block)
- 3
options = resources.extract_options!.dup
- 3
if apply_common_behavior_for(:resource, resources, options, &block)
- 1
return self
end
- 2
with_scope_level(:resource) do
- 2
options = apply_action_options options
- 2
resource_scope(SingletonResource.new(resources.pop, api_only?, @scope[:shallow], options)) do
- 2
yield if block_given?
- 2
concerns(options[:concerns]) if options[:concerns]
new do
- 2
get :new
- 2
end if parent_resource.actions.include?(:new)
- 2
set_member_mappings_for_resource
collection do
- 2
post :create
- 2
end if parent_resource.actions.include?(:create)
end
end
- 2
self
end
# In Rails, a resourceful route provides a mapping between HTTP verbs
# and URLs and controller actions. By convention, each action also maps
# to particular CRUD operations in a database. A single entry in the
# routing file, such as
#
# resources :photos
#
# creates seven different routes in your application, all mapping to
# the +Photos+ controller:
#
# GET /photos
# GET /photos/new
# POST /photos
# GET /photos/:id
# GET /photos/:id/edit
# PATCH/PUT /photos/:id
# DELETE /photos/:id
#
# Resources can also be nested infinitely by using this block syntax:
#
# resources :photos do
# resources :comments
# end
#
# This generates the following comments routes:
#
# GET /photos/:photo_id/comments
# GET /photos/:photo_id/comments/new
# POST /photos/:photo_id/comments
# GET /photos/:photo_id/comments/:id
# GET /photos/:photo_id/comments/:id/edit
# PATCH/PUT /photos/:photo_id/comments/:id
# DELETE /photos/:photo_id/comments/:id
#
# === Options
# Takes same options as match[rdoc-ref:Base#match] as well as:
#
# [:path_names]
# Allows you to change the segment component of the +edit+ and +new+ actions.
# Actions not specified are not changed.
#
# resources :posts, path_names: { new: "brand_new" }
#
# The above example will now change /posts/new to /posts/brand_new.
#
# [:path]
# Allows you to change the path prefix for the resource.
#
# resources :posts, path: 'postings'
#
# The resource and all segments will now route to /postings instead of /posts.
#
# [:only]
# Only generate routes for the given actions.
#
# resources :cows, only: :show
# resources :cows, only: [:show, :index]
#
# [:except]
# Generate all routes except for the given actions.
#
# resources :cows, except: :show
# resources :cows, except: [:show, :index]
#
# [:shallow]
# Generates shallow routes for nested resource(s). When placed on a parent resource,
# generates shallow routes for all nested resources.
#
# resources :posts, shallow: true do
# resources :comments
# end
#
# Is the same as:
#
# resources :posts do
# resources :comments, except: [:show, :edit, :update, :destroy]
# end
# resources :comments, only: [:show, :edit, :update, :destroy]
#
# This allows URLs for resources that otherwise would be deeply nested such
# as a comment on a blog post like <tt>/posts/a-long-permalink/comments/1234</tt>
# to be shortened to just <tt>/comments/1234</tt>.
#
# Set <tt>shallow: false</tt> on a child resource to ignore a parent's shallow parameter.
#
# [:shallow_path]
# Prefixes nested shallow routes with the specified path.
#
# scope shallow_path: "sekret" do
# resources :posts do
# resources :comments, shallow: true
# end
# end
#
# The +comments+ resource here will have the following routes generated for it:
#
# post_comments GET /posts/:post_id/comments(.:format)
# post_comments POST /posts/:post_id/comments(.:format)
# new_post_comment GET /posts/:post_id/comments/new(.:format)
# edit_comment GET /sekret/comments/:id/edit(.:format)
# comment GET /sekret/comments/:id(.:format)
# comment PATCH/PUT /sekret/comments/:id(.:format)
# comment DELETE /sekret/comments/:id(.:format)
#
# [:shallow_prefix]
# Prefixes nested shallow route names with specified prefix.
#
# scope shallow_prefix: "sekret" do
# resources :posts do
# resources :comments, shallow: true
# end
# end
#
# The +comments+ resource here will have the following routes generated for it:
#
# post_comments GET /posts/:post_id/comments(.:format)
# post_comments POST /posts/:post_id/comments(.:format)
# new_post_comment GET /posts/:post_id/comments/new(.:format)
# edit_sekret_comment GET /comments/:id/edit(.:format)
# sekret_comment GET /comments/:id(.:format)
# sekret_comment PATCH/PUT /comments/:id(.:format)
# sekret_comment DELETE /comments/:id(.:format)
#
# [:format]
# Allows you to specify the default value for optional +format+
# segment or disable it by supplying +false+.
#
# [:param]
# Allows you to override the default param name of +:id+ in the URL.
#
# === Examples
#
# # routes call <tt>Admin::PostsController</tt>
# resources :posts, module: "admin"
#
# # resource actions are at /admin/posts.
# resources :posts, path: "admin/posts"
- 1
def resources(*resources, &block)
- 30
options = resources.extract_options!.dup
- 30
if apply_common_behavior_for(:resources, resources, options, &block)
- 11
return self
end
- 19
with_scope_level(:resources) do
- 19
options = apply_action_options options
- 19
resource_scope(Resource.new(resources.pop, api_only?, @scope[:shallow], options)) do
- 19
yield if block_given?
- 19
concerns(options[:concerns]) if options[:concerns]
- 19
collection do
- 19
get :index if parent_resource.actions.include?(:index)
- 19
post :create if parent_resource.actions.include?(:create)
end
new do
- 18
get :new
- 19
end if parent_resource.actions.include?(:new)
- 19
set_member_mappings_for_resource
end
end
- 19
self
end
# To add a route to the collection:
#
# resources :photos do
# collection do
# get 'search'
# end
# end
#
# This will enable Rails to recognize paths such as <tt>/photos/search</tt>
# with GET, and route to the search action of +PhotosController+. It will also
# create the <tt>search_photos_url</tt> and <tt>search_photos_path</tt>
# route helpers.
- 1
def collection
- 21
unless resource_scope?
raise ArgumentError, "can't use collection outside resource(s) scope"
end
- 21
with_scope_level(:collection) do
- 21
path_scope(parent_resource.collection_scope) do
- 21
yield
end
end
end
# To add a member route, add a member block into the resource block:
#
# resources :photos do
# member do
# get 'preview'
# end
# end
#
# This will recognize <tt>/photos/1/preview</tt> with GET, and route to the
# preview action of +PhotosController+. It will also create the
# <tt>preview_photo_url</tt> and <tt>preview_photo_path</tt> helpers.
- 1
def member
- 21
unless resource_scope?
raise ArgumentError, "can't use member outside resource(s) scope"
end
- 21
with_scope_level(:member) do
- 21
if shallow?
shallow_scope {
path_scope(parent_resource.member_scope) { yield }
}
else
- 42
path_scope(parent_resource.member_scope) { yield }
end
end
end
- 1
def new
- 20
unless resource_scope?
raise ArgumentError, "can't use new outside resource(s) scope"
end
- 20
with_scope_level(:new) do
- 20
path_scope(parent_resource.new_scope(action_path(:new))) do
- 20
yield
end
end
end
- 1
def nested
- 10
unless resource_scope?
raise ArgumentError, "can't use nested outside resource(s) scope"
end
- 10
with_scope_level(:nested) do
- 10
if shallow? && shallow_nesting_depth >= 1
shallow_scope do
path_scope(parent_resource.nested_scope) do
scope(nested_options) { yield }
end
end
else
- 10
path_scope(parent_resource.nested_scope) do
- 20
scope(nested_options) { yield }
end
end
end
end
# See ActionDispatch::Routing::Mapper::Scoping#namespace.
- 1
def namespace(path, options = {})
- 3
if resource_scope?
nested { super }
else
- 3
super
end
end
- 1
def shallow
@scope = @scope.new(shallow: true)
yield
ensure
@scope = @scope.parent
end
- 1
def shallow?
- 31
!parent_resource.singleton? && @scope[:shallow]
end
- 1
def draw(name)
path = @draw_paths.find do |_path|
File.exist? "#{_path}/#{name}.rb"
end
unless path
msg = "Your router tried to #draw the external file #{name}.rb,\n" \
"but the file was not found in:\n\n"
msg += @draw_paths.map { |_path| " * #{_path}" }.join("\n")
raise ArgumentError, msg
end
route_path = "#{path}/#{name}.rb"
instance_eval(File.read(route_path), route_path.to_s)
end
# Matches a URL pattern to one or more routes.
# For more information, see match[rdoc-ref:Base#match].
#
# match 'path' => 'controller#action', via: :patch
# match 'path', to: 'controller#action', via: :post
# match 'path', 'otherpath', on: :member, via: :get
- 1
def match(path, *rest, &block)
- 326
if rest.empty? && Hash === path
- 42
options = path
- 84
path, to = options.find { |name, _value| name.is_a?(String) }
- 42
raise ArgumentError, "Route path not specified" if path.nil?
- 42
case to
when Symbol
options[:action] = to
when String
- 13
if /#/.match?(to)
- 13
options[:to] = to
else
options[:controller] = to
end
else
- 29
options[:to] = to
end
- 42
options.delete(path)
- 42
paths = [path]
else
- 284
options = rest.pop || {}
- 284
paths = [path] + rest
end
- 326
if options.key?(:defaults)
defaults(options.delete(:defaults)) { map_match(paths, options, &block) }
else
- 326
map_match(paths, options, &block)
end
end
# You can specify what Rails should route "/" to with the root method:
#
# root to: 'pages#main'
#
# For options, see +match+, as +root+ uses it internally.
#
# You can also pass a string which will expand
#
# root 'pages#main'
#
# You should put the root route at the top of <tt>config/routes.rb</tt>,
# because this means it will be matched first. As this is the most popular route
# of most Rails applications, this is beneficial.
- 1
def root(path, options = {})
- 3
if path.is_a?(String)
options[:to] = path
- 3
elsif path.is_a?(Hash) && options.empty?
- 3
options = path
else
raise ArgumentError, "must be called with a path and/or options"
end
- 3
if @scope.resources?
with_scope_level(:root) do
path_scope(parent_resource.path) do
match_root_route(options)
end
end
else
- 3
match_root_route(options)
end
end
- 1
private
- 1
def parent_resource
- 915
@scope[:scope_level_resource]
end
- 1
def apply_common_behavior_for(method, resources, options, &block)
- 33
if resources.length > 1
- 5
resources.each { |r| send(method, r, options, &block) }
- 1
return true
end
- 32
if options[:shallow]
options.delete(:shallow)
shallow do
send(method, resources.pop, options, &block)
end
return true
end
- 32
if resource_scope?
- 18
nested { send(method, resources.pop, options, &block) }
- 9
return true
end
- 23
options.keys.each do |k|
- 9
(options[:constraints] ||= {})[k] = options.delete(k) if options[k].is_a?(Regexp)
end
- 23
scope_options = options.slice!(*RESOURCE_OPTIONS)
- 23
unless scope_options.empty?
- 2
scope(scope_options) do
- 2
send(method, resources.pop, options, &block)
end
- 2
return true
end
- 21
false
end
- 1
def apply_action_options(options)
- 21
return options if action_options? options
- 19
options.merge scope_action_options
end
- 1
def action_options?(options)
- 21
options[:only] || options[:except]
end
- 1
def scope_action_options
- 19
@scope[:action_options] || {}
end
- 1
def resource_scope?
- 107
@scope.resource_scope?
end
- 1
def resource_method_scope?
- 429
@scope.resource_method_scope?
end
- 1
def nested_scope?
- 34
@scope.nested?
end
- 1
def with_scope_level(kind) # :doc:
- 93
@scope = @scope.new_level(kind)
- 93
yield
ensure
- 93
@scope = @scope.parent
end
- 1
def resource_scope(resource)
- 21
@scope = @scope.new(scope_level_resource: resource)
- 42
controller(resource.resource_scope) { yield }
ensure
- 21
@scope = @scope.parent
end
- 1
def nested_options
- 10
options = { as: parent_resource.member_name }
options[:constraints] = {
parent_resource.nested_param => param_constraint
- 10
} if param_constraint?
- 10
options
end
- 1
def shallow_nesting_depth
@scope.find_all { |node|
node.frame[:scope_level_resource]
}.count { |node| node.frame[:scope_level_resource].shallow? }
end
- 1
def param_constraint?
- 10
@scope[:constraints] && @scope[:constraints][parent_resource.param].is_a?(Regexp)
end
- 1
def param_constraint
@scope[:constraints][parent_resource.param]
end
- 1
def canonical_action?(action)
- 429
resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s)
end
- 1
def shallow_scope
scope = { as: @scope[:shallow_prefix],
path: @scope[:shallow_path] }
@scope = @scope.new scope
yield
ensure
@scope = @scope.parent
end
- 1
def path_for_action(action, path)
- 326
return "#{@scope[:path]}/#{path}" if path
- 158
if canonical_action?(action)
- 138
@scope[:path].to_s
else
- 20
"#{@scope[:path]}/#{action_path(action)}"
end
end
- 1
def action_path(name)
- 40
@scope[:path_names][name.to_sym] || name
end
- 1
def prefix_name_for_action(as, action)
- 334
if as
- 63
prefix = as
- 271
elsif !canonical_action?(action)
- 133
prefix = action
end
- 334
if prefix && prefix != "/" && !prefix.empty?
- 158
Mapper.normalize_name prefix.to_s.tr("-", "_")
end
end
- 1
def name_for_action(as, action)
- 334
prefix = prefix_name_for_action(as, action)
- 334
name_prefix = @scope[:as]
- 334
if parent_resource
- 160
return nil unless as || action
- 160
collection_name = parent_resource.collection_name
- 160
member_name = parent_resource.member_name
end
- 334
action_name = @scope.action_name(name_prefix, prefix, collection_name, member_name)
- 334
candidate = action_name.select(&:present?).join("_")
- 334
unless candidate.empty?
# If a name was not explicitly given, we check if it is valid
# and return nil in case it isn't. Otherwise, we pass the invalid name
# forward so the underlying router engine treats it and raises an exception.
- 296
if as.nil?
- 233
candidate unless !candidate.match?(/\A[_a-z]/i) || has_named_route?(candidate)
else
- 63
candidate
end
end
end
- 1
def set_member_mappings_for_resource # :doc:
- 21
member do
- 21
get :edit if parent_resource.actions.include?(:edit)
- 21
get :show if parent_resource.actions.include?(:show)
- 21
if parent_resource.actions.include?(:update)
- 20
patch :update
- 20
put :update
end
- 21
delete :destroy if parent_resource.actions.include?(:destroy)
end
end
- 1
def api_only? # :doc:
- 21
@set.api_only?
end
- 1
def path_scope(path)
- 75
@scope = @scope.new(path: merge_path_scope(@scope[:path], path))
- 75
yield
ensure
- 75
@scope = @scope.parent
end
- 1
def map_match(paths, options)
- 326
if options[:on] && !VALID_ON_OPTIONS.include?(options[:on])
raise ArgumentError, "Unknown scope #{on.inspect} given to :on"
end
- 326
if @scope[:to]
options[:to] ||= @scope[:to]
end
- 326
if @scope[:controller] && @scope[:action]
options[:to] ||= "#{@scope[:controller]}##{@scope[:action]}"
end
- 326
controller = options.delete(:controller) || @scope[:controller]
- 326
option_path = options.delete :path
- 326
to = options.delete :to
- 326
via = Mapping.check_via Array(options.delete(:via) {
@scope[:via]
})
- 636
formatted = options.delete(:format) { @scope[:format] }
- 637
anchor = options.delete(:anchor) { true }
- 326
options_constraints = options.delete(:constraints) || {}
- 326
path_types = paths.group_by(&:class)
- 326
(path_types[String] || []).each do |_path|
- 168
route_options = options.dup
- 168
if _path && option_path
raise ArgumentError, "Ambiguous route definition. Both :path and the route path were specified as strings."
end
- 168
to = get_to_from_path(_path, to, route_options[:action])
- 168
decomposed_match(_path, controller, route_options, _path, to, via, formatted, anchor, options_constraints)
end
- 326
(path_types[Symbol] || []).each do |action|
- 158
route_options = options.dup
- 158
decomposed_match(action, controller, route_options, option_path, to, via, formatted, anchor, options_constraints)
end
- 326
self
end
- 1
def get_to_from_path(path, to, action)
- 168
return to if to || action
- 5
path_without_format = path.sub(/\(\.:format\)$/, "")
- 5
if using_match_shorthand?(path_without_format)
path_without_format.delete_prefix("/").sub(%r{/([^/]*)$}, '#\1').tr("-", "_")
else
nil
end
end
- 1
def using_match_shorthand?(path)
- 5
%r{^/?[-\w]+/[-\w/]+$}.match?(path)
end
- 1
def decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints)
- 327
if on = options.delete(:on)
send(on) { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) }
else
- 327
case @scope.scope_level
when :resources
- 2
nested { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) }
when :resource
member { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) }
else
- 326
add_route(path, controller, options, _path, to, via, formatted, anchor, options_constraints)
end
end
end
- 1
def add_route(action, controller, options, _path, to, via, formatted, anchor, options_constraints)
- 326
path = path_for_action(action, _path)
- 326
raise ArgumentError, "path is required" if path.blank?
- 326
action = action.to_s
- 326
default_action = options.delete(:action) || @scope[:action]
- 326
if /^[\w\-\/]+$/.match?(action)
- 270
default_action ||= action.tr("-", "_") unless action.include?("/")
else
- 56
action = nil
end
- 326
as = if !options.fetch(:as, true) # if it's set to nil or false
- 6
options.delete(:as)
else
- 320
name_for_action(options.delete(:as), action)
end
- 326
path = Mapping.normalize_path URI::DEFAULT_PARSER.escape(path), formatted
- 326
ast = Journey::Parser.parse path
- 326
mapping = Mapping.build(@scope, @set, ast, controller, default_action, to, via, formatted, options_constraints, anchor, options)
- 326
@set.add_route(mapping, as)
end
- 1
def match_root_route(options)
- 3
args = ["/", { as: :root, via: :get }.merge(options)]
- 3
match(*args)
end
end
# Routing Concerns allow you to declare common routes that can be reused
# inside others resources and routes.
#
# concern :commentable do
# resources :comments
# end
#
# concern :image_attachable do
# resources :images, only: :index
# end
#
# These concerns are used in Resources routing:
#
# resources :messages, concerns: [:commentable, :image_attachable]
#
# or in a scope or namespace:
#
# namespace :posts do
# concerns :commentable
# end
- 1
module Concerns
# Define a routing concern using a name.
#
# Concerns may be defined inline, using a block, or handled by
# another object, by passing that object as the second parameter.
#
# The concern object, if supplied, should respond to <tt>call</tt>,
# which will receive two parameters:
#
# * The current mapper
# * A hash of options which the concern object may use
#
# Options may also be used by concerns defined in a block by accepting
# a block parameter. So, using a block, you might do something as
# simple as limit the actions available on certain resources, passing
# standard resource options through the concern:
#
# concern :commentable do |options|
# resources :comments, options
# end
#
# resources :posts, concerns: :commentable
# resources :archived_posts do
# # Don't allow comments on archived posts
# concerns :commentable, only: [:index, :show]
# end
#
# Or, using a callable object, you might implement something more
# specific to your application, which would be out of place in your
# routes file.
#
# # purchasable.rb
# class Purchasable
# def initialize(defaults = {})
# @defaults = defaults
# end
#
# def call(mapper, options = {})
# options = @defaults.merge(options)
# mapper.resources :purchases
# mapper.resources :receipts
# mapper.resources :returns if options[:returnable]
# end
# end
#
# # routes.rb
# concern :purchasable, Purchasable.new(returnable: true)
#
# resources :toys, concerns: :purchasable
# resources :electronics, concerns: :purchasable
# resources :pets do
# concerns :purchasable, returnable: false
# end
#
# Any routing helpers can be used inside a concern. If using a
# callable, they're accessible from the Mapper that's passed to
# <tt>call</tt>.
- 1
def concern(name, callable = nil, &block)
- 9
callable ||= lambda { |mapper, options| mapper.instance_exec(options, &block) }
- 3
@concerns[name] = callable
end
# Use the named concerns
#
# resources :posts do
# concerns :commentable
# end
#
# Concerns also work in any routes helper that you want to use:
#
# namespace :posts do
# concerns :commentable
# end
- 1
def concerns(*args)
- 6
options = args.extract_options!
- 6
args.flatten.each do |name|
- 8
if concern = @concerns[name]
- 8
concern.call(self, options)
else
raise ArgumentError, "No concern named #{name} was found!"
end
end
end
end
- 1
module CustomUrls
# Define custom URL helpers that will be added to the application's
# routes. This allows you to override and/or replace the default behavior
# of routing helpers, e.g:
#
# direct :homepage do
# "https://rubyonrails.org"
# end
#
# direct :commentable do |model|
# [ model, anchor: model.dom_id ]
# end
#
# direct :main do
# { controller: "pages", action: "index", subdomain: "www" }
# end
#
# The return value from the block passed to +direct+ must be a valid set of
# arguments for +url_for+ which will actually build the URL string. This can
# be one of the following:
#
# * A string, which is treated as a generated URL
# * A hash, e.g. <tt>{ controller: "pages", action: "index" }</tt>
# * An array, which is passed to +polymorphic_url+
# * An Active Model instance
# * An Active Model class
#
# NOTE: Other URL helpers can be called in the block but be careful not to invoke
# your custom URL helper again otherwise it will result in a stack overflow error.
#
# You can also specify default options that will be passed through to
# your URL helper definition, e.g:
#
# direct :browse, page: 1, size: 10 do |options|
# [ :products, options.merge(params.permit(:page, :size).to_h.symbolize_keys) ]
# end
#
# In this instance the +params+ object comes from the context in which the
# block is executed, e.g. generating a URL inside a controller action or a view.
# If the block is executed where there isn't a +params+ object such as this:
#
# Rails.application.routes.url_helpers.browse_path
#
# then it will raise a +NameError+. Because of this you need to be aware of the
# context in which you will use your custom URL helper when defining it.
#
# NOTE: The +direct+ method can't be used inside of a scope block such as
# +namespace+ or +scope+ and will raise an error if it detects that it is.
- 1
def direct(name, options = {}, &block)
- 12
unless @scope.root?
raise RuntimeError, "The direct method can't be used inside a routes scope block"
end
- 12
@set.add_url_helper(name, options, &block)
end
# Define custom polymorphic mappings of models to URLs. This alters the
# behavior of +polymorphic_url+ and consequently the behavior of
# +link_to+ and +form_for+ when passed a model instance, e.g:
#
# resource :basket
#
# resolve "Basket" do
# [:basket]
# end
#
# This will now generate "/basket" when a +Basket+ instance is passed to
# +link_to+ or +form_for+ instead of the standard "/baskets/:id".
#
# NOTE: This custom behavior only applies to simple polymorphic URLs where
# a single model instance is passed and not more complicated forms, e.g:
#
# # config/routes.rb
# resource :profile
# namespace :admin do
# resources :users
# end
#
# resolve("User") { [:profile] }
#
# # app/views/application/_menu.html.erb
# link_to "Profile", @current_user
# link_to "Profile", [:admin, @current_user]
#
# The first +link_to+ will generate "/profile" but the second will generate
# the standard polymorphic URL of "/admin/users/1".
#
# You can pass options to a polymorphic mapping - the arity for the block
# needs to be two as the instance is passed as the first argument, e.g:
#
# resolve "Basket", anchor: "items" do |basket, options|
# [:basket, options]
# end
#
# This generates the URL "/basket#items" because when the last item in an
# array passed to +polymorphic_url+ is a hash then it's treated as options
# to the URL helper that gets called.
#
# NOTE: The +resolve+ method can't be used inside of a scope block such as
# +namespace+ or +scope+ and will raise an error if it detects that it is.
- 1
def resolve(*args, &block)
- 6
unless @scope.root?
raise RuntimeError, "The resolve method can't be used inside a routes scope block"
end
- 6
options = args.extract_options!
- 6
args = args.flatten(1)
- 6
args.each do |klass|
- 8
@set.add_polymorphic_mapping(klass, options, &block)
end
end
end
- 1
class Scope # :nodoc:
- 1
OPTIONS = [:path, :shallow_path, :as, :shallow_prefix, :module,
:controller, :action, :path_names, :constraints,
:shallow, :blocks, :defaults, :via, :format, :options, :to]
- 1
RESOURCE_SCOPES = [:resource, :resources]
- 1
RESOURCE_METHOD_SCOPES = [:collection, :member, :new]
- 1
attr_reader :parent, :scope_level
- 1
def initialize(hash, parent = NULL, scope_level = nil)
- 299
@hash = hash
- 299
@parent = parent
- 299
@scope_level = scope_level
end
- 1
def nested?
- 34
scope_level == :nested
end
- 1
def null?
- 18
@hash.nil? && @parent.nil?
end
- 1
def root?
- 18
@parent.null?
end
- 1
def resources?
- 3
scope_level == :resources
end
- 1
def resource_method_scope?
- 429
RESOURCE_METHOD_SCOPES.include? scope_level
end
- 1
def action_name(name_prefix, prefix, collection_name, member_name)
- 334
case scope_level
when :nested
- 1
[name_prefix, prefix]
when :collection
- 39
[prefix, name_prefix, collection_name]
when :new
- 20
[prefix, :new, name_prefix, member_name]
when :member
- 99
[prefix, name_prefix, member_name]
when :root
[name_prefix, collection_name, prefix]
else
- 175
[name_prefix, member_name, prefix]
end
end
- 1
def resource_scope?
- 107
RESOURCE_SCOPES.include? scope_level
end
- 1
def options
- 34
OPTIONS
end
- 1
def new(hash)
- 151
self.class.new hash, self, scope_level
end
- 1
def new_level(level)
- 93
self.class.new(frame, self, level)
end
- 1
def [](key)
- 24998
scope = find { |node| node.frame.key? key }
- 5351
scope && scope.frame[key]
end
- 1
include Enumerable
- 1
def each
- 5351
node = self
- 5351
until node.equal? NULL
- 19647
yield node
- 17684
node = node.parent
end
end
- 21704
def frame; @hash; end
- 1
NULL = Scope.new(nil, nil)
end
- 1
def initialize(set) #:nodoc:
- 54
@set = set
- 54
@draw_paths = set.draw_paths
- 54
@scope = Scope.new(path_names: @set.resources_path_names)
- 54
@concerns = {}
end
- 1
include Base
- 1
include HttpHelpers
- 1
include Redirection
- 1
include Scoping
- 1
include Concerns
- 1
include Resources
- 1
include CustomUrls
end
end
end
# frozen_string_literal: true
- 1
module ActionDispatch
- 1
module Routing
# Polymorphic URL helpers are methods for smart resolution to a named route call when
# given an Active Record model instance. They are to be used in combination with
# ActionController::Resources.
#
# These methods are useful when you want to generate the correct URL or path to a RESTful
# resource without having to know the exact type of the record in question.
#
# Nested resources and/or namespaces are also supported, as illustrated in the example:
#
# polymorphic_url([:admin, @article, @comment])
#
# results in:
#
# admin_article_comment_url(@article, @comment)
#
# == Usage within the framework
#
# Polymorphic URL helpers are used in a number of places throughout the \Rails framework:
#
# * <tt>url_for</tt>, so you can use it with a record as the argument, e.g.
# <tt>url_for(@article)</tt>;
# * ActionView::Helpers::FormHelper uses <tt>polymorphic_path</tt>, so you can write
# <tt>form_for(@article)</tt> without having to specify <tt>:url</tt> parameter for the form
# action;
# * <tt>redirect_to</tt> (which, in fact, uses <tt>url_for</tt>) so you can write
# <tt>redirect_to(post)</tt> in your controllers;
# * ActionView::Helpers::AtomFeedHelper, so you don't have to explicitly specify URLs
# for feed entries.
#
# == Prefixed polymorphic helpers
#
# In addition to <tt>polymorphic_url</tt> and <tt>polymorphic_path</tt> methods, a
# number of prefixed helpers are available as a shorthand to <tt>action: "..."</tt>
# in options. Those are:
#
# * <tt>edit_polymorphic_url</tt>, <tt>edit_polymorphic_path</tt>
# * <tt>new_polymorphic_url</tt>, <tt>new_polymorphic_path</tt>
#
# Example usage:
#
# edit_polymorphic_path(@post) # => "/posts/1/edit"
# polymorphic_path(@post, format: :pdf) # => "/posts/1.pdf"
#
# == Usage with mounted engines
#
# If you are using a mounted engine and you need to use a polymorphic_url
# pointing at the engine's routes, pass in the engine's route proxy as the first
# argument to the method. For example:
#
# polymorphic_url([blog, @post]) # calls blog.post_path(@post)
# form_for([blog, @post]) # => "/blog/posts/1"
#
- 1
module PolymorphicRoutes
# Constructs a call to a named RESTful route for the given record and returns the
# resulting URL string. For example:
#
# # calls post_url(post)
# polymorphic_url(post) # => "http://example.com/posts/1"
# polymorphic_url([blog, post]) # => "http://example.com/blogs/1/posts/1"
# polymorphic_url([:admin, blog, post]) # => "http://example.com/admin/blogs/1/posts/1"
# polymorphic_url([user, :blog, post]) # => "http://example.com/users/1/blog/posts/1"
# polymorphic_url(Comment) # => "http://example.com/comments"
#
# ==== Options
#
# * <tt>:action</tt> - Specifies the action prefix for the named route:
# <tt>:new</tt> or <tt>:edit</tt>. Default is no prefix.
# * <tt>:routing_type</tt> - Allowed values are <tt>:path</tt> or <tt>:url</tt>.
# Default is <tt>:url</tt>.
#
# Also includes all the options from <tt>url_for</tt>. These include such
# things as <tt>:anchor</tt> or <tt>:trailing_slash</tt>. Example usage
# is given below:
#
# polymorphic_url([blog, post], anchor: 'my_anchor')
# # => "http://example.com/blogs/1/posts/1#my_anchor"
# polymorphic_url([blog, post], anchor: 'my_anchor', script_name: "/my_app")
# # => "http://example.com/my_app/blogs/1/posts/1#my_anchor"
#
# For all of these options, see the documentation for {url_for}[rdoc-ref:ActionDispatch::Routing::UrlFor].
#
# ==== Functionality
#
# # an Article record
# polymorphic_url(record) # same as article_url(record)
#
# # a Comment record
# polymorphic_url(record) # same as comment_url(record)
#
# # it recognizes new records and maps to the collection
# record = Comment.new
# polymorphic_url(record) # same as comments_url()
#
# # the class of a record will also map to the collection
# polymorphic_url(Comment) # same as comments_url()
#
- 1
def polymorphic_url(record_or_hash_or_array, options = {})
if Hash === record_or_hash_or_array
options = record_or_hash_or_array.merge(options)
record = options.delete :id
return polymorphic_url record, options
end
if mapping = polymorphic_mapping(record_or_hash_or_array)
return mapping.call(self, [record_or_hash_or_array, options], false)
end
opts = options.dup
action = opts.delete :action
type = opts.delete(:routing_type) || :url
HelperMethodBuilder.polymorphic_method self,
record_or_hash_or_array,
action,
type,
opts
end
# Returns the path component of a URL for the given record.
- 1
def polymorphic_path(record_or_hash_or_array, options = {})
if Hash === record_or_hash_or_array
options = record_or_hash_or_array.merge(options)
record = options.delete :id
return polymorphic_path record, options
end
if mapping = polymorphic_mapping(record_or_hash_or_array)
return mapping.call(self, [record_or_hash_or_array, options], true)
end
opts = options.dup
action = opts.delete :action
type = :path
HelperMethodBuilder.polymorphic_method self,
record_or_hash_or_array,
action,
type,
opts
end
- 1
%w(edit new).each do |action|
- 2
module_eval <<-EOT, __FILE__, __LINE__ + 1
# frozen_string_literal: true
def #{action}_polymorphic_url(record_or_hash, options = {})
polymorphic_url_for_action("#{action}", record_or_hash, options)
end
def #{action}_polymorphic_path(record_or_hash, options = {})
polymorphic_path_for_action("#{action}", record_or_hash, options)
end
EOT
end
- 1
private
- 1
def polymorphic_url_for_action(action, record_or_hash, options)
polymorphic_url(record_or_hash, options.merge(action: action))
end
- 1
def polymorphic_path_for_action(action, record_or_hash, options)
polymorphic_path(record_or_hash, options.merge(action: action))
end
- 1
def polymorphic_mapping(record)
if record.respond_to?(:to_model)
_routes.polymorphic_mappings[record.to_model.model_name.name]
else
_routes.polymorphic_mappings[record.class.name]
end
end
- 1
class HelperMethodBuilder # :nodoc:
- 1
CACHE = { path: {}, url: {} }
- 1
def self.get(action, type)
type = type.to_sym
CACHE[type].fetch(action) { build action, type }
end
- 1
def self.url; CACHE[:url][nil]; end
- 1
def self.path; CACHE[:path][nil]; end
- 1
def self.build(action, type)
- 6
prefix = action ? "#{action}_" : ""
- 6
suffix = type
- 6
if action.to_s == "new"
- 2
HelperMethodBuilder.singular prefix, suffix
else
- 4
HelperMethodBuilder.plural prefix, suffix
end
end
- 1
def self.singular(prefix, suffix)
- 2
new(->(name) { name.singular_route_key }, prefix, suffix)
end
- 1
def self.plural(prefix, suffix)
- 4
new(->(name) { name.route_key }, prefix, suffix)
end
- 1
def self.polymorphic_method(recipient, record_or_hash_or_array, action, type, options)
builder = get action, type
case record_or_hash_or_array
when Array
record_or_hash_or_array = record_or_hash_or_array.compact
if record_or_hash_or_array.empty?
raise ArgumentError, "Nil location provided. Can't build URI."
end
if record_or_hash_or_array.first.is_a?(ActionDispatch::Routing::RoutesProxy)
recipient = record_or_hash_or_array.shift
end
method, args = builder.handle_list record_or_hash_or_array
when String, Symbol
method, args = builder.handle_string record_or_hash_or_array
when Class
method, args = builder.handle_class record_or_hash_or_array
when nil
raise ArgumentError, "Nil location provided. Can't build URI."
else
method, args = builder.handle_model record_or_hash_or_array
end
if options.empty?
recipient.send(method, *args)
else
recipient.send(method, *args, options)
end
end
- 1
attr_reader :suffix, :prefix
- 1
def initialize(key_strategy, prefix, suffix)
- 6
@key_strategy = key_strategy
- 6
@prefix = prefix
- 6
@suffix = suffix
end
- 1
def handle_string(record)
[get_method_for_string(record), []]
end
- 1
def handle_string_call(target, str)
target.send get_method_for_string str
end
- 1
def handle_class(klass)
[get_method_for_class(klass), []]
end
- 1
def handle_class_call(target, klass)
target.send get_method_for_class klass
end
- 1
def handle_model(record)
args = []
model = record.to_model
named_route = if model.persisted?
args << model
get_method_for_string model.model_name.singular_route_key
else
get_method_for_class model
end
[named_route, args]
end
- 1
def handle_model_call(target, record)
if mapping = polymorphic_mapping(target, record)
mapping.call(target, [record], suffix == "path")
else
method, args = handle_model(record)
target.send(method, *args)
end
end
- 1
def handle_list(list)
record_list = list.dup
record = record_list.pop
args = []
route = record_list.map { |parent|
case parent
when Symbol, String
parent.to_s
when Class
args << parent
parent.model_name.singular_route_key
else
args << parent.to_model
parent.to_model.model_name.singular_route_key
end
}
route <<
case record
when Symbol, String
record.to_s
when Class
@key_strategy.call record.model_name
else
model = record.to_model
if model.persisted?
args << model
model.model_name.singular_route_key
else
@key_strategy.call model.model_name
end
end
route << suffix
named_route = prefix + route.join("_")
[named_route, args]
end
- 1
private
- 1
def polymorphic_mapping(target, record)
if record.respond_to?(:to_model)
target._routes.polymorphic_mappings[record.to_model.model_name.name]
else
target._routes.polymorphic_mappings[record.class.name]
end
end
- 1
def get_method_for_class(klass)
name = @key_strategy.call klass.model_name
get_method_for_string name
end
- 1
def get_method_for_string(str)
"#{prefix}#{str}_#{suffix}"
end
- 1
[nil, "new", "edit"].each do |action|
- 3
CACHE[:url][action] = build action, "url"
- 3
CACHE[:path][action] = build action, "path"
end
end
end
end
end
# frozen_string_literal: true
- 1
require "action_dispatch/http/request"
- 1
require "active_support/core_ext/uri"
- 1
require "active_support/core_ext/array/extract_options"
- 1
require "rack/utils"
- 1
require "action_controller/metal/exceptions"
- 1
require "action_dispatch/routing/endpoint"
- 1
module ActionDispatch
- 1
module Routing
- 1
class Redirect < Endpoint # :nodoc:
- 1
attr_reader :status, :block
- 1
def initialize(status, block)
- 32
@status = status
- 32
@block = block
end
- 1
def redirect?; true; end
- 1
def call(env)
serve Request.new env
end
- 1
def serve(req)
uri = URI.parse(path(req.path_parameters, req))
unless uri.host
if relative_path?(uri.path)
uri.path = "#{req.script_name}/#{uri.path}"
elsif uri.path.empty?
uri.path = req.script_name.empty? ? "/" : req.script_name
end
end
uri.scheme ||= req.scheme
uri.host ||= req.host
uri.port ||= req.port unless req.standard_port?
req.commit_flash
body = %(<html><body>You are being <a href="#{ERB::Util.unwrapped_html_escape(uri.to_s)}">redirected</a>.</body></html>)
headers = {
"Location" => uri.to_s,
"Content-Type" => "text/html",
"Content-Length" => body.length.to_s
}
[ status, headers, [body] ]
end
- 1
def path(params, request)
block.call params, request
end
- 1
def inspect
"redirect(#{status})"
end
- 1
private
- 1
def relative_path?(path)
path && !path.empty? && path[0] != "/"
end
- 1
def escape(params)
params.transform_values { |v| Rack::Utils.escape(v) }
end
- 1
def escape_fragment(params)
params.transform_values { |v| Journey::Router::Utils.escape_fragment(v) }
end
- 1
def escape_path(params)
params.transform_values { |v| Journey::Router::Utils.escape_path(v) }
end
end
- 1
class PathRedirect < Redirect
- 1
URL_PARTS = /\A([^?]+)?(\?[^#]+)?(#.+)?\z/
- 1
def path(params, request)
if block.match(URL_PARTS)
path = interpolation_required?($1, params) ? $1 % escape_path(params) : $1
query = interpolation_required?($2, params) ? $2 % escape(params) : $2
fragment = interpolation_required?($3, params) ? $3 % escape_fragment(params) : $3
"#{path}#{query}#{fragment}"
else
interpolation_required?(block, params) ? block % escape(params) : block
end
end
- 1
def inspect
"redirect(#{status}, #{block})"
end
- 1
private
- 1
def interpolation_required?(string, params)
!params.empty? && string && string.match(/%\{\w*\}/)
end
end
- 1
class OptionRedirect < Redirect # :nodoc:
- 1
alias :options :block
- 1
def path(params, request)
url_options = {
protocol: request.protocol,
host: request.host,
port: request.optional_port,
path: request.path,
params: request.query_parameters
}.merge! options
if !params.empty? && url_options[:path].match(/%\{\w*\}/)
url_options[:path] = (url_options[:path] % escape_path(params))
end
unless options[:host] || options[:domain]
if relative_path?(url_options[:path])
url_options[:path] = "/#{url_options[:path]}"
url_options[:script_name] = request.script_name
elsif url_options[:path].empty?
url_options[:path] = request.script_name.empty? ? "/" : ""
url_options[:script_name] = request.script_name
end
end
ActionDispatch::Http::URL.url_for url_options
end
- 1
def inspect
"redirect(#{status}, #{options.map { |k, v| "#{k}: #{v}" }.join(', ')})"
end
end
- 1
module Redirection
# Redirect any path to another path:
#
# get "/stories" => redirect("/posts")
#
# This will redirect the user, while ignoring certain parts of the request, including query string, etc.
# <tt>/stories</tt>, <tt>/stories?foo=bar</tt>, etc all redirect to <tt>/posts</tt>.
#
# You can also use interpolation in the supplied redirect argument:
#
# get 'docs/:article', to: redirect('/wiki/%{article}')
#
# Note that if you return a path without a leading slash then the URL is prefixed with the
# current SCRIPT_NAME environment variable. This is typically '/' but may be different in
# a mounted engine or where the application is deployed to a subdirectory of a website.
#
# Alternatively you can use one of the other syntaxes:
#
# The block version of redirect allows for the easy encapsulation of any logic associated with
# the redirect in question. Either the params and request are supplied as arguments, or just
# params, depending of how many arguments your block accepts. A string is required as a
# return value.
#
# get 'jokes/:number', to: redirect { |params, request|
# path = (params[:number].to_i.even? ? "wheres-the-beef" : "i-love-lamp")
# "http://#{request.host_with_port}/#{path}"
# }
#
# Note that the +do end+ syntax for the redirect block wouldn't work, as Ruby would pass
# the block to +get+ instead of +redirect+. Use <tt>{ ... }</tt> instead.
#
# The options version of redirect allows you to supply only the parts of the URL which need
# to change, it also supports interpolation of the path similar to the first example.
#
# get 'stores/:name', to: redirect(subdomain: 'stores', path: '/%{name}')
# get 'stores/:name(*all)', to: redirect(subdomain: 'stores', path: '/%{name}%{all}')
# get '/stories', to: redirect(path: '/posts')
#
# This will redirect the user, while changing only the specified parts of the request,
# for example the +path+ option in the last example.
# <tt>/stories</tt>, <tt>/stories?foo=bar</tt>, redirect to <tt>/posts</tt> and <tt>/posts?foo=bar</tt> respectively.
#
# Finally, an object which responds to call can be supplied to redirect, allowing you to reuse
# common redirect routes. The call method must accept two arguments, params and request, and return
# a string.
#
# get 'accounts/:name' => redirect(SubdomainRedirector.new('api'))
#
- 1
def redirect(*args, &block)
- 32
options = args.extract_options!
- 32
status = options.delete(:status) || 301
- 32
path = args.shift
- 32
return OptionRedirect.new(status, options) if options.any?
- 23
return PathRedirect.new(status, path) if String === path
- 9
block = path if path.respond_to? :call
- 9
raise ArgumentError, "redirection argument not supported" unless block
- 9
Redirect.new status, block
end
end
end
end
# frozen_string_literal: true
- 1
require "action_dispatch/journey"
- 1
require "active_support/core_ext/object/to_query"
- 1
require "active_support/core_ext/module/redefine_method"
- 1
require "active_support/core_ext/module/remove_method"
- 1
require "active_support/core_ext/array/extract_options"
- 1
require "action_controller/metal/exceptions"
- 1
require "action_dispatch/http/request"
- 1
require "action_dispatch/routing/endpoint"
- 1
module ActionDispatch
- 1
module Routing
# :stopdoc:
- 1
class RouteSet
# Since the router holds references to many parts of the system
# like engines, controllers and the application itself, inspecting
# the route set can actually be really slow, therefore we default
# alias inspect to to_s.
- 1
alias inspect to_s
- 1
class Dispatcher < Routing::Endpoint
- 1
def initialize(raise_on_name_error)
- 239
@raise_on_name_error = raise_on_name_error
end
- 1
def dispatcher?; true; end
- 1
def serve(req)
params = req.path_parameters
controller = controller req
res = controller.make_response! req
dispatch(controller, params[:action], req, res)
rescue ActionController::RoutingError
if @raise_on_name_error
raise
else
[404, { "X-Cascade" => "pass" }, []]
end
end
- 1
private
- 1
def controller(req)
req.controller_class
rescue NameError => e
raise ActionController::RoutingError, e.message, e.backtrace
end
- 1
def dispatch(controller, action, req, res)
controller.dispatch(action, req, res)
end
end
- 1
class StaticDispatcher < Dispatcher
- 1
def initialize(controller_class)
super(false)
@controller_class = controller_class
end
- 1
private
- 1
def controller(_); @controller_class; end
end
# A NamedRouteCollection instance is a collection of named routes, and also
# maintains an anonymous module that can be used to install helpers for the
# named routes.
- 1
class NamedRouteCollection
- 1
include Enumerable
- 1
attr_reader :routes, :url_helpers_module, :path_helpers_module
- 1
private :routes
- 1
def initialize
- 56
@routes = {}
- 56
@path_helpers = Set.new
- 56
@url_helpers = Set.new
- 56
@url_helpers_module = Module.new
- 56
@path_helpers_module = Module.new
end
- 1
def route_defined?(name)
key = name.to_sym
@path_helpers.include?(key) || @url_helpers.include?(key)
end
- 1
def helper_names
@path_helpers.map(&:to_s) + @url_helpers.map(&:to_s)
end
- 1
def clear!
- 54
@path_helpers.each do |helper|
@path_helpers_module.remove_method helper
end
- 54
@url_helpers.each do |helper|
@url_helpers_module.remove_method helper
end
- 54
@routes.clear
- 54
@path_helpers.clear
- 54
@url_helpers.clear
end
- 1
def add(name, route)
- 203
key = name.to_sym
- 203
path_name = :"#{name}_path"
- 203
url_name = :"#{name}_url"
- 203
if routes.key? key
@path_helpers_module.undef_method path_name
@url_helpers_module.undef_method url_name
end
- 203
routes[key] = route
- 203
helper = UrlHelper.create(route, route.defaults, name)
- 203
define_url_helper @path_helpers_module, path_name, helper, PATH
- 203
define_url_helper @url_helpers_module, url_name, helper, UNKNOWN
- 203
@path_helpers << path_name
- 203
@url_helpers << url_name
end
- 1
def get(name)
- 206
routes[name.to_sym]
end
- 1
def key?(name)
- 233
return unless name
- 233
routes.key? name.to_sym
end
- 1
alias []= add
- 1
alias [] get
- 1
alias clear clear!
- 1
def each
routes.each { |name, route| yield name, route }
self
end
- 1
def names
routes.keys
end
- 1
def length
routes.length
end
# Given a +name+, defines name_path and name_url helpers.
# Used by 'direct', 'resolve', and 'polymorphic' route helpers.
- 1
def add_url_helper(name, defaults, &block)
- 12
helper = CustomUrlHelper.new(name, defaults, &block)
- 12
path_name = :"#{name}_path"
- 12
url_name = :"#{name}_url"
- 12
@path_helpers_module.module_eval do
- 12
redefine_method(path_name) do |*args|
helper.call(self, args, true)
end
end
- 12
@url_helpers_module.module_eval do
- 12
redefine_method(url_name) do |*args|
helper.call(self, args, false)
end
end
- 12
@path_helpers << path_name
- 12
@url_helpers << url_name
- 12
self
end
- 1
class UrlHelper
- 1
def self.create(route, options, route_name)
- 203
if optimize_helper?(route)
- 192
OptimizedUrlHelper.new(route, options, route_name)
else
- 11
new(route, options, route_name)
end
end
- 1
def self.optimize_helper?(route)
- 203
route.path.requirements.empty? && !route.glob?
end
- 1
attr_reader :route_name
- 1
class OptimizedUrlHelper < UrlHelper
- 1
attr_reader :arg_size
- 1
def initialize(route, options, route_name)
- 192
super
- 192
@required_parts = @route.required_parts
- 192
@arg_size = @required_parts.size
end
- 1
def call(t, method_name, args, inner_options, url_strategy)
if args.size == arg_size && !inner_options && optimize_routes_generation?(t)
options = t.url_options.merge @options
options[:path] = optimized_helper(args)
original_script_name = options.delete(:original_script_name)
script_name = t._routes.find_script_name(options)
if original_script_name
script_name = original_script_name + script_name
end
options[:script_name] = script_name
url_strategy.call options
else
super
end
end
- 1
private
- 1
def optimized_helper(args)
params = parameterize_args(args) do
raise_generation_error(args)
end
@route.format params
end
- 1
def optimize_routes_generation?(t)
t.send(:optimize_routes_generation?)
end
- 1
def parameterize_args(args)
params = {}
@arg_size.times { |i|
key = @required_parts[i]
value = args[i].to_param
yield key if value.nil? || value.empty?
params[key] = value
}
params
end
- 1
def raise_generation_error(args)
missing_keys = []
params = parameterize_args(args) { |missing_key|
missing_keys << missing_key
}
constraints = Hash[@route.requirements.merge(params).sort_by { |k, v| k.to_s }]
message = +"No route matches #{constraints.inspect}"
message << ", missing required keys: #{missing_keys.sort.inspect}"
raise ActionController::UrlGenerationError, message
end
end
- 1
def initialize(route, options, route_name)
- 203
@options = options
- 203
@segment_keys = route.segment_keys.uniq
- 203
@route = route
- 203
@route_name = route_name
end
- 1
def call(t, method_name, args, inner_options, url_strategy)
controller_options = t.url_options
options = controller_options.merge @options
hash = handle_positional_args(controller_options,
inner_options || {},
args,
options,
@segment_keys)
t._routes.url_for(hash, route_name, url_strategy, method_name)
end
- 1
def handle_positional_args(controller_options, inner_options, args, result, path_params)
if args.size > 0
# take format into account
if path_params.include?(:format)
path_params_size = path_params.size - 1
else
path_params_size = path_params.size
end
if args.size < path_params_size
path_params -= controller_options.keys
path_params -= result.keys
else
path_params = path_params.dup
end
inner_options.each_key do |key|
path_params.delete(key)
end
args.each_with_index do |arg, index|
param = path_params[index]
result[param] = arg if param
end
end
result.merge!(inner_options)
end
end
- 1
private
# Create a URL helper allowing ordered parameters to be associated
# with corresponding dynamic segments, so you can do:
#
# foo_url(bar, baz, bang)
#
# Instead of:
#
# foo_url(bar: bar, baz: baz, bang: bang)
#
# Also allow options hash, so you can do:
#
# foo_url(bar, baz, bang, sort_by: 'baz')
#
- 1
def define_url_helper(mod, name, helper, url_strategy)
- 406
mod.define_method(name) do |*args|
last = args.last
options = \
case last
when Hash
args.pop
when ActionController::Parameters
args.pop.to_h
end
helper.call(self, name, args, options, url_strategy)
end
end
end
# strategy for building URLs to send to the client
- 1
PATH = ->(options) { ActionDispatch::Http::URL.path_for(options) }
- 1
UNKNOWN = ->(options) { ActionDispatch::Http::URL.url_for(options) }
- 1
attr_accessor :formatter, :set, :named_routes, :default_scope, :router
- 1
attr_accessor :disable_clear_and_finalize, :resources_path_names
- 1
attr_accessor :default_url_options, :draw_paths
- 1
attr_reader :env_key, :polymorphic_mappings
- 1
alias :routes :set
- 1
def self.default_resources_path_names
- 56
{ new: "new", edit: "edit" }
end
- 1
def self.new_with_config(config)
- 4
route_set_config = DEFAULT_CONFIG
# engines apparently don't have this set
- 4
if config.respond_to? :relative_url_root
route_set_config.relative_url_root = config.relative_url_root
end
- 4
if config.respond_to? :api_only
route_set_config.api_only = config.api_only
end
- 4
new route_set_config
end
- 1
Config = Struct.new :relative_url_root, :api_only
- 1
DEFAULT_CONFIG = Config.new(nil, false)
- 1
def initialize(config = DEFAULT_CONFIG)
- 56
self.named_routes = NamedRouteCollection.new
- 56
self.resources_path_names = self.class.default_resources_path_names
- 56
self.default_url_options = {}
- 56
self.draw_paths = []
- 56
@config = config
- 56
@append = []
- 56
@prepend = []
- 56
@disable_clear_and_finalize = false
- 56
@finalized = false
- 56
@env_key = "ROUTES_#{object_id}_SCRIPT_NAME"
- 56
@set = Journey::Routes.new
- 56
@router = Journey::Router.new @set
- 56
@formatter = Journey::Formatter.new self
- 56
@polymorphic_mappings = {}
end
- 1
def eager_load!
router.eager_load!
routes.each(&:eager_load!)
nil
end
- 1
def relative_url_root
@config.relative_url_root
end
- 1
def api_only?
- 21
@config.api_only
end
- 1
def request_class
- 324
ActionDispatch::Request
end
- 1
def make_request(env)
request_class.new env
end
- 1
private :make_request
- 1
def draw(&block)
- 54
clear! unless @disable_clear_and_finalize
- 54
eval_block(block)
- 54
finalize! unless @disable_clear_and_finalize
nil
end
- 1
def append(&block)
@append << block
end
- 1
def prepend(&block)
@prepend << block
end
- 1
def eval_block(block)
- 54
mapper = Mapper.new(self)
- 54
if default_scope
- 2
mapper.with_default_scope(default_scope, &block)
else
- 52
mapper.instance_exec(&block)
end
end
- 1
private :eval_block
- 1
def finalize!
- 54
return if @finalized
- 54
@append.each { |blk| eval_block(blk) }
- 54
@finalized = true
end
- 1
def clear!
- 54
@finalized = false
- 54
named_routes.clear
- 54
set.clear
- 54
formatter.clear
- 54
@polymorphic_mappings.clear
- 54
@prepend.each { |blk| eval_block(blk) }
end
- 1
module MountedHelpers
- 1
extend ActiveSupport::Concern
- 1
include UrlFor
end
# Contains all the mounted helpers across different
# engines and the `main_app` helper for the application.
# You can include this in your classes if you want to
# access routes for other engines.
- 1
def mounted_helpers
- 5
MountedHelpers
end
- 1
def define_mounted_helper(name, script_namer = nil)
- 4
return if MountedHelpers.method_defined?(name)
- 4
routes = self
- 4
helpers = routes.url_helpers
- 4
MountedHelpers.class_eval do
- 4
define_method "_#{name}" do
RoutesProxy.new(routes, _routes_context, helpers, script_namer)
end
end
- 4
MountedHelpers.class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
def #{name}
@_#{name} ||= _#{name}
end
RUBY
end
- 1
def url_helpers(supports_path = true)
- 325
if supports_path
- 325
@url_helpers_with_paths ||= generate_url_helpers(true)
else
@url_helpers_without_paths ||= generate_url_helpers(false)
end
end
- 1
def generate_url_helpers(supports_path)
- 35
routes = self
- 35
Module.new do
- 35
extend ActiveSupport::Concern
- 35
include UrlFor
# Define url_for in the singleton level so one can do:
# Rails.application.routes.url_helpers.url_for(args)
- 35
proxy_class = Class.new do
- 35
include UrlFor
- 35
include routes.named_routes.path_helpers_module
- 35
include routes.named_routes.url_helpers_module
- 35
attr_reader :_routes
- 35
def initialize(routes)
- 35
@_routes = routes
end
- 35
def optimize_routes_generation?
@_routes.optimize_routes_generation?
end
end
- 35
@_proxy = proxy_class.new(routes)
- 35
class << self
- 35
def url_for(options)
@_proxy.url_for(options)
end
- 35
def full_url_for(options)
@_proxy.full_url_for(options)
end
- 35
def route_for(name, *args)
@_proxy.route_for(name, *args)
end
- 35
def optimize_routes_generation?
@_proxy.optimize_routes_generation?
end
- 35
def polymorphic_url(record_or_hash_or_array, options = {})
@_proxy.polymorphic_url(record_or_hash_or_array, options)
end
- 35
def polymorphic_path(record_or_hash_or_array, options = {})
@_proxy.polymorphic_path(record_or_hash_or_array, options)
end
- 35
def _routes; @_proxy._routes; end
- 35
def url_options; {}; end
end
- 35
url_helpers = routes.named_routes.url_helpers_module
# Make named_routes available in the module singleton
# as well, so one can do:
# Rails.application.routes.url_helpers.posts_path
- 35
extend url_helpers
# Any class that includes this module will get all
# named routes...
- 35
include url_helpers
- 35
if supports_path
- 35
path_helpers = routes.named_routes.path_helpers_module
- 35
include path_helpers
- 35
extend path_helpers
end
# plus a singleton class method called _routes ...
- 35
included do
- 228
redefine_singleton_method(:_routes) { routes }
end
# And an instance method _routes. Note that
# UrlFor (included in this module) add extra
# conveniences for working with @_routes.
- 35
define_method(:_routes) { @_routes || routes }
- 35
define_method(:_generate_paths_by_default) do
supports_path
end
- 35
private :_generate_paths_by_default
end
end
- 1
def empty?
routes.empty?
end
- 1
def add_route(mapping, name)
- 326
raise ArgumentError, "Invalid route name: '#{name}'" unless name.blank? || name.to_s.match(/^[_a-z]\w*$/i)
- 326
if name && named_routes[name]
raise ArgumentError, "Invalid route name, already in use: '#{name}' \n" \
"You may have defined two routes with the same name using the `:as` option, or " \
"you may be overriding a route already defined by a resource with the same naming. " \
"For the latter, you can restrict the routes created with `resources` as explained here: \n" \
"https://guides.rubyonrails.org/routing.html#restricting-the-routes-created"
end
- 326
route = @set.add_route(name, mapping)
- 326
named_routes[name] = route if name
- 326
if route.segment_keys.include?(:controller)
- 5
ActiveSupport::Deprecation.warn(<<-MSG.squish)
Using a dynamic :controller segment in a route is deprecated and
will be removed in Rails 6.1.
MSG
end
- 326
if route.segment_keys.include?(:action)
- 6
ActiveSupport::Deprecation.warn(<<-MSG.squish)
Using a dynamic :action segment in a route is deprecated and
will be removed in Rails 6.1.
MSG
end
- 326
route
end
- 1
def add_polymorphic_mapping(klass, options, &block)
- 8
@polymorphic_mappings[klass] = CustomUrlHelper.new(klass, options, &block)
end
- 1
def add_url_helper(name, options, &block)
- 12
named_routes.add_url_helper(name, options, &block)
end
- 1
class CustomUrlHelper
- 1
attr_reader :name, :defaults, :block
- 1
def initialize(name, defaults, &block)
- 20
@name = name
- 20
@defaults = defaults
- 20
@block = block
end
- 1
def call(t, args, only_path = false)
options = args.extract_options!
url = t.full_url_for(eval_block(t, args, options))
if only_path
"/" + url.partition(%r{(?<!/)/(?!/)}).last
else
url
end
end
- 1
private
- 1
def eval_block(t, args, options)
t.instance_exec(*args, merge_defaults(options), &block)
end
- 1
def merge_defaults(options)
defaults ? defaults.merge(options) : options
end
end
- 1
class Generator
- 1
attr_reader :options, :recall, :set, :named_route
- 1
def initialize(named_route, options, recall, set)
@named_route = named_route
@options = options
@recall = recall
@set = set
normalize_options!
normalize_controller_action_id!
use_relative_controller!
normalize_controller!
end
- 1
def controller
@options[:controller]
end
- 1
def current_controller
@recall[:controller]
end
- 1
def use_recall_for(key)
if @recall[key] && (!@options.key?(key) || @options[key] == @recall[key])
if !named_route_exists? || segment_keys.include?(key)
@options[key] = @recall[key]
end
end
end
- 1
def normalize_options!
# If an explicit :controller was given, always make :action explicit
# too, so that action expiry works as expected for things like
#
# generate({controller: 'content'}, {controller: 'content', action: 'show'})
#
# (the above is from the unit tests). In the above case, because the
# controller was explicitly given, but no action, the action is implied to
# be "index", not the recalled action of "show".
if options[:controller]
options[:action] ||= "index"
options[:controller] = options[:controller].to_s
end
if options.key?(:action)
options[:action] = (options[:action] || "index").to_s
end
end
# This pulls :controller, :action, and :id out of the recall.
# The recall key is only used if there is no key in the options
# or if the key in the options is identical. If any of
# :controller, :action or :id is not found, don't pull any
# more keys from the recall.
- 1
def normalize_controller_action_id!
use_recall_for(:controller) || return
use_recall_for(:action) || return
use_recall_for(:id)
end
# if the current controller is "foo/bar/baz" and controller: "baz/bat"
# is specified, the controller becomes "foo/baz/bat"
- 1
def use_relative_controller!
if !named_route && different_controller? && !controller.start_with?("/")
old_parts = current_controller.split("/")
size = controller.count("/") + 1
parts = old_parts[0...-size] << controller
@options[:controller] = parts.join("/")
end
end
# Remove leading slashes from controllers
- 1
def normalize_controller!
if controller
if controller.start_with?("/")
@options[:controller] = controller[1..-1]
else
@options[:controller] = controller
end
end
end
# Generates a path from routes, returns a RouteWithParams or MissingRoute.
# MissingRoute will raise ActionController::UrlGenerationError.
- 1
def generate
@set.formatter.generate(named_route, options, recall)
end
- 1
def different_controller?
return false unless current_controller
controller.to_param != current_controller.to_param
end
- 1
private
- 1
def named_route_exists?
named_route && set.named_routes[named_route]
end
- 1
def segment_keys
set.named_routes[named_route].segment_keys
end
end
# Generate the path indicated by the arguments, and return an array of
# the keys that were not used to generate it.
- 1
def extra_keys(options, recall = {})
generate_extras(options, recall).last
end
- 1
def generate_extras(options, recall = {})
if recall
options = options.merge(_recall: recall)
end
route_name = options.delete :use_route
path = path_for(options, route_name, [])
uri = URI.parse(path)
params = Rack::Utils.parse_nested_query(uri.query).symbolize_keys
[uri.path, params.keys]
end
- 1
def generate(route_name, options, recall = {}, method_name = nil)
Generator.new(route_name, options, recall, self).generate
end
- 1
private :generate
- 1
RESERVED_OPTIONS = [:host, :protocol, :port, :subdomain, :domain, :tld_length,
:trailing_slash, :anchor, :params, :only_path, :script_name,
:original_script_name, :relative_url_root]
- 1
def optimize_routes_generation?
default_url_options.empty?
end
- 1
def find_script_name(options)
options.delete(:script_name) || find_relative_url_root(options) || ""
end
- 1
def find_relative_url_root(options)
options.delete(:relative_url_root) || relative_url_root
end
- 1
def path_for(options, route_name = nil, reserved = RESERVED_OPTIONS)
url_for(options, route_name, PATH, nil, reserved)
end
# The +options+ argument must be a hash whose keys are *symbols*.
- 1
def url_for(options, route_name = nil, url_strategy = UNKNOWN, method_name = nil, reserved = RESERVED_OPTIONS)
options = default_url_options.merge options
user = password = nil
if options[:user] && options[:password]
user = options.delete :user
password = options.delete :password
end
recall = options.delete(:_recall) { {} }
original_script_name = options.delete(:original_script_name)
script_name = find_script_name options
if original_script_name
script_name = original_script_name + script_name
end
path_options = options.dup
reserved.each { |ro| path_options.delete ro }
route_with_params = generate(route_name, path_options, recall)
path = route_with_params.path(method_name)
params = route_with_params.params
if options.key? :params
params.merge! options[:params]
end
options[:path] = path
options[:script_name] = script_name
options[:params] = params
options[:user] = user
options[:password] = password
url_strategy.call options
end
- 1
def call(env)
req = make_request(env)
req.path_info = Journey::Router::Utils.normalize_path(req.path_info)
@router.serve(req)
end
- 1
def recognize_path(path, environment = {})
method = (environment[:method] || "GET").to_s.upcase
path = Journey::Router::Utils.normalize_path(path) unless %r{://}.match?(path)
extras = environment[:extras] || {}
begin
env = Rack::MockRequest.env_for(path, method: method)
rescue URI::InvalidURIError => e
raise ActionController::RoutingError, e.message
end
req = make_request(env)
recognize_path_with_request(req, path, extras)
end
- 1
def recognize_path_with_request(req, path, extras, raise_on_missing: true)
@router.recognize(req) do |route, params|
params.merge!(extras)
params.each do |key, value|
if value.is_a?(String)
value = value.dup.force_encoding(Encoding::BINARY)
params[key] = URI::DEFAULT_PARSER.unescape(value)
end
end
req.path_parameters = params
app = route.app
if app.matches?(req) && app.dispatcher?
begin
req.controller_class
rescue NameError
raise ActionController::RoutingError, "A route matches #{path.inspect}, but references missing controller: #{params[:controller].camelize}Controller"
end
return req.path_parameters
elsif app.matches?(req) && app.engine?
path_parameters = app.rack_app.routes.recognize_path_with_request(req, path, extras, raise_on_missing: false)
return path_parameters if path_parameters
end
end
if raise_on_missing
raise ActionController::RoutingError, "No route matches #{path.inspect}"
end
end
end
# :startdoc:
end
end
# frozen_string_literal: true
require "active_support/core_ext/array/extract_options"
module ActionDispatch
module Routing
class RoutesProxy #:nodoc:
include ActionDispatch::Routing::UrlFor
attr_accessor :scope, :routes
alias :_routes :routes
def initialize(routes, scope, helpers, script_namer = nil)
@routes, @scope = routes, scope
@helpers = helpers
@script_namer = script_namer
end
def url_options
scope.send(:_with_routes, routes) do
scope.url_options
end
end
private
def respond_to_missing?(method, _)
super || @helpers.respond_to?(method)
end
def method_missing(method, *args)
if @helpers.respond_to?(method)
self.class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{method}(*args)
options = args.extract_options!
options = url_options.merge((options || {}).symbolize_keys)
if @script_namer
options[:script_name] = merge_script_names(
options[:script_name],
@script_namer.call(options)
)
end
args << options
@helpers.#{method}(*args)
end
RUBY
public_send(method, *args)
else
super
end
end
# Keeps the part of the script name provided by the global
# context via ENV["SCRIPT_NAME"], which `mount` doesn't know
# about since it depends on the specific request, but use our
# script name resolver for the mount point dependent part.
def merge_script_names(previous_script_name, new_script_name)
return new_script_name unless previous_script_name
resolved_parts = new_script_name.count("/")
previous_parts = previous_script_name.count("/")
context_parts = previous_parts - resolved_parts + 1
(previous_script_name.split("/").slice(0, context_parts).join("/")) + new_script_name
end
end
end
end
# frozen_string_literal: true
- 1
module ActionDispatch
- 1
module Routing
# In <tt>config/routes.rb</tt> you define URL-to-controller mappings, but the reverse
# is also possible: a URL can be generated from one of your routing definitions.
# URL generation functionality is centralized in this module.
#
# See ActionDispatch::Routing for general information about routing and routes.rb.
#
# <b>Tip:</b> If you need to generate URLs from your models or some other place,
# then ActionController::UrlFor is what you're looking for. Read on for
# an introduction. In general, this module should not be included on its own,
# as it is usually included by url_helpers (as in Rails.application.routes.url_helpers).
#
# == URL generation from parameters
#
# As you may know, some functions, such as ActionController::Base#url_for
# and ActionView::Helpers::UrlHelper#link_to, can generate URLs given a set
# of parameters. For example, you've probably had the chance to write code
# like this in one of your views:
#
# <%= link_to('Click here', controller: 'users',
# action: 'new', message: 'Welcome!') %>
# # => <a href="/users/new?message=Welcome%21">Click here</a>
#
# link_to, and all other functions that require URL generation functionality,
# actually use ActionController::UrlFor under the hood. And in particular,
# they use the ActionController::UrlFor#url_for method. One can generate
# the same path as the above example by using the following code:
#
# include UrlFor
# url_for(controller: 'users',
# action: 'new',
# message: 'Welcome!',
# only_path: true)
# # => "/users/new?message=Welcome%21"
#
# Notice the <tt>only_path: true</tt> part. This is because UrlFor has no
# information about the website hostname that your Rails app is serving. So if you
# want to include the hostname as well, then you must also pass the <tt>:host</tt>
# argument:
#
# include UrlFor
# url_for(controller: 'users',
# action: 'new',
# message: 'Welcome!',
# host: 'www.example.com')
# # => "http://www.example.com/users/new?message=Welcome%21"
#
# By default, all controllers and views have access to a special version of url_for,
# that already knows what the current hostname is. So if you use url_for in your
# controllers or your views, then you don't need to explicitly pass the <tt>:host</tt>
# argument.
#
# For convenience reasons, mailers provide a shortcut for ActionController::UrlFor#url_for.
# So within mailers, you only have to type +url_for+ instead of 'ActionController::UrlFor#url_for'
# in full. However, mailers don't have hostname information, and you still have to provide
# the +:host+ argument or set the default host that will be used in all mailers using the
# configuration option +config.action_mailer.default_url_options+. For more information on
# url_for in mailers read the ActionMailer#Base documentation.
#
#
# == URL generation for named routes
#
# UrlFor also allows one to access methods that have been auto-generated from
# named routes. For example, suppose that you have a 'users' resource in your
# <tt>config/routes.rb</tt>:
#
# resources :users
#
# This generates, among other things, the method <tt>users_path</tt>. By default,
# this method is accessible from your controllers, views and mailers. If you need
# to access this auto-generated method from other places (such as a model), then
# you can do that by including Rails.application.routes.url_helpers in your class:
#
# class User < ActiveRecord::Base
# include Rails.application.routes.url_helpers
#
# def base_uri
# user_path(self)
# end
# end
#
# User.find(1).base_uri # => "/users/1"
#
- 1
module UrlFor
- 1
extend ActiveSupport::Concern
- 1
include PolymorphicRoutes
- 1
included do
- 42
unless method_defined?(:default_url_options)
# Including in a class uses an inheritable hash. Modules get a plain hash.
- 41
if respond_to?(:class_attribute)
- 41
class_attribute :default_url_options
else
mattr_writer :default_url_options
end
- 41
self.default_url_options = {}
end
- 42
include(*_url_for_modules) if respond_to?(:_url_for_modules)
end
- 1
def initialize(*)
@_routes = nil
super
end
- 1
ruby2_keywords(:initialize) if respond_to?(:ruby2_keywords, true)
# Hook overridden in controller to add request information
# with +default_url_options+. Application logic should not
# go into url_options.
- 1
def url_options
default_url_options
end
# Generate a URL based on the options provided, default_url_options and the
# routes defined in routes.rb. The following options are supported:
#
# * <tt>:only_path</tt> - If true, the relative URL is returned. Defaults to +false+.
# * <tt>:protocol</tt> - The protocol to connect to. Defaults to 'http'.
# * <tt>:host</tt> - Specifies the host the link should be targeted at.
# If <tt>:only_path</tt> is false, this option must be
# provided either explicitly, or via +default_url_options+.
# * <tt>:subdomain</tt> - Specifies the subdomain of the link, using the +tld_length+
# to split the subdomain from the host.
# If false, removes all subdomains from the host part of the link.
# * <tt>:domain</tt> - Specifies the domain of the link, using the +tld_length+
# to split the domain from the host.
# * <tt>:tld_length</tt> - Number of labels the TLD id composed of, only used if
# <tt>:subdomain</tt> or <tt>:domain</tt> are supplied. Defaults to
# <tt>ActionDispatch::Http::URL.tld_length</tt>, which in turn defaults to 1.
# * <tt>:port</tt> - Optionally specify the port to connect to.
# * <tt>:anchor</tt> - An anchor name to be appended to the path.
# * <tt>:params</tt> - The query parameters to be appended to the path.
# * <tt>:trailing_slash</tt> - If true, adds a trailing slash, as in "/archive/2009/"
# * <tt>:script_name</tt> - Specifies application path relative to domain root. If provided, prepends application path.
#
# Any other key (<tt>:controller</tt>, <tt>:action</tt>, etc.) given to
# +url_for+ is forwarded to the Routes module.
#
# url_for controller: 'tasks', action: 'testing', host: 'somehost.org', port: '8080'
# # => 'http://somehost.org:8080/tasks/testing'
# url_for controller: 'tasks', action: 'testing', host: 'somehost.org', anchor: 'ok', only_path: true
# # => '/tasks/testing#ok'
# url_for controller: 'tasks', action: 'testing', trailing_slash: true
# # => 'http://somehost.org/tasks/testing/'
# url_for controller: 'tasks', action: 'testing', host: 'somehost.org', number: '33'
# # => 'http://somehost.org/tasks/testing?number=33'
# url_for controller: 'tasks', action: 'testing', host: 'somehost.org', script_name: "/myapp"
# # => 'http://somehost.org/myapp/tasks/testing'
# url_for controller: 'tasks', action: 'testing', host: 'somehost.org', script_name: "/myapp", only_path: true
# # => '/myapp/tasks/testing'
#
# Missing routes keys may be filled in from the current request's parameters
# (e.g. +:controller+, +:action+, +:id+ and any other parameters that are
# placed in the path). Given that the current action has been reached
# through <tt>GET /users/1</tt>:
#
# url_for(only_path: true) # => '/users/1'
# url_for(only_path: true, action: 'edit') # => '/users/1/edit'
# url_for(only_path: true, action: 'edit', id: 2) # => '/users/2/edit'
#
# Notice that no +:id+ parameter was provided to the first +url_for+ call
# and the helper used the one from the route's path. Any path parameter
# implicitly used by +url_for+ can always be overwritten like shown on the
# last +url_for+ calls.
- 1
def url_for(options = nil)
full_url_for(options)
end
- 1
def full_url_for(options = nil) # :nodoc:
case options
when nil
_routes.url_for(url_options.symbolize_keys)
when Hash, ActionController::Parameters
route_name = options.delete :use_route
merged_url_options = options.to_h.symbolize_keys.reverse_merge!(url_options)
_routes.url_for(merged_url_options, route_name)
when String
options
when Symbol
HelperMethodBuilder.url.handle_string_call self, options
when Array
components = options.dup
polymorphic_url(components, components.extract_options!)
when Class
HelperMethodBuilder.url.handle_class_call self, options
else
HelperMethodBuilder.url.handle_model_call self, options
end
end
# Allows calling direct or regular named route.
#
# resources :buckets
#
# direct :recordable do |recording|
# route_for(:bucket, recording.bucket)
# end
#
# direct :threadable do |threadable|
# route_for(:recordable, threadable.parent)
# end
#
# This maintains the context of the original caller on
# whether to return a path or full URL, e.g:
#
# threadable_path(threadable) # => "/buckets/1"
# threadable_url(threadable) # => "http://example.com/buckets/1"
#
- 1
def route_for(name, *args)
public_send(:"#{name}_url", *args)
end
- 1
protected
- 1
def optimize_routes_generation?
_routes.optimize_routes_generation? && default_url_options.empty?
end
- 1
private
- 1
def _with_routes(routes) # :doc:
old_routes, @_routes = @_routes, routes
yield
ensure
@_routes = old_routes
end
- 1
def _routes_context # :doc:
self
end
end
end
end
# frozen_string_literal: true
- 1
gem "capybara", ">= 3.26"
- 1
require "capybara/dsl"
- 1
require "capybara/minitest"
- 1
require "action_controller"
- 1
require "action_dispatch/system_testing/driver"
- 1
require "action_dispatch/system_testing/browser"
- 1
require "action_dispatch/system_testing/server"
- 1
require "action_dispatch/system_testing/test_helpers/screenshot_helper"
- 1
require "action_dispatch/system_testing/test_helpers/setup_and_teardown"
- 1
module ActionDispatch
# = System Testing
#
# System tests let you test applications in the browser. Because system
# tests use a real browser experience, you can test all of your JavaScript
# easily from your test suite.
#
# To create a system test in your application, extend your test class
# from <tt>ApplicationSystemTestCase</tt>. System tests use Capybara as a
# base and allow you to configure the settings through your
# <tt>application_system_test_case.rb</tt> file that is generated with a new
# application or scaffold.
#
# Here is an example system test:
#
# require "application_system_test_case"
#
# class Users::CreateTest < ApplicationSystemTestCase
# test "adding a new user" do
# visit users_path
# click_on 'New User'
#
# fill_in 'Name', with: 'Arya'
# click_on 'Create User'
#
# assert_text 'Arya'
# end
# end
#
# When generating an application or scaffold, an +application_system_test_case.rb+
# file will also be generated containing the base class for system testing.
# This is where you can change the driver, add Capybara settings, and other
# configuration for your system tests.
#
# require "test_helper"
#
# class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
# driven_by :selenium, using: :chrome, screen_size: [1400, 1400]
# end
#
# By default, <tt>ActionDispatch::SystemTestCase</tt> is driven by the
# Selenium driver, with the Chrome browser, and a browser size of 1400x1400.
#
# Changing the driver configuration options is easy. Let's say you want to use
# the Firefox browser instead of Chrome. In your +application_system_test_case.rb+
# file add the following:
#
# require "test_helper"
#
# class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
# driven_by :selenium, using: :firefox
# end
#
# +driven_by+ has a required argument for the driver name. The keyword
# arguments are +:using+ for the browser and +:screen_size+ to change the
# size of the browser screen. These two options are not applicable for
# headless drivers and will be silently ignored if passed.
#
# Headless browsers such as headless Chrome and headless Firefox are also supported.
# You can use these browsers by setting the +:using+ argument to +:headless_chrome+ or +:headless_firefox+.
#
# To use a headless driver, like Poltergeist, update your Gemfile to use
# Poltergeist instead of Selenium and then declare the driver name in the
# +application_system_test_case.rb+ file. In this case, you would leave out
# the +:using+ option because the driver is headless, but you can still use
# +:screen_size+ to change the size of the browser screen, also you can use
# +:options+ to pass options supported by the driver. Please refer to your
# driver documentation to learn about supported options.
#
# require "test_helper"
# require "capybara/poltergeist"
#
# class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
# driven_by :poltergeist, screen_size: [1400, 1400], options:
# { js_errors: true }
# end
#
# Some drivers require browser capabilities to be passed as a block instead
# of through the +options+ hash.
#
# As an example, if you want to add mobile emulation on chrome, you'll have to
# create an instance of selenium's +Chrome::Options+ object and add
# capabilities with a block.
#
# The block will be passed an instance of <tt><Driver>::Options</tt> where you can
# define the capabilities you want. Please refer to your driver documentation
# to learn about supported options.
#
# class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
# driven_by :selenium, using: :chrome, screen_size: [1024, 768] do |driver_option|
# driver_option.add_emulation(device_name: 'iPhone 6')
# driver_option.add_extension('path/to/chrome_extension.crx')
# end
# end
#
# Because <tt>ActionDispatch::SystemTestCase</tt> is a shim between Capybara
# and Rails, any driver that is supported by Capybara is supported by system
# tests as long as you include the required gems and files.
- 1
class SystemTestCase < ActiveSupport::TestCase
- 1
include Capybara::DSL
- 1
include Capybara::Minitest::Assertions
- 1
include SystemTesting::TestHelpers::SetupAndTeardown
- 1
include SystemTesting::TestHelpers::ScreenshotHelper
- 1
def initialize(*) # :nodoc:
super
self.class.driven_by(:selenium) unless self.class.driver?
self.class.driver.use
end
- 1
def self.start_application # :nodoc:
- 1
Capybara.app = Rack::Builder.new do
- 1
map "/" do
run Rails.application
end
end
- 1
SystemTesting::Server.new.run
end
- 1
class_attribute :driver, instance_accessor: false
# System Test configuration options
#
# The default settings are Selenium, using Chrome, with a screen size
# of 1400x1400.
#
# Examples:
#
# driven_by :poltergeist
#
# driven_by :selenium, screen_size: [800, 800]
#
# driven_by :selenium, using: :chrome
#
# driven_by :selenium, using: :headless_chrome
#
# driven_by :selenium, using: :firefox
#
# driven_by :selenium, using: :headless_firefox
- 1
def self.driven_by(driver, using: :chrome, screen_size: [1400, 1400], options: {}, &capabilities)
- 5
driver_options = { using: using, screen_size: screen_size, options: options }
- 5
self.driver = SystemTesting::Driver.new(driver, **driver_options, &capabilities)
end
- 1
private
- 1
def url_helpers
@url_helpers ||=
if ActionDispatch.test_app
Class.new do
include ActionDispatch.test_app.routes.url_helpers
include ActionDispatch.test_app.routes.mounted_helpers
def url_options
default_url_options.reverse_merge(host: Capybara.app_host || Capybara.current_session.server_url)
end
end.new
end
end
- 1
def method_missing(name, *args, &block)
if url_helpers.respond_to?(name)
url_helpers.public_send(name, *args, &block)
else
super
end
end
- 1
def respond_to_missing?(name, include_private = false)
url_helpers.respond_to?(name)
end
end
end
- 1
ActiveSupport.run_load_hooks :action_dispatch_system_test_case, ActionDispatch::SystemTestCase
- 1
ActionDispatch::SystemTestCase.start_application
# frozen_string_literal: true
- 1
module ActionDispatch
- 1
module SystemTesting
- 1
class Browser # :nodoc:
- 1
attr_reader :name, :options
- 1
def initialize(name)
- 5
@name = name
- 5
set_default_options
end
- 1
def type
- 5
case name
when :headless_chrome
- 2
:chrome
when :headless_firefox
- 2
:firefox
else
- 1
name
end
end
- 1
def configure
- 2
initialize_options
- 2
yield options if block_given? && options
end
# driver_path can be configured as a proc. The webdrivers gem uses this
# proc to update web drivers. Running this proc early allows us to only
# update the webdriver once and avoid race conditions when using
# parallel tests.
- 1
def preload
- 3
case type
when :chrome
- 2
if ::Selenium::WebDriver::Service.respond_to? :driver_path=
- 2
::Selenium::WebDriver::Chrome::Service.driver_path&.call
else
# Selenium <= v3.141.0
::Selenium::WebDriver::Chrome.driver_path
end
when :firefox
- 1
if ::Selenium::WebDriver::Service.respond_to? :driver_path=
- 1
::Selenium::WebDriver::Firefox::Service.driver_path&.call
else
# Selenium <= v3.141.0
::Selenium::WebDriver::Firefox.driver_path
end
end
end
- 1
private
- 1
def initialize_options
- 2
@options ||=
case type
when :chrome
- 1
::Selenium::WebDriver::Chrome::Options.new
when :firefox
- 1
::Selenium::WebDriver::Firefox::Options.new
end
end
- 1
def set_default_options
- 5
case name
when :headless_chrome
- 1
set_headless_chrome_browser_options
when :headless_firefox
- 1
set_headless_firefox_browser_options
end
end
- 1
def set_headless_chrome_browser_options
- 1
configure do |capabilities|
- 1
capabilities.args << "--headless"
- 1
capabilities.args << "--disable-gpu" if Gem.win_platform?
end
end
- 1
def set_headless_firefox_browser_options
- 1
configure do |capabilities|
- 1
capabilities.args << "-headless"
end
end
end
end
end
# frozen_string_literal: true
- 1
module ActionDispatch
- 1
module SystemTesting
- 1
class Driver # :nodoc:
- 1
def initialize(name, **options, &capabilities)
- 5
@name = name
- 5
@browser = Browser.new(options[:using])
- 5
@screen_size = options[:screen_size]
- 5
@options = options[:options] || {}
- 5
@capabilities = capabilities
- 5
if name == :selenium
- 3
require "selenium/webdriver"
- 3
@browser.preload
end
end
- 1
def use
register if registerable?
setup
end
- 1
private
- 1
def registerable?
[:selenium, :poltergeist, :webkit].include?(@name)
end
- 1
def register
@browser.configure(&@capabilities)
Capybara.register_driver @name do |app|
case @name
when :selenium then register_selenium(app)
when :poltergeist then register_poltergeist(app)
when :webkit then register_webkit(app)
end
end
end
- 1
def browser_options
@options.merge(options: @browser.options).compact
end
- 1
def register_selenium(app)
Capybara::Selenium::Driver.new(app, **{ browser: @browser.type }.merge(browser_options)).tap do |driver|
driver.browser.manage.window.size = Selenium::WebDriver::Dimension.new(*@screen_size)
end
end
- 1
def register_poltergeist(app)
Capybara::Poltergeist::Driver.new(app, @options.merge(window_size: @screen_size))
end
- 1
def register_webkit(app)
Capybara::Webkit::Driver.new(app, Capybara::Webkit::Configuration.to_hash.merge(@options)).tap do |driver|
driver.resize_window_to(driver.current_window_handle, *@screen_size)
end
end
- 1
def setup
Capybara.current_driver = @name
end
end
end
end
# frozen_string_literal: true
- 1
module ActionDispatch
- 1
module SystemTesting
- 1
class Server # :nodoc:
- 1
class << self
- 1
attr_accessor :silence_puma
end
- 1
self.silence_puma = false
- 1
def run
- 1
setup
end
- 1
private
- 1
def setup
- 1
set_server
- 1
set_port
end
- 1
def set_server
- 1
Capybara.server = :puma, { Silent: self.class.silence_puma } if Capybara.server == Capybara.servers[:default]
end
- 1
def set_port
- 1
Capybara.always_include_port = true
end
end
end
end
# frozen_string_literal: true
- 1
module ActionDispatch
- 1
module SystemTesting
- 1
module TestHelpers
# Screenshot helper for system testing.
- 1
module ScreenshotHelper
# Takes a screenshot of the current page in the browser.
#
# +take_screenshot+ can be used at any point in your system tests to take
# a screenshot of the current state. This can be useful for debugging or
# automating visual testing. You can take multiple screenshots per test
# to investigate changes at different points during your test. These will be
# named with a sequential prefix (or 'failed' for failing tests)
#
# The screenshot will be displayed in your console, if supported.
#
# You can set the +RAILS_SYSTEM_TESTING_SCREENSHOT_HTML+ environment variable to
# save the HTML from the page that is being screenhoted so you can investigate the
# elements on the page at the time of the screenshot
#
# You can set the +RAILS_SYSTEM_TESTING_SCREENSHOT+ environment variable to
# control the output. Possible values are:
# * [+simple+ (default)] Only displays the screenshot path.
# This is the default value.
# * [+inline+] Display the screenshot in the terminal using the
# iTerm image protocol (https://iterm2.com/documentation-images.html).
# * [+artifact+] Display the screenshot in the terminal, using the terminal
# artifact format (https://buildkite.github.io/terminal-to-html/inline-images/).
- 1
def take_screenshot
increment_unique
save_html if save_html?
save_image
puts display_image
end
# Takes a screenshot of the current page in the browser if the test
# failed.
#
# +take_failed_screenshot+ is included in <tt>application_system_test_case.rb</tt>
# that is generated with the application. To take screenshots when a test
# fails add +take_failed_screenshot+ to the teardown block before clearing
# sessions.
- 1
def take_failed_screenshot
take_screenshot if failed? && supports_screenshot?
end
- 1
private
- 1
attr_accessor :_screenshot_counter
- 1
def save_html?
ENV["RAILS_SYSTEM_TESTING_SCREENSHOT_HTML"] == "1"
end
- 1
def increment_unique
@_screenshot_counter ||= 0
@_screenshot_counter += 1
end
- 1
def unique
failed? ? "failures" : (_screenshot_counter || 0).to_s
end
- 1
def image_name
sanitized_method_name = method_name.tr("/\\", "--")
name = "#{unique}_#{sanitized_method_name}"
name[0...225]
end
- 1
def image_path
absolute_image_path.to_s
end
- 1
def html_path
absolute_html_path.to_s
end
- 1
def absolute_path
Rails.root.join("tmp/screenshots/#{image_name}")
end
- 1
def absolute_image_path
"#{absolute_path}.png"
end
- 1
def absolute_html_path
"#{absolute_path}.html"
end
- 1
def save_html
page.save_page(absolute_html_path)
end
- 1
def save_image
page.save_screenshot(absolute_image_path)
end
- 1
def output_type
# Environment variables have priority
output_type = ENV["RAILS_SYSTEM_TESTING_SCREENSHOT"] || ENV["CAPYBARA_INLINE_SCREENSHOT"]
# Default to outputting a path to the screenshot
output_type ||= "simple"
output_type
end
- 1
def display_image
message = +"[Screenshot Image]: #{image_path}\n"
message << +"[Screenshot HTML]: #{html_path}\n" if save_html?
case output_type
when "artifact"
message << "\e]1338;url=artifact://#{absolute_image_path}\a\n"
when "inline"
name = inline_base64(File.basename(absolute_image_path))
image = inline_base64(File.read(absolute_image_path))
message << "\e]1337;File=name=#{name};height=400px;inline=1:#{image}\a\n"
end
message
end
- 1
def inline_base64(path)
Base64.strict_encode64(path)
end
- 1
def failed?
!passed? && !skipped?
end
- 1
def supports_screenshot?
Capybara.current_driver != :rack_test
end
end
end
end
end
# frozen_string_literal: true
- 1
module ActionDispatch
- 1
module SystemTesting
- 1
module TestHelpers
- 1
module SetupAndTeardown # :nodoc:
- 1
def host!(host)
ActiveSupport::Deprecation.warn \
"ActionDispatch::SystemTestCase#host! is deprecated with no replacement. " \
"Set Capybara.app_host directly or rely on Capybara's default host."
Capybara.app_host = host
end
- 1
def before_teardown
take_failed_screenshot
ensure
super
end
- 1
def after_teardown
Capybara.reset_sessions!
ensure
super
end
end
end
end
end
# frozen_string_literal: true
module ActionDispatch
# This is a class that abstracts away an asserted response. It purposely
# does not inherit from Response because it doesn't need it. That means it
# does not have headers or a body.
class AssertionResponse
attr_reader :code, :name
GENERIC_RESPONSE_CODES = { # :nodoc:
success: "2XX",
missing: "404",
redirect: "3XX",
error: "5XX"
}
# Accepts a specific response status code as an Integer (404) or String
# ('404') or a response status range as a Symbol pseudo-code (:success,
# indicating any 200-299 status code).
def initialize(code_or_name)
if code_or_name.is_a?(Symbol)
@name = code_or_name
@code = code_from_name(code_or_name)
else
@name = name_from_code(code_or_name)
@code = code_or_name
end
raise ArgumentError, "Invalid response name: #{name}" if @code.nil?
raise ArgumentError, "Invalid response code: #{code}" if @name.nil?
end
def code_and_name
"#{code}: #{name}"
end
private
def code_from_name(name)
GENERIC_RESPONSE_CODES[name] || Rack::Utils::SYMBOL_TO_STATUS_CODE[name]
end
def name_from_code(code)
GENERIC_RESPONSE_CODES.invert[code] || Rack::Utils::HTTP_STATUS_CODES[code]
end
end
end
# frozen_string_literal: true
- 1
require "rails-dom-testing"
- 1
module ActionDispatch
- 1
module Assertions
- 1
autoload :ResponseAssertions, "action_dispatch/testing/assertions/response"
- 1
autoload :RoutingAssertions, "action_dispatch/testing/assertions/routing"
- 1
extend ActiveSupport::Concern
- 1
include ResponseAssertions
- 1
include RoutingAssertions
- 1
include Rails::Dom::Testing::Assertions
- 1
def html_document
@html_document ||= if @response.media_type&.end_with?("xml")
Nokogiri::XML::Document.parse(@response.body)
else
Nokogiri::HTML::Document.parse(@response.body)
end
end
end
end
# frozen_string_literal: true
- 1
module ActionDispatch
- 1
module Assertions
# A small suite of assertions that test responses from \Rails applications.
- 1
module ResponseAssertions
- 1
RESPONSE_PREDICATES = { # :nodoc:
success: :successful?,
missing: :not_found?,
redirect: :redirection?,
error: :server_error?,
}
# Asserts that the response is one of the following types:
#
# * <tt>:success</tt> - Status code was in the 200-299 range
# * <tt>:redirect</tt> - Status code was in the 300-399 range
# * <tt>:missing</tt> - Status code was 404
# * <tt>:error</tt> - Status code was in the 500-599 range
#
# You can also pass an explicit status number like <tt>assert_response(501)</tt>
# or its symbolic equivalent <tt>assert_response(:not_implemented)</tt>.
# See Rack::Utils::SYMBOL_TO_STATUS_CODE for a full list.
#
# # Asserts that the response was a redirection
# assert_response :redirect
#
# # Asserts that the response code was status code 401 (unauthorized)
# assert_response 401
- 1
def assert_response(type, message = nil)
message ||= generate_response_message(type)
if RESPONSE_PREDICATES.keys.include?(type)
assert @response.send(RESPONSE_PREDICATES[type]), message
else
assert_equal AssertionResponse.new(type).code, @response.response_code, message
end
end
# Asserts that the response is a redirect to a URL matching the given options.
#
# # Asserts that the redirection was to the "index" action on the WeblogController
# assert_redirected_to controller: "weblog", action: "index"
#
# # Asserts that the redirection was to the named route login_url
# assert_redirected_to login_url
#
# # Asserts that the redirection was to the URL for @customer
# assert_redirected_to @customer
#
# # Asserts that the redirection matches the regular expression
# assert_redirected_to %r(\Ahttp://example.org)
- 1
def assert_redirected_to(options = {}, message = nil)
assert_response(:redirect, message)
return true if options === @response.location
redirect_is = normalize_argument_to_redirection(@response.location)
redirect_expected = normalize_argument_to_redirection(options)
message ||= "Expected response to be a redirect to <#{redirect_expected}> but was a redirect to <#{redirect_is}>"
assert_operator redirect_expected, :===, redirect_is, message
end
- 1
private
# Proxy to to_param if the object will respond to it.
- 1
def parameterize(value)
value.respond_to?(:to_param) ? value.to_param : value
end
- 1
def normalize_argument_to_redirection(fragment)
if Regexp === fragment
fragment
else
handle = @controller || ActionController::Redirecting
handle._compute_redirect_to_location(@request, fragment)
end
end
- 1
def generate_response_message(expected, actual = @response.response_code)
(+"Expected response to be a <#{code_with_name(expected)}>,"\
" but was a <#{code_with_name(actual)}>").concat(location_if_redirected).concat(response_body_if_short)
end
- 1
def response_body_if_short
return "" if @response.body.size > 500
"\nResponse body: #{@response.body}"
end
- 1
def location_if_redirected
return "" unless @response.redirection? && @response.location.present?
location = normalize_argument_to_redirection(@response.location)
" redirect to <#{location}>"
end
- 1
def code_with_name(code_or_name)
if RESPONSE_PREDICATES.values.include?("#{code_or_name}?".to_sym)
code_or_name = RESPONSE_PREDICATES.invert["#{code_or_name}?".to_sym]
end
AssertionResponse.new(code_or_name).code_and_name
end
end
end
end
# frozen_string_literal: true
- 1
require "uri"
- 1
require "active_support/core_ext/hash/indifferent_access"
- 1
require "active_support/core_ext/string/access"
- 1
require "action_controller/metal/exceptions"
- 1
module ActionDispatch
- 1
module Assertions
# Suite of assertions to test routes generated by \Rails and the handling of requests made to them.
- 1
module RoutingAssertions
- 1
def setup # :nodoc:
@routes ||= nil
super
end
# Asserts that the routing of the given +path+ was handled correctly and that the parsed options (given in the +expected_options+ hash)
# match +path+. Basically, it asserts that \Rails recognizes the route given by +expected_options+.
#
# Pass a hash in the second argument (+path+) to specify the request method. This is useful for routes
# requiring a specific HTTP method. The hash should contain a :path with the incoming request path
# and a :method containing the required HTTP verb.
#
# # Asserts that POSTing to /items will call the create action on ItemsController
# assert_recognizes({controller: 'items', action: 'create'}, {path: 'items', method: :post})
#
# You can also pass in +extras+ with a hash containing URL parameters that would normally be in the query string. This can be used
# to assert that values in the query string will end up in the params hash correctly. To test query strings you must use the extras
# argument because appending the query string on the path directly will not work. For example:
#
# # Asserts that a path of '/items/list/1?view=print' returns the correct options
# assert_recognizes({controller: 'items', action: 'list', id: '1', view: 'print'}, 'items/list/1', { view: "print" })
#
# The +message+ parameter allows you to pass in an error message that is displayed upon failure.
#
# # Check the default route (i.e., the index action)
# assert_recognizes({controller: 'items', action: 'index'}, 'items')
#
# # Test a specific action
# assert_recognizes({controller: 'items', action: 'list'}, 'items/list')
#
# # Test an action with a parameter
# assert_recognizes({controller: 'items', action: 'destroy', id: '1'}, 'items/destroy/1')
#
# # Test a custom route
# assert_recognizes({controller: 'items', action: 'show', id: '1'}, 'view/item1')
- 1
def assert_recognizes(expected_options, path, extras = {}, msg = nil)
if path.is_a?(Hash) && path[:method].to_s == "all"
[:get, :post, :put, :delete].each do |method|
assert_recognizes(expected_options, path.merge(method: method), extras, msg)
end
else
request = recognized_request_for(path, extras, msg)
expected_options = expected_options.clone
expected_options.stringify_keys!
msg = message(msg, "") {
sprintf("The recognized options <%s> did not match <%s>, difference:",
request.path_parameters, expected_options)
}
assert_equal(expected_options, request.path_parameters, msg)
end
end
# Asserts that the provided options can be used to generate the provided path. This is the inverse of +assert_recognizes+.
# The +extras+ parameter is used to tell the request the names and values of additional request parameters that would be in
# a query string. The +message+ parameter allows you to specify a custom error message for assertion failures.
#
# The +defaults+ parameter is unused.
#
# # Asserts that the default action is generated for a route with no action
# assert_generates "/items", controller: "items", action: "index"
#
# # Tests that the list action is properly routed
# assert_generates "/items/list", controller: "items", action: "list"
#
# # Tests the generation of a route with a parameter
# assert_generates "/items/list/1", { controller: "items", action: "list", id: "1" }
#
# # Asserts that the generated route gives us our custom route
# assert_generates "changesets/12", { controller: 'scm', action: 'show_diff', revision: "12" }
- 1
def assert_generates(expected_path, options, defaults = {}, extras = {}, message = nil)
if %r{://}.match?(expected_path)
fail_on(URI::InvalidURIError, message) do
uri = URI.parse(expected_path)
expected_path = uri.path.to_s.empty? ? "/" : uri.path
end
else
expected_path = "/#{expected_path}" unless expected_path.start_with?("/")
end
# Load routes.rb if it hasn't been loaded.
options = options.clone
generated_path, query_string_keys = @routes.generate_extras(options, defaults)
found_extras = options.reject { |k, _| ! query_string_keys.include? k }
msg = message || sprintf("found extras <%s>, not <%s>", found_extras, extras)
assert_equal(extras, found_extras, msg)
msg = message || sprintf("The generated path <%s> did not match <%s>", generated_path,
expected_path)
assert_equal(expected_path, generated_path, msg)
end
# Asserts that path and options match both ways; in other words, it verifies that <tt>path</tt> generates
# <tt>options</tt> and then that <tt>options</tt> generates <tt>path</tt>. This essentially combines +assert_recognizes+
# and +assert_generates+ into one step.
#
# The +extras+ hash allows you to specify options that would normally be provided as a query string to the action. The
# +message+ parameter allows you to specify a custom error message to display upon failure.
#
# # Asserts a basic route: a controller with the default action (index)
# assert_routing '/home', controller: 'home', action: 'index'
#
# # Test a route generated with a specific controller, action, and parameter (id)
# assert_routing '/entries/show/23', controller: 'entries', action: 'show', id: 23
#
# # Asserts a basic route (controller + default action), with an error message if it fails
# assert_routing '/store', { controller: 'store', action: 'index' }, {}, {}, 'Route for store index not generated properly'
#
# # Tests a route, providing a defaults hash
# assert_routing 'controller/action/9', {id: "9", item: "square"}, {controller: "controller", action: "action"}, {}, {item: "square"}
#
# # Tests a route with an HTTP method
# assert_routing({ method: 'put', path: '/product/321' }, { controller: "product", action: "update", id: "321" })
- 1
def assert_routing(path, options, defaults = {}, extras = {}, message = nil)
assert_recognizes(options, path, extras, message)
controller, default_controller = options[:controller], defaults[:controller]
if controller && controller.include?(?/) && default_controller && default_controller.include?(?/)
options[:controller] = "/#{controller}"
end
generate_options = options.dup.delete_if { |k, _| defaults.key?(k) }
assert_generates(path.is_a?(Hash) ? path[:path] : path, generate_options, defaults, extras, message)
end
# A helper to make it easier to test different route configurations.
# This method temporarily replaces @routes with a new RouteSet instance.
#
# The new instance is yielded to the passed block. Typically the block
# will create some routes using <tt>set.draw { match ... }</tt>:
#
# with_routing do |set|
# set.draw do
# resources :users
# end
# assert_equal "/users", users_path
# end
#
- 1
def with_routing
old_routes, @routes = @routes, ActionDispatch::Routing::RouteSet.new
if defined?(@controller) && @controller
old_controller, @controller = @controller, @controller.clone
_routes = @routes
@controller.singleton_class.include(_routes.url_helpers)
if @controller.respond_to? :view_context_class
view_context_class = Class.new(@controller.view_context_class) do
include _routes.url_helpers
end
custom_view_context = Module.new {
define_method(:view_context_class) do
view_context_class
end
}
@controller.extend(custom_view_context)
end
end
yield @routes
ensure
@routes = old_routes
if defined?(@controller) && @controller
@controller = old_controller
end
end
# ROUTES TODO: These assertions should really work in an integration context
- 1
def method_missing(selector, *args, &block)
if defined?(@controller) && @controller && defined?(@routes) && @routes && @routes.named_routes.route_defined?(selector)
@controller.send(selector, *args, &block)
else
super
end
end
- 1
private
# Recognizes the route for a given path.
- 1
def recognized_request_for(path, extras = {}, msg)
if path.is_a?(Hash)
method = path[:method]
path = path[:path]
else
method = :get
end
controller = @controller if defined?(@controller)
request = ActionController::TestRequest.create controller&.class
if %r{://}.match?(path)
fail_on(URI::InvalidURIError, msg) do
uri = URI.parse(path)
request.env["rack.url_scheme"] = uri.scheme || "http"
request.host = uri.host if uri.host
request.port = uri.port if uri.port
request.path = uri.path.to_s.empty? ? "/" : uri.path
end
else
path = "/#{path}" unless path.start_with?("/")
request.path = path
end
request.request_method = method if method
params = fail_on(ActionController::RoutingError, msg) do
@routes.recognize_path(path, method: method, extras: extras)
end
request.path_parameters = params.with_indifferent_access
request
end
- 1
def fail_on(exception_class, message)
yield
rescue exception_class => e
raise Minitest::Assertion, message || e.message
end
end
end
end
# frozen_string_literal: true
- 1
require "stringio"
- 1
require "uri"
- 1
require "rack/test"
- 1
require "minitest"
- 1
require "action_dispatch/testing/request_encoder"
- 1
module ActionDispatch
- 1
module Integration #:nodoc:
- 1
module RequestHelpers
# Performs a GET request with the given parameters. See ActionDispatch::Integration::Session#process
# for more details.
- 1
def get(path, **args)
process(:get, path, **args)
end
# Performs a POST request with the given parameters. See ActionDispatch::Integration::Session#process
# for more details.
- 1
def post(path, **args)
process(:post, path, **args)
end
# Performs a PATCH request with the given parameters. See ActionDispatch::Integration::Session#process
# for more details.
- 1
def patch(path, **args)
process(:patch, path, **args)
end
# Performs a PUT request with the given parameters. See ActionDispatch::Integration::Session#process
# for more details.
- 1
def put(path, **args)
process(:put, path, **args)
end
# Performs a DELETE request with the given parameters. See ActionDispatch::Integration::Session#process
# for more details.
- 1
def delete(path, **args)
process(:delete, path, **args)
end
# Performs a HEAD request with the given parameters. See ActionDispatch::Integration::Session#process
# for more details.
- 1
def head(path, **args)
process(:head, path, **args)
end
# Performs an OPTIONS request with the given parameters. See ActionDispatch::Integration::Session#process
# for more details.
- 1
def options(path, **args)
process(:options, path, **args)
end
# Follow a single redirect response. If the last response was not a
# redirect, an exception will be raised. Otherwise, the redirect is
# performed on the location header. If the redirection is a 307 or 308 redirect,
# the same HTTP verb will be used when redirecting, otherwise a GET request
# will be performed. Any arguments are passed to the
# underlying request.
- 1
def follow_redirect!(**args)
raise "not a redirect! #{status} #{status_message}" unless redirect?
method =
if [307, 308].include?(response.status)
request.method.downcase
else
:get
end
public_send(method, response.location, **args)
status
end
end
# An instance of this class represents a set of requests and responses
# performed sequentially by a test process. Because you can instantiate
# multiple sessions and run them side-by-side, you can also mimic (to some
# limited extent) multiple simultaneous users interacting with your system.
#
# Typically, you will instantiate a new session using
# IntegrationTest#open_session, rather than instantiating
# Integration::Session directly.
- 1
class Session
- 1
DEFAULT_HOST = "www.example.com"
- 1
include Minitest::Assertions
- 1
include TestProcess, RequestHelpers, Assertions
- 1
delegate :status, :status_message, :headers, :body, :redirect?, to: :response, allow_nil: true
- 1
delegate :path, to: :request, allow_nil: true
# The hostname used in the last request.
- 1
def host
@host || DEFAULT_HOST
end
- 1
attr_writer :host
# The remote_addr used in the last request.
- 1
attr_accessor :remote_addr
# The Accept header to send.
- 1
attr_accessor :accept
# A map of the cookies returned by the last response, and which will be
# sent with the next request.
- 1
def cookies
_mock_session.cookie_jar
end
# A reference to the controller instance used by the last request.
- 1
attr_reader :controller
# A reference to the request instance used by the last request.
- 1
attr_reader :request
# A reference to the response instance used by the last request.
- 1
attr_reader :response
# A running counter of the number of requests processed.
- 1
attr_accessor :request_count
- 1
include ActionDispatch::Routing::UrlFor
# Create and initialize a new Session instance.
- 1
def initialize(app)
super()
@app = app
reset!
end
- 1
def url_options
@url_options ||= default_url_options.dup.tap do |url_options|
url_options.reverse_merge!(controller.url_options) if controller.respond_to?(:url_options)
if @app.respond_to?(:routes)
url_options.reverse_merge!(@app.routes.default_url_options)
end
url_options.reverse_merge!(host: host, protocol: https? ? "https" : "http")
end
end
# Resets the instance. This can be used to reset the state information
# in an existing session instance, so it can be used from a clean-slate
# condition.
#
# session.reset!
- 1
def reset!
@https = false
@controller = @request = @response = nil
@_mock_session = nil
@request_count = 0
@url_options = nil
self.host = DEFAULT_HOST
self.remote_addr = "127.0.0.1"
self.accept = "text/xml,application/xml,application/xhtml+xml," \
"text/html;q=0.9,text/plain;q=0.8,image/png," \
"*/*;q=0.5"
unless defined? @named_routes_configured
# the helpers are made protected by default--we make them public for
# easier access during testing and troubleshooting.
@named_routes_configured = true
end
end
# Specify whether or not the session should mimic a secure HTTPS request.
#
# session.https!
# session.https!(false)
- 1
def https!(flag = true)
@https = flag
end
# Returns +true+ if the session is mimicking a secure HTTPS request.
#
# if session.https?
# ...
# end
- 1
def https?
@https
end
# Performs the actual request.
#
# - +method+: The HTTP method (GET, POST, PATCH, PUT, DELETE, HEAD, OPTIONS)
# as a symbol.
# - +path+: The URI (as a String) on which you want to perform the
# request.
# - +params+: The HTTP parameters that you want to pass. This may
# be +nil+,
# a Hash, or a String that is appropriately encoded
# (<tt>application/x-www-form-urlencoded</tt> or
# <tt>multipart/form-data</tt>).
# - +headers+: Additional headers to pass, as a Hash. The headers will be
# merged into the Rack env hash.
# - +env+: Additional env to pass, as a Hash. The headers will be
# merged into the Rack env hash.
# - +xhr+: Set to `true` if you want to make and Ajax request.
# Adds request headers characteristic of XMLHttpRequest e.g. HTTP_X_REQUESTED_WITH.
# The headers will be merged into the Rack env hash.
# - +as+: Used for encoding the request with different content type.
# Supports `:json` by default and will set the appropriate request headers.
# The headers will be merged into the Rack env hash.
#
# This method is rarely used directly. Use +#get+, +#post+, or other standard
# HTTP methods in integration tests. +#process+ is only required when using a
# request method that doesn't have a method defined in the integration tests.
#
# This method returns the response status, after performing the request.
# Furthermore, if this method was called from an ActionDispatch::IntegrationTest object,
# then that object's <tt>@response</tt> instance variable will point to a Response object
# which one can use to inspect the details of the response.
#
# Example:
# process :get, '/author', params: { since: 201501011400 }
- 1
def process(method, path, params: nil, headers: nil, env: nil, xhr: false, as: nil)
request_encoder = RequestEncoder.encoder(as)
headers ||= {}
if method == :get && as == :json && params
headers["X-Http-Method-Override"] = "GET"
method = :post
end
if %r{://}.match?(path)
path = build_expanded_path(path) do |location|
https! URI::HTTPS === location if location.scheme
if url_host = location.host
default = Rack::Request::DEFAULT_PORTS[location.scheme]
url_host += ":#{location.port}" if default != location.port
host! url_host
end
end
end
hostname, port = host.split(":")
request_env = {
:method => method,
:params => request_encoder.encode_params(params),
"SERVER_NAME" => hostname,
"SERVER_PORT" => port || (https? ? "443" : "80"),
"HTTPS" => https? ? "on" : "off",
"rack.url_scheme" => https? ? "https" : "http",
"REQUEST_URI" => path,
"HTTP_HOST" => host,
"REMOTE_ADDR" => remote_addr,
"CONTENT_TYPE" => request_encoder.content_type,
"HTTP_ACCEPT" => request_encoder.accept_header || accept
}
wrapped_headers = Http::Headers.from_hash({})
wrapped_headers.merge!(headers) if headers
if xhr
wrapped_headers["HTTP_X_REQUESTED_WITH"] = "XMLHttpRequest"
wrapped_headers["HTTP_ACCEPT"] ||= [Mime[:js], Mime[:html], Mime[:xml], "text/xml", "*/*"].join(", ")
end
# This modifies the passed request_env directly.
if wrapped_headers.present?
Http::Headers.from_hash(request_env).merge!(wrapped_headers)
end
if env.present?
Http::Headers.from_hash(request_env).merge!(env)
end
session = Rack::Test::Session.new(_mock_session)
# NOTE: rack-test v0.5 doesn't build a default uri correctly
# Make sure requested path is always a full URI.
session.request(build_full_uri(path, request_env), request_env)
@request_count += 1
@request = ActionDispatch::Request.new(session.last_request.env)
response = _mock_session.last_response
@response = ActionDispatch::TestResponse.from_response(response)
@response.request = @request
@html_document = nil
@url_options = nil
@controller = @request.controller_instance
response.status
end
# Set the host name to use in the next request.
#
# session.host! "www.example.com"
- 1
alias :host! :host=
- 1
private
- 1
def _mock_session
@_mock_session ||= Rack::MockSession.new(@app, host)
end
- 1
def build_full_uri(path, env)
"#{env['rack.url_scheme']}://#{env['SERVER_NAME']}:#{env['SERVER_PORT']}#{path}"
end
- 1
def build_expanded_path(path)
location = URI.parse(path)
yield location if block_given?
path = location.path
location.query ? "#{path}?#{location.query}" : path
end
end
- 1
module Runner
- 1
include ActionDispatch::Assertions
- 1
APP_SESSIONS = {}
- 1
attr_reader :app
- 1
attr_accessor :root_session # :nodoc:
- 1
def initialize(*args, &blk)
super(*args, &blk)
@integration_session = nil
end
- 1
def before_setup # :nodoc:
@app = nil
super
end
- 1
def integration_session
@integration_session ||= create_session(app)
end
# Reset the current session. This is useful for testing multiple sessions
# in a single test case.
- 1
def reset!
@integration_session = create_session(app)
end
- 1
def create_session(app)
klass = APP_SESSIONS[app] ||= Class.new(Integration::Session) {
# If the app is a Rails app, make url_helpers available on the session.
# This makes app.url_for and app.foo_path available in the console.
if app.respond_to?(:routes) && app.routes.is_a?(ActionDispatch::Routing::RouteSet)
include app.routes.url_helpers
include app.routes.mounted_helpers
end
}
klass.new(app)
end
- 1
def remove! # :nodoc:
@integration_session = nil
end
- 1
%w(get post patch put head delete cookies assigns follow_redirect!).each do |method|
# reset the html_document variable, except for cookies/assigns calls
- 9
unless method == "cookies" || method == "assigns"
- 7
reset_html_document = "@html_document = nil"
end
- 9
definition = RUBY_VERSION >= "2.7" ? "..." : "*args"
- 9
module_eval <<~RUBY, __FILE__, __LINE__ + 1
def #{method}(#{definition})
#{reset_html_document}
result = integration_session.#{method}(#{definition})
copy_session_variables!
result
end
RUBY
end
# Open a new session instance. If a block is given, the new session is
# yielded to the block before being returned.
#
# session = open_session do |sess|
# sess.extend(CustomAssertions)
# end
#
# By default, a single session is automatically created for you, but you
# can use this method to open multiple sessions that ought to be tested
# simultaneously.
- 1
def open_session
dup.tap do |session|
session.reset!
session.root_session = self.root_session || self
yield session if block_given?
end
end
- 1
def assertions # :nodoc:
root_session ? root_session.assertions : super
end
- 1
def assertions=(assertions) # :nodoc:
root_session ? root_session.assertions = assertions : super
end
# Copy the instance variables from the current session instance into the
# test instance.
- 1
def copy_session_variables! #:nodoc:
@controller = @integration_session.controller
@response = @integration_session.response
@request = @integration_session.request
end
- 1
def default_url_options
integration_session.default_url_options
end
- 1
def default_url_options=(options)
integration_session.default_url_options = options
end
- 1
private
- 1
def respond_to_missing?(method, _)
integration_session.respond_to?(method) || super
end
# Delegate unhandled messages to the current session instance.
- 1
def method_missing(method, *args, &block)
if integration_session.respond_to?(method)
integration_session.public_send(method, *args, &block).tap do
copy_session_variables!
end
else
super
end
end
- 1
ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
end
end
# An integration test spans multiple controllers and actions,
# tying them all together to ensure they work together as expected. It tests
# more completely than either unit or functional tests do, exercising the
# entire stack, from the dispatcher to the database.
#
# At its simplest, you simply extend <tt>IntegrationTest</tt> and write your tests
# using the get/post methods:
#
# require "test_helper"
#
# class ExampleTest < ActionDispatch::IntegrationTest
# fixtures :people
#
# def test_login
# # get the login page
# get "/login"
# assert_equal 200, status
#
# # post the login and follow through to the home page
# post "/login", params: { username: people(:jamis).username,
# password: people(:jamis).password }
# follow_redirect!
# assert_equal 200, status
# assert_equal "/home", path
# end
# end
#
# However, you can also have multiple session instances open per test, and
# even extend those instances with assertions and methods to create a very
# powerful testing DSL that is specific for your application. You can even
# reference any named routes you happen to have defined.
#
# require "test_helper"
#
# class AdvancedTest < ActionDispatch::IntegrationTest
# fixtures :people, :rooms
#
# def test_login_and_speak
# jamis, david = login(:jamis), login(:david)
# room = rooms(:office)
#
# jamis.enter(room)
# jamis.speak(room, "anybody home?")
#
# david.enter(room)
# david.speak(room, "hello!")
# end
#
# private
#
# module CustomAssertions
# def enter(room)
# # reference a named route, for maximum internal consistency!
# get(room_url(id: room.id))
# assert(...)
# ...
# end
#
# def speak(room, message)
# post "/say/#{room.id}", xhr: true, params: { message: message }
# assert(...)
# ...
# end
# end
#
# def login(who)
# open_session do |sess|
# sess.extend(CustomAssertions)
# who = people(who)
# sess.post "/login", params: { username: who.username,
# password: who.password }
# assert(...)
# end
# end
# end
#
# Another longer example would be:
#
# A simple integration test that exercises multiple controllers:
#
# require "test_helper"
#
# class UserFlowsTest < ActionDispatch::IntegrationTest
# test "login and browse site" do
# # login via https
# https!
# get "/login"
# assert_response :success
#
# post "/login", params: { username: users(:david).username, password: users(:david).password }
# follow_redirect!
# assert_equal '/welcome', path
# assert_equal 'Welcome david!', flash[:notice]
#
# https!(false)
# get "/articles/all"
# assert_response :success
# assert_select 'h1', 'Articles'
# end
# end
#
# As you can see the integration test involves multiple controllers and
# exercises the entire stack from database to dispatcher. In addition you can
# have multiple session instances open simultaneously in a test and extend
# those instances with assertion methods to create a very powerful testing
# DSL (domain-specific language) just for your application.
#
# Here's an example of multiple sessions and custom DSL in an integration test
#
# require "test_helper"
#
# class UserFlowsTest < ActionDispatch::IntegrationTest
# test "login and browse site" do
# # User david logs in
# david = login(:david)
# # User guest logs in
# guest = login(:guest)
#
# # Both are now available in different sessions
# assert_equal 'Welcome david!', david.flash[:notice]
# assert_equal 'Welcome guest!', guest.flash[:notice]
#
# # User david can browse site
# david.browses_site
# # User guest can browse site as well
# guest.browses_site
#
# # Continue with other assertions
# end
#
# private
#
# module CustomDsl
# def browses_site
# get "/products/all"
# assert_response :success
# assert_select 'h1', 'Products'
# end
# end
#
# def login(user)
# open_session do |sess|
# sess.extend(CustomDsl)
# u = users(user)
# sess.https!
# sess.post "/login", params: { username: u.username, password: u.password }
# assert_equal '/welcome', sess.path
# sess.https!(false)
# end
# end
# end
#
# See the {request helpers documentation}[rdoc-ref:ActionDispatch::Integration::RequestHelpers] for help on how to
# use +get+, etc.
#
# === Changing the request encoding
#
# You can also test your JSON API easily by setting what the request should
# be encoded as:
#
# require "test_helper"
#
# class ApiTest < ActionDispatch::IntegrationTest
# test "creates articles" do
# assert_difference -> { Article.count } do
# post articles_path, params: { article: { title: "Ahoy!" } }, as: :json
# end
#
# assert_response :success
# assert_equal({ id: Article.last.id, title: "Ahoy!" }, response.parsed_body)
# end
# end
#
# The +as+ option passes an "application/json" Accept header (thereby setting
# the request format to JSON unless overridden), sets the content type to
# "application/json" and encodes the parameters as JSON.
#
# Calling +parsed_body+ on the response parses the response body based on the
# last response MIME type.
#
# Out of the box, only <tt>:json</tt> is supported. But for any custom MIME
# types you've registered, you can add your own encoders with:
#
# ActionDispatch::IntegrationTest.register_encoder :wibble,
# param_encoder: -> params { params.to_wibble },
# response_parser: -> body { body }
#
# Where +param_encoder+ defines how the params should be encoded and
# +response_parser+ defines how the response body should be parsed through
# +parsed_body+.
#
# Consult the Rails Testing Guide for more.
- 1
class IntegrationTest < ActiveSupport::TestCase
- 1
include TestProcess::FixtureFile
- 1
module UrlOptions
- 1
extend ActiveSupport::Concern
- 1
def url_options
integration_session.url_options
end
end
- 1
module Behavior
- 1
extend ActiveSupport::Concern
- 1
include Integration::Runner
- 1
include ActionController::TemplateAssertions
- 1
included do
- 1
include ActionDispatch::Routing::UrlFor
- 1
include UrlOptions # don't let UrlFor override the url_options method
- 1
ActiveSupport.run_load_hooks(:action_dispatch_integration_test, self)
- 1
@@app = nil
end
- 1
module ClassMethods
- 1
def app
- 1
if defined?(@@app) && @@app
- 1
@@app
else
ActionDispatch.test_app
end
end
- 1
def app=(app)
- 1
@@app = app
end
- 1
def register_encoder(*args, **options)
RequestEncoder.register_encoder(*args, **options)
end
end
- 1
def app
super || self.class.app
end
- 1
def document_root_element
html_document.root
end
end
- 1
include Behavior
end
end
# frozen_string_literal: true
- 1
module ActionDispatch
- 1
class RequestEncoder # :nodoc:
- 1
class IdentityEncoder
- 1
def content_type; end
- 1
def accept_header; end
- 1
def encode_params(params); params; end
- 1
def response_parser; -> body { body }; end
end
- 1
@encoders = { identity: IdentityEncoder.new }
- 1
attr_reader :response_parser
- 1
def initialize(mime_name, param_encoder, response_parser)
- 1
@mime = Mime[mime_name]
- 1
unless @mime
raise ArgumentError, "Can't register a request encoder for " \
"unregistered MIME Type: #{mime_name}. See `Mime::Type.register`."
end
- 1
@response_parser = response_parser || -> body { body }
- 1
@param_encoder = param_encoder || :"to_#{@mime.symbol}".to_proc
end
- 1
def content_type
@mime.to_s
end
- 1
def accept_header
@mime.to_s
end
- 1
def encode_params(params)
@param_encoder.call(params) if params
end
- 1
def self.parser(content_type)
type = Mime::Type.lookup(content_type).ref if content_type
encoder(type).response_parser
end
- 1
def self.encoder(name)
@encoders[name] || @encoders[:identity]
end
- 1
def self.register_encoder(mime_name, param_encoder: nil, response_parser: nil)
- 1
@encoders[mime_name] = new(mime_name, param_encoder, response_parser)
end
- 1
register_encoder :json, response_parser: -> body { JSON.parse(body) }
end
end
# frozen_string_literal: true
- 1
require "action_dispatch/middleware/cookies"
- 1
require "action_dispatch/middleware/flash"
- 1
module ActionDispatch
- 1
module TestProcess
- 1
module FixtureFile
# Shortcut for <tt>Rack::Test::UploadedFile.new(File.join(ActionDispatch::IntegrationTest.file_fixture_path, path), type)</tt>:
#
# post :change_avatar, params: { avatar: fixture_file_upload('spongebob.png', 'image/png') }
#
# Default fixture files location is <tt>test/fixtures/files</tt>.
#
# To upload binary files on Windows, pass <tt>:binary</tt> as the last parameter.
# This will not affect other platforms:
#
# post :change_avatar, params: { avatar: fixture_file_upload('spongebob.png', 'image/png', :binary) }
- 1
def fixture_file_upload(path, mime_type = nil, binary = false)
if self.class.respond_to?(:fixture_path) && self.class.fixture_path &&
!File.exist?(path)
original_path = path
path = Pathname.new(self.class.fixture_path).join(path)
if !self.class.file_fixture_path
ActiveSupport::Deprecation.warn(<<~EOM)
Passing a path to `fixture_file_upload` relative to `fixture_path` is deprecated.
In Rails 6.2, the path needs to be relative to `file_fixture_path` which you
haven't set yet. Set `file_fixture_path` to discard this warning.
EOM
elsif path.exist?
non_deprecated_path = path.relative_path_from(Pathname(self.class.file_fixture_path))
ActiveSupport::Deprecation.warn(<<~EOM)
Passing a path to `fixture_file_upload` relative to `fixture_path` is deprecated.
In Rails 6.2, the path needs to be relative to `file_fixture_path`.
Please modify the call from
`fixture_file_upload("#{original_path}")` to `fixture_file_upload("#{non_deprecated_path}")`.
EOM
else
path = file_fixture(original_path)
end
elsif self.class.file_fixture_path && !File.exist?(path)
path = file_fixture(path)
end
Rack::Test::UploadedFile.new(path, mime_type, binary)
end
end
- 1
include FixtureFile
- 1
def assigns(key = nil)
raise NoMethodError,
"assigns has been extracted to a gem. To continue using it,
add `gem 'rails-controller-testing'` to your Gemfile."
end
- 1
def session
@request.session
end
- 1
def flash
@request.flash
end
- 1
def cookies
@cookie_jar ||= Cookies::CookieJar.build(@request, @request.cookies)
end
- 1
def redirect_to_url
@response.redirect_url
end
end
end
# frozen_string_literal: true
- 1
require "active_support/core_ext/hash/indifferent_access"
- 1
require "rack/utils"
- 1
module ActionDispatch
- 1
class TestRequest < Request
- 1
DEFAULT_ENV = Rack::MockRequest.env_for("/",
"HTTP_HOST" => "test.host".b,
"REMOTE_ADDR" => "0.0.0.0".b,
"HTTP_USER_AGENT" => "Rails Testing".b,
)
# Create a new test request with default +env+ values.
- 1
def self.create(env = {})
env = Rails.application.env_config.merge(env) if defined?(Rails.application) && Rails.application
env["rack.request.cookie_hash"] ||= {}.with_indifferent_access
new(default_env.merge(env))
end
- 1
def self.default_env
DEFAULT_ENV
end
- 1
private_class_method :default_env
- 1
def request_method=(method)
super(method.to_s.upcase)
end
- 1
def host=(host)
set_header("HTTP_HOST", host)
end
- 1
def port=(number)
set_header("SERVER_PORT", number.to_i)
end
- 1
def request_uri=(uri)
set_header("REQUEST_URI", uri)
end
- 1
def path=(path)
set_header("PATH_INFO", path)
end
- 1
def action=(action_name)
path_parameters[:action] = action_name.to_s
end
- 1
def if_modified_since=(last_modified)
set_header("HTTP_IF_MODIFIED_SINCE", last_modified)
end
- 1
def if_none_match=(etag)
set_header("HTTP_IF_NONE_MATCH", etag)
end
- 1
def remote_addr=(addr)
set_header("REMOTE_ADDR", addr)
end
- 1
def user_agent=(user_agent)
set_header("HTTP_USER_AGENT", user_agent)
end
- 1
def accept=(mime_types)
delete_header("action_dispatch.request.accepts")
set_header("HTTP_ACCEPT", Array(mime_types).collect(&:to_s).join(","))
end
end
end
# frozen_string_literal: true
require "action_dispatch/testing/request_encoder"
module ActionDispatch
# Integration test methods such as ActionDispatch::Integration::Session#get
# and ActionDispatch::Integration::Session#post return objects of class
# TestResponse, which represent the HTTP response results of the requested
# controller actions.
#
# See Response for more information on controller response objects.
class TestResponse < Response
def self.from_response(response)
new response.status, response.headers, response.body
end
def parsed_body
@parsed_body ||= response_parser.call(body)
end
def response_parser
@response_parser ||= RequestEncoder.parser(media_type)
end
end
end
# frozen_string_literal: true
#--
# Copyright (c) 2004-2020 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++
- 1
require "action_pack/version"
# frozen_string_literal: true
- 1
module ActionPack
# Returns the version of the currently loaded Action Pack as a <tt>Gem::Version</tt>
- 1
def self.gem_version
Gem::Version.new VERSION::STRING
end
- 1
module VERSION
- 1
MAJOR = 6
- 1
MINOR = 1
- 1
TINY = 0
- 1
PRE = "alpha"
- 1
STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
end
end
# frozen_string_literal: true
- 1
require_relative "gem_version"
- 1
module ActionPack
# Returns the version of the currently loaded ActionPack as a <tt>Gem::Version</tt>
- 1
def self.version
gem_version
end
end