aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib
diff options
context:
space:
mode:
Diffstat (limited to 'actionpack/lib')
-rw-r--r--actionpack/lib/abstract_controller.rb9
-rw-r--r--actionpack/lib/abstract_controller/asset_paths.rb2
-rw-r--r--actionpack/lib/abstract_controller/base.rb47
-rw-r--r--actionpack/lib/abstract_controller/caching.rb14
-rw-r--r--actionpack/lib/abstract_controller/caching/fragments.rb35
-rw-r--r--actionpack/lib/abstract_controller/callbacks.rb83
-rw-r--r--actionpack/lib/abstract_controller/collector.rb8
-rw-r--r--actionpack/lib/abstract_controller/error.rb2
-rw-r--r--actionpack/lib/abstract_controller/helpers.rb49
-rw-r--r--actionpack/lib/abstract_controller/logger.rb2
-rw-r--r--actionpack/lib/abstract_controller/railties/routes_helpers.rb4
-rw-r--r--actionpack/lib/abstract_controller/rendering.rb47
-rw-r--r--actionpack/lib/abstract_controller/translation.rb9
-rw-r--r--actionpack/lib/abstract_controller/url_for.rb2
-rw-r--r--actionpack/lib/action_controller.rb32
-rw-r--r--actionpack/lib/action_controller/api.rb50
-rw-r--r--actionpack/lib/action_controller/api/api_rendering.rb2
-rw-r--r--actionpack/lib/action_controller/base.rb37
-rw-r--r--actionpack/lib/action_controller/caching.rb2
-rw-r--r--actionpack/lib/action_controller/form_builder.rb2
-rw-r--r--actionpack/lib/action_controller/log_subscriber.rb21
-rw-r--r--actionpack/lib/action_controller/metal.rb108
-rw-r--r--actionpack/lib/action_controller/metal/basic_implicit_render.rb4
-rw-r--r--actionpack/lib/action_controller/metal/conditional_get.rb32
-rw-r--r--actionpack/lib/action_controller/metal/content_security_policy.rb52
-rw-r--r--actionpack/lib/action_controller/metal/cookies.rb4
-rw-r--r--actionpack/lib/action_controller/metal/data_streaming.rb54
-rw-r--r--actionpack/lib/action_controller/metal/default_headers.rb17
-rw-r--r--actionpack/lib/action_controller/metal/etag_with_flash.rb18
-rw-r--r--actionpack/lib/action_controller/metal/etag_with_template_digest.rb31
-rw-r--r--actionpack/lib/action_controller/metal/exceptions.rb46
-rw-r--r--actionpack/lib/action_controller/metal/flash.rb17
-rw-r--r--actionpack/lib/action_controller/metal/force_ssl.rb85
-rw-r--r--actionpack/lib/action_controller/metal/head.rb36
-rw-r--r--actionpack/lib/action_controller/metal/helpers.rb31
-rw-r--r--actionpack/lib/action_controller/metal/http_authentication.rb96
-rw-r--r--actionpack/lib/action_controller/metal/implicit_render.rb31
-rw-r--r--actionpack/lib/action_controller/metal/instrumentation.rb48
-rw-r--r--actionpack/lib/action_controller/metal/live.rb118
-rw-r--r--actionpack/lib/action_controller/metal/mime_responds.rb27
-rw-r--r--actionpack/lib/action_controller/metal/parameter_encoding.rb51
-rw-r--r--actionpack/lib/action_controller/metal/params_wrapper.rb120
-rw-r--r--actionpack/lib/action_controller/metal/redirecting.rb80
-rw-r--r--actionpack/lib/action_controller/metal/renderers.rb16
-rw-r--r--actionpack/lib/action_controller/metal/rendering.rb114
-rw-r--r--actionpack/lib/action_controller/metal/request_forgery_protection.rb189
-rw-r--r--actionpack/lib/action_controller/metal/rescue.rb27
-rw-r--r--actionpack/lib/action_controller/metal/streaming.rb14
-rw-r--r--actionpack/lib/action_controller/metal/strong_parameters.rb573
-rw-r--r--actionpack/lib/action_controller/metal/testing.rb8
-rw-r--r--actionpack/lib/action_controller/metal/url_for.rb14
-rw-r--r--actionpack/lib/action_controller/railtie.rb38
-rw-r--r--actionpack/lib/action_controller/railties/helpers.rb4
-rw-r--r--actionpack/lib/action_controller/renderer.rb63
-rw-r--r--actionpack/lib/action_controller/template_assertions.rb2
-rw-r--r--actionpack/lib/action_controller/test_case.rb314
-rw-r--r--actionpack/lib/action_dispatch.rb45
-rw-r--r--actionpack/lib/action_dispatch/http/cache.rb67
-rw-r--r--actionpack/lib/action_dispatch/http/content_disposition.rb45
-rw-r--r--actionpack/lib/action_dispatch/http/content_security_policy.rb273
-rw-r--r--actionpack/lib/action_dispatch/http/filter_parameters.rb32
-rw-r--r--actionpack/lib/action_dispatch/http/filter_redirect.rb8
-rw-r--r--actionpack/lib/action_dispatch/http/headers.rb24
-rw-r--r--actionpack/lib/action_dispatch/http/mime_negotiation.rb67
-rw-r--r--actionpack/lib/action_dispatch/http/mime_type.rb113
-rw-r--r--actionpack/lib/action_dispatch/http/mime_types.rb21
-rw-r--r--actionpack/lib/action_dispatch/http/parameter_filter.rb86
-rw-r--r--actionpack/lib/action_dispatch/http/parameters.rb90
-rw-r--r--actionpack/lib/action_dispatch/http/rack_cache.rb2
-rw-r--r--actionpack/lib/action_dispatch/http/request.rb156
-rw-r--r--actionpack/lib/action_dispatch/http/response.rb121
-rw-r--r--actionpack/lib/action_dispatch/http/upload.rb28
-rw-r--r--actionpack/lib/action_dispatch/http/url.rb279
-rw-r--r--actionpack/lib/action_dispatch/journey.rb12
-rw-r--r--actionpack/lib/action_dispatch/journey/formatter.rb33
-rw-r--r--actionpack/lib/action_dispatch/journey/gtg/builder.rb12
-rw-r--r--actionpack/lib/action_dispatch/journey/gtg/simulator.rb12
-rw-r--r--actionpack/lib/action_dispatch/journey/gtg/transition_table.rb33
-rw-r--r--actionpack/lib/action_dispatch/journey/nfa/builder.rb8
-rw-r--r--actionpack/lib/action_dispatch/journey/nfa/dot.rb26
-rw-r--r--actionpack/lib/action_dispatch/journey/nfa/simulator.rb6
-rw-r--r--actionpack/lib/action_dispatch/journey/nfa/transition_table.rb6
-rw-r--r--actionpack/lib/action_dispatch/journey/nodes/node.rb25
-rw-r--r--actionpack/lib/action_dispatch/journey/parser.rb45
-rw-r--r--actionpack/lib/action_dispatch/journey/parser.y5
-rw-r--r--actionpack/lib/action_dispatch/journey/parser_extras.rb12
-rw-r--r--actionpack/lib/action_dispatch/journey/path/pattern.rb21
-rw-r--r--actionpack/lib/action_dispatch/journey/route.rb73
-rw-r--r--actionpack/lib/action_dispatch/journey/router.rb47
-rw-r--r--actionpack/lib/action_dispatch/journey/router/utils.rb41
-rw-r--r--actionpack/lib/action_dispatch/journey/routes.rb5
-rw-r--r--actionpack/lib/action_dispatch/journey/scanner.rb44
-rw-r--r--actionpack/lib/action_dispatch/journey/visitors.rb54
-rw-r--r--actionpack/lib/action_dispatch/middleware/callbacks.rb18
-rw-r--r--actionpack/lib/action_dispatch/middleware/cookies.rb341
-rw-r--r--actionpack/lib/action_dispatch/middleware/debug_exceptions.rb256
-rw-r--r--actionpack/lib/action_dispatch/middleware/debug_locks.rb124
-rw-r--r--actionpack/lib/action_dispatch/middleware/debug_view.rb56
-rw-r--r--actionpack/lib/action_dispatch/middleware/exception_wrapper.rb161
-rw-r--r--actionpack/lib/action_dispatch/middleware/executor.rb4
-rw-r--r--actionpack/lib/action_dispatch/middleware/flash.rb39
-rw-r--r--actionpack/lib/action_dispatch/middleware/host_authorization.rb103
-rw-r--r--actionpack/lib/action_dispatch/middleware/params_parser.rb46
-rw-r--r--actionpack/lib/action_dispatch/middleware/public_exceptions.rb56
-rw-r--r--actionpack/lib/action_dispatch/middleware/reloader.rb52
-rw-r--r--actionpack/lib/action_dispatch/middleware/remote_ip.rb36
-rw-r--r--actionpack/lib/action_dispatch/middleware/request_id.rb15
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/abstract_store.rb44
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/cache_store.rb8
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/cookie_store.rb114
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb6
-rw-r--r--actionpack/lib/action_dispatch/middleware/show_exceptions.rb42
-rw-r--r--actionpack/lib/action_dispatch/middleware/ssl.rb135
-rw-r--r--actionpack/lib/action_dispatch/middleware/stack.rb62
-rw-r--r--actionpack/lib/action_dispatch/middleware/static.rb64
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/_source.html.erb6
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb80
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb7
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb5
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb22
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb24
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb15
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb1
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb19
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb3
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb4
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb2
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb4
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb11
-rw-r--r--actionpack/lib/action_dispatch/railtie.rb31
-rw-r--r--actionpack/lib/action_dispatch/request/session.rb71
-rw-r--r--actionpack/lib/action_dispatch/request/utils.rb23
-rw-r--r--actionpack/lib/action_dispatch/routing.rb55
-rw-r--r--actionpack/lib/action_dispatch/routing/endpoint.rb11
-rw-r--r--actionpack/lib/action_dispatch/routing/inspector.rb221
-rw-r--r--actionpack/lib/action_dispatch/routing/mapper.rb667
-rw-r--r--actionpack/lib/action_dispatch/routing/polymorphic_routes.rb301
-rw-r--r--actionpack/lib/action_dispatch/routing/redirection.rb56
-rw-r--r--actionpack/lib/action_dispatch/routing/route_set.rb344
-rw-r--r--actionpack/lib/action_dispatch/routing/routes_proxy.rb37
-rw-r--r--actionpack/lib/action_dispatch/routing/url_for.rb76
-rw-r--r--actionpack/lib/action_dispatch/system_test_case.rb167
-rw-r--r--actionpack/lib/action_dispatch/system_testing/browser.rb57
-rw-r--r--actionpack/lib/action_dispatch/system_testing/driver.rb66
-rw-r--r--actionpack/lib/action_dispatch/system_testing/server.rb31
-rw-r--r--actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb96
-rw-r--r--actionpack/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb31
-rw-r--r--actionpack/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb26
-rw-r--r--actionpack/lib/action_dispatch/testing/assertion_response.rb30
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions.rb10
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions/response.rb18
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions/routing.rb55
-rw-r--r--actionpack/lib/action_dispatch/testing/integration.rb492
-rw-r--r--actionpack/lib/action_dispatch/testing/request_encoder.rb55
-rw-r--r--actionpack/lib/action_dispatch/testing/test_process.rb41
-rw-r--r--actionpack/lib/action_dispatch/testing/test_request.rb38
-rw-r--r--actionpack/lib/action_dispatch/testing/test_response.rb21
-rw-r--r--actionpack/lib/action_pack.rb6
-rw-r--r--actionpack/lib/action_pack/gem_version.rb4
-rw-r--r--actionpack/lib/action_pack/version.rb4
160 files changed, 6098 insertions, 3946 deletions
diff --git a/actionpack/lib/abstract_controller.rb b/actionpack/lib/abstract_controller.rb
index 1e57cbaac4..3a98931167 100644
--- a/actionpack/lib/abstract_controller.rb
+++ b/actionpack/lib/abstract_controller.rb
@@ -1,10 +1,13 @@
-require 'action_pack'
-require 'active_support/rails'
-require 'active_support/i18n'
+# frozen_string_literal: true
+
+require "action_pack"
+require "active_support/rails"
+require "active_support/i18n"
module AbstractController
extend ActiveSupport::Autoload
+ autoload :ActionNotFound, "abstract_controller/base"
autoload :Base
autoload :Caching
autoload :Callbacks
diff --git a/actionpack/lib/abstract_controller/asset_paths.rb b/actionpack/lib/abstract_controller/asset_paths.rb
index e6170228d9..d6ee84b87b 100644
--- a/actionpack/lib/abstract_controller/asset_paths.rb
+++ b/actionpack/lib/abstract_controller/asset_paths.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module AbstractController
module AssetPaths #:nodoc:
extend ActiveSupport::Concern
diff --git a/actionpack/lib/abstract_controller/base.rb b/actionpack/lib/abstract_controller/base.rb
index 16dec31938..bb42f2e119 100644
--- a/actionpack/lib/abstract_controller/base.rb
+++ b/actionpack/lib/abstract_controller/base.rb
@@ -1,9 +1,10 @@
-require 'erubis'
-require 'abstract_controller/error'
-require 'active_support/configurable'
-require 'active_support/descendants_tracker'
-require 'active_support/core_ext/module/anonymous'
-require 'active_support/core_ext/module/attr_internal'
+# frozen_string_literal: true
+
+require "abstract_controller/error"
+require "active_support/configurable"
+require "active_support/descendants_tracker"
+require "active_support/core_ext/module/anonymous"
+require "active_support/core_ext/module/attr_internal"
module AbstractController
# Raised when a non-existing controller action is triggered.
@@ -15,14 +16,21 @@ module AbstractController
# expected to provide their own +render+ method, since rendering means
# different things depending on the context.
class Base
+ ##
+ # Returns the body of the HTTP response sent by the controller.
attr_internal :response_body
+
+ ##
+ # Returns the name of the action this controller is processing.
attr_internal :action_name
+
+ ##
+ # Returns the formats that can be processed by the controller.
attr_internal :formats
include ActiveSupport::Configurable
extend ActiveSupport::DescendantsTracker
- undef_method :not_implemented
class << self
attr_reader :abstract
alias_method :abstract?, :abstract
@@ -70,13 +78,15 @@ module AbstractController
# 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)).uniq.map(&:to_s)
+ public_instance_methods(false))
+
+ methods.map!(&:to_s)
methods.to_set
end
end
- # action_methods are cached and there is sometimes need to refresh
+ # 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.
def clear_action_methods!
@@ -94,7 +104,7 @@ module AbstractController
# ==== Returns
# * <tt>String</tt>
def controller_path
- @controller_path ||= name.sub(/Controller$/, ''.freeze).underscore unless anonymous?
+ @controller_path ||= name.sub(/Controller$/, "").underscore unless anonymous?
end
# Refresh the cached action_methods when a new action_method is added.
@@ -150,6 +160,13 @@ module AbstractController
_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+.
+ 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
@@ -165,8 +182,6 @@ module AbstractController
#
# ==== Parameters
# * <tt>name</tt> - The name of an action to be tested
- #
- # :api: private
def action_method?(name)
self.class.action_methods.include?(name)
end
@@ -208,7 +223,7 @@ module AbstractController
# ==== Returns
# * <tt>string</tt> - The name of the method that handles the action
# * false - No valid method name could be found.
- # Raise AbstractController::ActionNotFound.
+ # Raise +AbstractController::ActionNotFound+.
def _find_action_name(action_name)
_valid_action_name?(action_name) && method_for_action(action_name)
end
@@ -224,11 +239,11 @@ module AbstractController
# 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
+ # 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.
+ # 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
diff --git a/actionpack/lib/abstract_controller/caching.rb b/actionpack/lib/abstract_controller/caching.rb
index 0dea50889a..ce6b757c3c 100644
--- a/actionpack/lib/abstract_controller/caching.rb
+++ b/actionpack/lib/abstract_controller/caching.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module AbstractController
module Caching
extend ActiveSupport::Concern
@@ -29,13 +31,15 @@ module AbstractController
extend ConfigMethods
config_accessor :default_static_extension
- self.default_static_extension ||= '.html'
+ self.default_static_extension ||= ".html"
config_accessor :perform_caching
self.perform_caching = true if perform_caching.nil?
- class_attribute :_view_cache_dependencies
- self._view_cache_dependencies = []
+ config_accessor :enable_fragment_cache_logging
+ self.enable_fragment_cache_logging = false
+
+ class_attribute :_view_cache_dependencies, default: []
helper_method :view_cache_dependencies if respond_to?(:helper_method)
end
@@ -49,9 +53,9 @@ module AbstractController
self.class._view_cache_dependencies.map { |dep| instance_exec(&dep) }.compact
end
- protected
+ private
# Convenience accessor.
- def cache(key, options = {}, &block)
+ def cache(key, options = {}, &block) # :doc:
if cache_configured?
cache_store.fetch(ActiveSupport::Cache.expand_cache_key(key, :controller), options, &block)
else
diff --git a/actionpack/lib/abstract_controller/caching/fragments.rb b/actionpack/lib/abstract_controller/caching/fragments.rb
index 3257a731ed..18677ddd18 100644
--- a/actionpack/lib/abstract_controller/caching/fragments.rb
+++ b/actionpack/lib/abstract_controller/caching/fragments.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module AbstractController
module Caching
# Fragment caching is used for caching various blocks within
@@ -25,7 +27,9 @@ module AbstractController
self.fragment_cache_keys = []
- helper_method :fragment_cache_key if respond_to?(:helper_method)
+ if respond_to?(:helper_method)
+ helper_method :combined_fragment_cache_key
+ end
end
module ClassMethods
@@ -51,20 +55,24 @@ module AbstractController
# end
# end
def fragment_cache_key(value = nil, &key)
- self.fragment_cache_keys += [key || ->{ value }]
+ self.fragment_cache_keys += [key || -> { value }]
end
end
# Given a key (as described in +expire_fragment+), returns
- # a key suitable for use in reading, writing, or expiring a
- # cached fragment. All keys begin with <tt>views/</tt>,
+ # 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. The key is expanded using
- # ActiveSupport::Cache.expand_cache_key.
- def fragment_cache_key(key)
+ # with the specified +key+ value.
+ 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
- ActiveSupport::Cache.expand_cache_key([*head, *tail], :views)
+
+ 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
@@ -72,7 +80,7 @@ module AbstractController
def write_fragment(key, content, options = nil)
return content unless cache_configured?
- key = fragment_cache_key(key)
+ key = combined_fragment_cache_key(key)
instrument_fragment_cache :write_fragment, key do
content = content.to_str
cache_store.write(key, content, options)
@@ -85,7 +93,7 @@ module AbstractController
def read_fragment(key, options = nil)
return unless cache_configured?
- key = fragment_cache_key(key)
+ 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
@@ -96,7 +104,7 @@ module AbstractController
# +key+ exists (see +expire_fragment+ for acceptable formats).
def fragment_exist?(key, options = nil)
return unless cache_configured?
- key = fragment_cache_key(key)
+ key = combined_fragment_cache_key(key)
instrument_fragment_cache :exist_fragment?, key do
cache_store.exist?(key, options)
@@ -123,7 +131,7 @@ module AbstractController
# method (or <tt>delete_matched</tt>, for Regexp keys).
def expire_fragment(key, options = nil)
return unless cache_configured?
- key = fragment_cache_key(key) unless key.is_a?(Regexp)
+ key = combined_fragment_cache_key(key) unless key.is_a?(Regexp)
instrument_fragment_cache :expire_fragment, key do
if key.is_a?(Regexp)
@@ -135,8 +143,7 @@ module AbstractController
end
def instrument_fragment_cache(name, key) # :nodoc:
- payload = instrument_payload(key)
- ActiveSupport::Notifications.instrument("#{name}.#{instrument_name}", payload) { yield }
+ ActiveSupport::Notifications.instrument("#{name}.#{instrument_name}", instrument_payload(key)) { yield }
end
end
end
diff --git a/actionpack/lib/abstract_controller/callbacks.rb b/actionpack/lib/abstract_controller/callbacks.rb
index d63ce9c1c3..42bab411d2 100644
--- a/actionpack/lib/abstract_controller/callbacks.rb
+++ b/actionpack/lib/abstract_controller/callbacks.rb
@@ -1,4 +1,26 @@
+# frozen_string_literal: true
+
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.
+ #
module Callbacks
extend ActiveSupport::Concern
@@ -9,12 +31,12 @@ module AbstractController
included do
define_callbacks :process_action,
- terminator: ->(controller, result_lambda) { result_lambda.call if result_lambda.is_a?(Proc); controller.response_body },
+ terminator: ->(controller, result_lambda) { result_lambda.call; controller.performed? },
skip_after_callbacks_if_terminated: true
end
- # Override AbstractController::Base's process_action to run the
- # process_action callbacks around the normal behavior.
+ # Override <tt>AbstractController::Base#process_action</tt> to run the
+ # <tt>process_action</tt> callbacks around the normal behavior.
def process_action(*args)
run_callbacks(:process_action) do
super
@@ -49,30 +71,11 @@ module AbstractController
def _normalize_callback_option(options, from, to) # :nodoc:
if from = options[from]
_from = Array(from).map(&:to_s).to_set
- from = proc {|c| _from.include? c.action_name }
+ from = proc { |c| _from.include? c.action_name }
options[to] = Array(options[to]).unshift(from)
end
end
- # Skip before, after, and around action callbacks matching any of the names.
- #
- # ==== Parameters
- # * <tt>names</tt> - A list of valid names that could be used for
- # callbacks. Note that skipping uses Ruby equality, so it's
- # impossible to skip a callback defined using an anonymous proc
- # using #skip_action_callback.
- def skip_action_callback(*names)
- ActiveSupport::Deprecation.warn('`skip_action_callback` is deprecated and will be removed in Rails 5.1. Please use skip_before_action, skip_after_action or skip_around_action instead.')
- skip_before_action(*names, raise: false)
- skip_after_action(*names, raise: false)
- skip_around_action(*names, raise: false)
- end
-
- def skip_filter(*names)
- ActiveSupport::Deprecation.warn("`skip_filter` is deprecated and will be removed in Rails 5.1. Use skip_before_action, skip_after_action or skip_around_action instead.")
- skip_action_callback(*names)
- 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.
@@ -100,6 +103,10 @@ module AbstractController
# :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
@@ -107,6 +114,10 @@ module AbstractController
# :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
@@ -121,6 +132,10 @@ module AbstractController
# :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
@@ -187,22 +202,12 @@ module AbstractController
end
end
- define_method "#{callback}_filter" do |*names, &blk|
- ActiveSupport::Deprecation.warn("#{callback}_filter is deprecated and will be removed in Rails 5.1. Use #{callback}_action instead.")
- send("#{callback}_action", *names, &blk)
- end
-
define_method "prepend_#{callback}_action" do |*names, &blk|
_insert_callbacks(names, blk) do |name, options|
- set_callback(:process_action, callback, name, options.merge(:prepend => true))
+ set_callback(:process_action, callback, name, options.merge(prepend: true))
end
end
- define_method "prepend_#{callback}_filter" do |*names, &blk|
- ActiveSupport::Deprecation.warn("prepend_#{callback}_filter is deprecated and will be removed in Rails 5.1. Use prepend_#{callback}_action instead.")
- send("prepend_#{callback}_action", *names, &blk)
- end
-
# Skip a before, after or around callback. See _insert_callbacks
# for details on the allowed parameters.
define_method "skip_#{callback}_action" do |*names|
@@ -211,18 +216,8 @@ module AbstractController
end
end
- define_method "skip_#{callback}_filter" do |*names, &blk|
- ActiveSupport::Deprecation.warn("skip_#{callback}_filter is deprecated and will be removed in Rails 5.1. Use skip_#{callback}_action instead.")
- send("skip_#{callback}_action", *names, &blk)
- end
-
# *_action is the same as append_*_action
alias_method :"append_#{callback}_action", :"#{callback}_action"
-
- define_method "append_#{callback}_filter" do |*names, &blk|
- ActiveSupport::Deprecation.warn("append_#{callback}_filter is deprecated and will be removed in Rails 5.1. Use append_#{callback}_action instead.")
- send("append_#{callback}_action", *names, &blk)
- end
end
end
end
diff --git a/actionpack/lib/abstract_controller/collector.rb b/actionpack/lib/abstract_controller/collector.rb
index 55654be224..d4a078ab32 100644
--- a/actionpack/lib/abstract_controller/collector.rb
+++ b/actionpack/lib/abstract_controller/collector.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "action_dispatch/http/mime_type"
module AbstractController
@@ -16,15 +18,15 @@ module AbstractController
end
Mime::Type.register_callback do |mime|
- generate_method_for_mime(mime) unless self.instance_methods.include?(mime.to_sym)
+ generate_method_for_mime(mime) unless instance_methods.include?(mime.to_sym)
end
- protected
+ private
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: " \
- "http://guides.rubyonrails.org/action_controller_overview.html#restful-downloads. " \
+ "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 { ... } }"
diff --git a/actionpack/lib/abstract_controller/error.rb b/actionpack/lib/abstract_controller/error.rb
index 7fafce4dd4..89a54f072e 100644
--- a/actionpack/lib/abstract_controller/error.rb
+++ b/actionpack/lib/abstract_controller/error.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module AbstractController
class Error < StandardError #:nodoc:
end
diff --git a/actionpack/lib/abstract_controller/helpers.rb b/actionpack/lib/abstract_controller/helpers.rb
index ab4355296b..3913259ecc 100644
--- a/actionpack/lib/abstract_controller/helpers.rb
+++ b/actionpack/lib/abstract_controller/helpers.rb
@@ -1,15 +1,14 @@
-require 'active_support/dependencies'
+# frozen_string_literal: true
+
+require "active_support/dependencies"
module AbstractController
module Helpers
extend ActiveSupport::Concern
included do
- class_attribute :_helpers
- self._helpers = Module.new
-
- class_attribute :_helper_methods
- self._helper_methods = Array.new
+ class_attribute :_helpers, default: Module.new
+ class_attribute :_helper_methods, default: Array.new
end
class MissingHelperError < LoadError
@@ -18,7 +17,7 @@ module AbstractController
@path = "helpers/#{path}.rb"
set_backtrace error.backtrace
- if error.path =~ /^#{path}(\.rb)?$/
+ if /^#{path}(\.rb)?$/.match?(error.path)
super("Missing helper file helpers/%s.rb" % path)
else
raise error
@@ -171,25 +170,25 @@ module AbstractController
end
private
- # Makes all the (instance) methods in the helper module available to templates
- # rendered through this controller.
- #
- # ==== Parameters
- # * <tt>module</tt> - The module to include into the current helper module
- # for the class
- def add_template_helper(mod)
- _helpers.module_eval { include mod }
- end
+ # Makes all the (instance) methods in the helper module available to templates
+ # rendered through this controller.
+ #
+ # ==== Parameters
+ # * <tt>module</tt> - The module to include into the current helper module
+ # for the class
+ def add_template_helper(mod)
+ _helpers.module_eval { include mod }
+ end
- def default_helper_module!
- module_name = name.sub(/Controller$/, ''.freeze)
- module_path = module_name.underscore
- helper module_path
- rescue LoadError => e
- raise e unless e.is_missing? "helpers/#{module_path}_helper"
- rescue NameError => e
- raise e unless e.missing_name? "#{module_name}Helper"
- end
+ def default_helper_module!
+ module_name = name.sub(/Controller$/, "")
+ module_path = module_name.underscore
+ helper module_path
+ rescue LoadError => e
+ raise e unless e.is_missing? "helpers/#{module_path}_helper"
+ rescue NameError => e
+ raise e unless e.missing_name? "#{module_name}Helper"
+ end
end
end
end
diff --git a/actionpack/lib/abstract_controller/logger.rb b/actionpack/lib/abstract_controller/logger.rb
index c31ea6c5b5..8d0acc1b5c 100644
--- a/actionpack/lib/abstract_controller/logger.rb
+++ b/actionpack/lib/abstract_controller/logger.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "active_support/benchmarkable"
module AbstractController
diff --git a/actionpack/lib/abstract_controller/railties/routes_helpers.rb b/actionpack/lib/abstract_controller/railties/routes_helpers.rb
index 14b574e322..fbd93705ed 100644
--- a/actionpack/lib/abstract_controller/railties/routes_helpers.rb
+++ b/actionpack/lib/abstract_controller/railties/routes_helpers.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module AbstractController
module Railties
module RoutesHelpers
@@ -5,7 +7,7 @@ module AbstractController
Module.new do
define_method(:inherited) do |klass|
super(klass)
- if namespace = klass.parents.detect { |m| m.respond_to?(:railtie_routes_url_helpers) }
+ if namespace = klass.module_parents.detect { |m| m.respond_to?(:railtie_routes_url_helpers) }
klass.include(namespace.railtie_routes_url_helpers(include_path_helpers))
else
klass.include(routes.url_helpers(include_path_helpers))
diff --git a/actionpack/lib/abstract_controller/rendering.rb b/actionpack/lib/abstract_controller/rendering.rb
index 9f192c54f7..8ba2b25552 100644
--- a/actionpack/lib/abstract_controller/rendering.rb
+++ b/actionpack/lib/abstract_controller/rendering.rb
@@ -1,9 +1,9 @@
-require 'abstract_controller/error'
-require 'active_support/concern'
-require 'active_support/core_ext/class/attribute'
-require 'action_view'
-require 'action_view/view_paths'
-require 'set'
+# frozen_string_literal: true
+
+require "abstract_controller/error"
+require "action_view"
+require "action_view/view_paths"
+require "set"
module AbstractController
class DoubleRenderError < Error
@@ -20,7 +20,6 @@ module AbstractController
# Normalizes arguments, options and then delegates render_to_body and
# sticks the result in <tt>self.response_body</tt>.
- # :api: public
def render(*args, &block)
options = _normalize_render(*args, &block)
rendered_body = render_to_body(options)
@@ -42,32 +41,26 @@ module AbstractController
# (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.
- # :api: plugin
def render_to_string(*args, &block)
options = _normalize_render(*args, &block)
render_to_body(options)
end
# Performs the actual template rendering.
- # :api: public
def render_to_body(options = {})
end
- # Returns Content-Type of rendered content
- # :api: public
+ # Returns Content-Type of rendered content.
def rendered_format
Mime[:text]
end
DEFAULT_PROTECTED_INSTANCE_VARIABLES = Set.new %i(
- @_action_name @_response_body @_formats @_prefixes @_config
- @_view_context_class @_view_renderer @_lookup_context
- @_routes @_db_runtime
+ @_action_name @_response_body @_formats @_prefixes
)
# This method should return a hash with assigns.
# You can overwrite this configuration per controller.
- # :api: public
def view_assigns
protected_vars = _protected_ivars
variables = instance_variables
@@ -78,11 +71,11 @@ module AbstractController
}
end
+ 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>.
- # :api: plugin
- def _normalize_args(action=nil, options={})
+ def _normalize_args(action = nil, options = {}) # :doc:
if action.respond_to?(:permitted?)
if action.permitted?
action
@@ -97,20 +90,20 @@ module AbstractController
end
# Normalize options.
- # :api: plugin
- def _normalize_options(options)
+ def _normalize_options(options) # :doc:
options
end
# Process extra options.
- # :api: plugin
- def _process_options(options)
+ def _process_options(options) # :doc:
options
end
# Process the rendered format.
- # :api: private
- def _process_format(format)
+ def _process_format(format) # :nodoc:
+ end
+
+ def _process_variant(options)
end
def _set_html_content_type # :nodoc:
@@ -120,13 +113,9 @@ module AbstractController
end
# Normalize args and options.
- # :api: private
- def _normalize_render(*args, &block)
+ def _normalize_render(*args, &block) # :nodoc:
options = _normalize_args(*args, &block)
- #TODO: remove defined? when we restore AP <=> AV dependency
- if defined?(request) && request.variant.present?
- options[:variant] = request.variant
- end
+ _process_variant(options)
_normalize_options(options)
options
end
diff --git a/actionpack/lib/abstract_controller/translation.rb b/actionpack/lib/abstract_controller/translation.rb
index 56b8ce895e..4dad2a2b93 100644
--- a/actionpack/lib/abstract_controller/translation.rb
+++ b/actionpack/lib/abstract_controller/translation.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module AbstractController
module Translation
# Delegates to <tt>I18n.translate</tt>. Also aliased as <tt>t</tt>.
@@ -9,11 +11,12 @@ module AbstractController
# to translate many keys within the same controller / action and gives you a
# simple framework for scoping them consistently.
def translate(key, options = {})
- if key.to_s.first == '.'
- path = controller_path.tr('/', '.')
+ options = options.dup
+ if key.to_s.first == "."
+ path = controller_path.tr("/", ".")
defaults = [:"#{path}#{key}"]
defaults << options[:default] if options[:default]
- options[:default] = defaults
+ options[:default] = defaults.flatten
key = "#{path}.#{action_name}#{key}"
end
I18n.translate(key, options)
diff --git a/actionpack/lib/abstract_controller/url_for.rb b/actionpack/lib/abstract_controller/url_for.rb
index 72d07b0927..bd74c27d3b 100644
--- a/actionpack/lib/abstract_controller/url_for.rb
+++ b/actionpack/lib/abstract_controller/url_for.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
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
diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb
index 62f5905205..29d61c3ceb 100644
--- a/actionpack/lib/action_controller.rb
+++ b/actionpack/lib/action_controller.rb
@@ -1,8 +1,10 @@
-require 'active_support/rails'
-require 'abstract_controller'
-require 'action_dispatch'
-require 'action_controller/metal/live'
-require 'action_controller/metal/strong_parameters'
+# frozen_string_literal: true
+
+require "active_support/rails"
+require "abstract_controller"
+require "action_dispatch"
+require "action_controller/metal/live"
+require "action_controller/metal/strong_parameters"
module ActionController
extend ActiveSupport::Autoload
@@ -20,9 +22,12 @@ module ActionController
autoload_under "metal" do
autoload :ConditionalGet
+ autoload :ContentSecurityPolicy
autoload :Cookies
autoload :DataStreaming
+ autoload :DefaultHeaders
autoload :EtagWithTemplateDigest
+ autoload :EtagWithFlash
autoload :Flash
autoload :ForceSSL
autoload :Head
@@ -40,6 +45,7 @@ module ActionController
autoload :Rescue
autoload :Streaming
autoload :StrongParameters
+ autoload :ParameterEncoding
autoload :Testing
autoload :UrlFor
end
@@ -48,14 +54,14 @@ module ActionController
autoload :ApiRendering
end
- autoload :TestCase, 'action_controller/test_case'
- autoload :TemplateAssertions, 'action_controller/test_case'
+ autoload :TestCase, "action_controller/test_case"
+ autoload :TemplateAssertions, "action_controller/test_case"
end
# Common Active Support usage in Action Controller
-require 'active_support/core_ext/module/attribute_accessors'
-require 'active_support/core_ext/load_error'
-require 'active_support/core_ext/module/attr_internal'
-require 'active_support/core_ext/name_error'
-require 'active_support/core_ext/uri'
-require 'active_support/inflector'
+require "active_support/core_ext/module/attribute_accessors"
+require "active_support/core_ext/load_error"
+require "active_support/core_ext/module/attr_internal"
+require "active_support/core_ext/name_error"
+require "active_support/core_ext/uri"
+require "active_support/inflector"
diff --git a/actionpack/lib/action_controller/api.rb b/actionpack/lib/action_controller/api.rb
index ff12705abe..c276ee57c0 100644
--- a/actionpack/lib/action_controller/api.rb
+++ b/actionpack/lib/action_controller/api.rb
@@ -1,6 +1,8 @@
-require 'action_view'
-require 'action_controller'
-require 'action_controller/log_subscriber'
+# frozen_string_literal: true
+
+require "action_view"
+require "action_controller"
+require "action_controller/log_subscriber"
module ActionController
# API Controller is a lightweight version of <tt>ActionController::Base</tt>,
@@ -10,26 +12,26 @@ module ActionController
#
# 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, cookies, sessions,
+ # 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.
+ # your application, they're just not part of the default API controller stack.
#
- # By default, only the ApplicationController in a \Rails application inherits
- # from <tt>ActionController::API</tt>. All other controllers in turn inherit
- # from ApplicationController.
+ # 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
+ # posts = Post.all
+ # render json: posts
# end
# end
#
- # Request, response and parameters objects all work the exact same way as
+ # Request, response, and parameters objects all work the exact same way as
# <tt>ActionController::Base</tt>.
#
# == Renders
@@ -37,18 +39,18 @@ module ActionController
# 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</tt> in
- # all actions, otherwise it will return 204 No Content response.
+ # 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
+ # 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</tt> method in your controllers in the same way as
+ # <tt>redirect_to</tt> method in your controllers in the same way as in
# <tt>ActionController::Base</tt>. For example:
#
# def create
@@ -56,7 +58,7 @@ module ActionController
# # do stuff here
# end
#
- # == Adding new behavior
+ # == 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
@@ -72,18 +74,18 @@ module ActionController
#
# class PostsController < ApplicationController
# def index
- # @posts = Post.all
+ # posts = Post.all
#
# respond_to do |format|
- # format.json { render json: @posts }
- # format.xml { render xml: @posts }
+ # format.json { render json: posts }
+ # format.xml { render xml: posts }
# end
# end
# end
#
- # Quite straightforward. Make sure to check <tt>ActionController::Base</tt>
- # available modules if you want to include any other functionality that is
- # not provided by <tt>ActionController::API</tt> out of the box.
+ # 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.
class API < Metal
abstract!
@@ -120,6 +122,7 @@ module ActionController
ForceSSL,
DataStreaming,
+ DefaultHeaders,
# Before callbacks should also be executed as early as possible, so
# also include them at the bottom.
@@ -141,6 +144,7 @@ module ActionController
include mod
end
+ ActiveSupport.run_load_hooks(:action_controller_api, self)
ActiveSupport.run_load_hooks(:action_controller, self)
end
end
diff --git a/actionpack/lib/action_controller/api/api_rendering.rb b/actionpack/lib/action_controller/api/api_rendering.rb
index 3a08d28c39..aca5265313 100644
--- a/actionpack/lib/action_controller/api/api_rendering.rb
+++ b/actionpack/lib/action_controller/api/api_rendering.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionController
module ApiRendering
extend ActiveSupport::Concern
diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb
index 04e5922ce8..2e565d5d44 100644
--- a/actionpack/lib/action_controller/base.rb
+++ b/actionpack/lib/action_controller/base.rb
@@ -1,4 +1,6 @@
-require 'action_view'
+# frozen_string_literal: true
+
+require "action_view"
require "action_controller/log_subscriber"
require "action_controller/metal/params_wrapper"
@@ -8,7 +10,7 @@ module ActionController
# 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 in turn inherit from ApplicationController. This gives you one class to configure things such as
+ # 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:
@@ -30,9 +32,9 @@ module ActionController
#
# Unlike index, the create action will not render a template. After performing its main purpose (creating a
# new post), it initiates a redirect instead. This redirect works by returning an external
- # "302 Moved" HTTP response that takes the user to the index action.
+ # <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.
+ # 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
@@ -51,16 +53,16 @@ module ActionController
# == 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 params 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 params.
+ # 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 stemming 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 params would have included
+ # 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
@@ -74,9 +76,9 @@ module ActionController
#
# session[:person] = Person.authenticate(user_name, password)
#
- # And retrieved again through the same hash:
+ # You can retrieve it again through the same hash:
#
- # Hello #{session[:person]}
+ # "Hello #{session[:person]}"
#
# For removing objects from the session, you can either assign a single key to +nil+:
#
@@ -213,23 +215,26 @@ module ActionController
Renderers::All,
ConditionalGet,
EtagWithTemplateDigest,
+ EtagWithFlash,
Caching,
MimeResponds,
ImplicitRender,
StrongParameters,
-
+ ParameterEncoding,
Cookies,
Flash,
FormBuilder,
RequestForgeryProtection,
+ ContentSecurityPolicy,
ForceSSL,
Streaming,
DataStreaming,
HttpAuthentication::Basic::ControllerMethods,
HttpAuthentication::Digest::ControllerMethods,
HttpAuthentication::Token::ControllerMethods,
+ DefaultHeaders,
- # Before callbacks should also be executed the earliest as possible, so
+ # Before callbacks should also be executed as early as possible, so
# also include them at the bottom.
AbstractController::Callbacks,
@@ -251,14 +256,16 @@ module ActionController
setup_renderer!
# Define some internal variables that should not be propagated to the view.
- PROTECTED_IVARS = AbstractController::Rendering::DEFAULT_PROTECTED_INSTANCE_VARIABLES + [
- :@_params, :@_response, :@_request,
- :@_view_runtime, :@_stream, :@_url_options, :@_action_has_layout ]
+ 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
+ )
def _protected_ivars # :nodoc:
PROTECTED_IVARS
end
+ ActiveSupport.run_load_hooks(:action_controller_base, self)
ActiveSupport.run_load_hooks(:action_controller, self)
end
end
diff --git a/actionpack/lib/action_controller/caching.rb b/actionpack/lib/action_controller/caching.rb
index a9a8508abc..bf3b00a7b7 100644
--- a/actionpack/lib/action_controller/caching.rb
+++ b/actionpack/lib/action_controller/caching.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
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.
diff --git a/actionpack/lib/action_controller/form_builder.rb b/actionpack/lib/action_controller/form_builder.rb
index f2656ca894..09d2ac1837 100644
--- a/actionpack/lib/action_controller/form_builder.rb
+++ b/actionpack/lib/action_controller/form_builder.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionController
# Override the default form builder for all views rendered by this
# controller and any of its descendants. Accepts a subclass of
diff --git a/actionpack/lib/action_controller/log_subscriber.rb b/actionpack/lib/action_controller/log_subscriber.rb
index a0917b4fdb..d8b04d8ddb 100644
--- a/actionpack/lib/action_controller/log_subscriber.rb
+++ b/actionpack/lib/action_controller/log_subscriber.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionController
class LogSubscriber < ActiveSupport::LogSubscriber
INTERNAL_PARAMS = %w(controller action format _method only_path)
@@ -16,16 +18,19 @@ module ActionController
def process_action(event)
info do
- payload = event.payload
+ payload = event.payload
additions = ActionController::Base.log_process_action(payload)
-
status = payload[:status]
+
if status.nil? && payload[:exception].present?
exception_class_name = payload[:exception].first
status = ActionDispatch::ExceptionWrapper.status_code_for_exception(exception_class_name)
end
- message = "Completed #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]} in #{event.duration.round}ms"
- message << " (#{additions.join(" | ".freeze)})" unless additions.empty?
+
+ additions << "Allocations: #{event.allocations}"
+
+ message = +"Completed #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]} in #{event.duration.round}ms"
+ message << " (#{additions.join(" | ")})" unless additions.empty?
message << "\n\n" if defined?(Rails.env) && Rails.env.development?
message
@@ -51,7 +56,7 @@ module ActionController
def unpermitted_parameters(event)
debug do
unpermitted_keys = event.payload[:keys]
- "Unpermitted parameter#{'s' if unpermitted_keys.size > 1}: #{unpermitted_keys.join(", ")}"
+ color("Unpermitted parameter#{'s' if unpermitted_keys.size > 1}: #{unpermitted_keys.map { |e| ":#{e}" }.join(", ")}", RED)
end
end
@@ -59,10 +64,10 @@ module ActionController
expire_fragment expire_page write_page).each do |method|
class_eval <<-METHOD, __FILE__, __LINE__ + 1
def #{method}(event)
- return unless logger.info?
- key_or_path = event.payload[:key] || event.payload[:path]
+ 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_or_path} (\#{event.duration.round(1)}ms)")
+ info("\#{human_name} \#{key} (\#{event.duration.round(1)}ms)")
end
METHOD
end
diff --git a/actionpack/lib/action_controller/metal.rb b/actionpack/lib/action_controller/metal.rb
index f6e67b02d7..b9088e6d86 100644
--- a/actionpack/lib/action_controller/metal.rb
+++ b/actionpack/lib/action_controller/metal.rb
@@ -1,7 +1,9 @@
-require 'active_support/core_ext/array/extract_options'
-require 'action_dispatch/middleware/stack'
-require 'action_dispatch/http/request'
-require 'action_dispatch/http/response'
+# frozen_string_literal: true
+
+require "active_support/core_ext/array/extract_options"
+require "action_dispatch/middleware/stack"
+require "action_dispatch/http/request"
+require "action_dispatch/http/response"
module ActionController
# Extend ActionDispatch middleware stack to make it aware of options
@@ -24,39 +26,39 @@ module ActionController
end
end
- def build(action, app = Proc.new)
+ def build(action, app = nil, &block)
action = action.to_s
- middlewares.reverse.inject(app) do |a, middleware|
+ middlewares.reverse.inject(app || block) do |a, middleware|
middleware.valid?(action) ? middleware.build(a) : a
end
end
private
- INCLUDE = ->(list, action) { list.include? action }
- EXCLUDE = ->(list, action) { !list.include? action }
- NULL = ->(list, action) { true }
-
- def build_middleware(klass, args, block)
- options = args.extract_options!
- only = Array(options.delete(:only)).map(&:to_s)
- except = Array(options.delete(:except)).map(&:to_s)
- args << options unless options.empty?
-
- strategy = NULL
- list = nil
-
- if only.any?
- strategy = INCLUDE
- list = only
- elsif except.any?
- strategy = EXCLUDE
- list = except
- end
+ INCLUDE = ->(list, action) { list.include? action }
+ EXCLUDE = ->(list, action) { !list.include? action }
+ NULL = ->(list, action) { true }
+
+ def build_middleware(klass, args, block)
+ options = args.extract_options!
+ only = Array(options.delete(:only)).map(&:to_s)
+ except = Array(options.delete(:except)).map(&:to_s)
+ args << options unless options.empty?
+
+ strategy = NULL
+ list = nil
+
+ if only.any?
+ strategy = INCLUDE
+ list = only
+ elsif except.any?
+ strategy = EXCLUDE
+ list = except
+ end
- Middleware.new(get_class(klass), args, list, strategy, block)
- end
+ Middleware.new(klass, args, list, strategy, block)
+ end
end
# <tt>ActionController::Metal</tt> is the simplest possible controller, providing a
@@ -118,11 +120,6 @@ module ActionController
class Metal < AbstractController::Base
abstract!
- def env
- @_request.env
- end
- deprecate :env
-
# 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.
@@ -130,24 +127,28 @@ module ActionController
# ==== Returns
# * <tt>string</tt>
def self.controller_name
- @controller_name ||= name.demodulize.sub(/Controller$/, '').underscore
+ @controller_name ||= name.demodulize.sub(/Controller$/, "").underscore
end
def self.make_response!(request)
- ActionDispatch::Response.create.tap do |res|
+ ActionDispatch::Response.new.tap do |res|
res.request = request
end
end
- # Delegates to the class' <tt>controller_name</tt>
+ def self.binary_params_for?(action) # :nodoc:
+ false
+ end
+
+ # Delegates to the class' <tt>controller_name</tt>.
def controller_name
self.class.controller_name
end
attr_internal :response, :request
- delegate :session, :to => "@_request"
+ delegate :session, to: "@_request"
delegate :headers, :status=, :location=, :content_type=,
- :status, :location, :content_type, :to => "@_response"
+ :status, :location, :content_type, to: "@_response"
def initialize
@_request = nil
@@ -209,8 +210,7 @@ module ActionController
@_request.reset_session
end
- class_attribute :middleware_stack
- self.middleware_stack = ActionController::MiddlewareStack.new
+ class_attribute :middleware_stack, default: ActionController::MiddlewareStack.new
def self.inherited(base) # :nodoc:
base.middleware_stack = middleware_stack.dup
@@ -228,32 +228,22 @@ module ActionController
middleware_stack
end
- # Makes the controller a Rack endpoint that runs the action in the given
- # +env+'s +action_dispatch.request.path_parameters+ key.
- def self.call(env)
- req = ActionDispatch::Request.new env
- action(req.path_parameters[:action]).call(env)
- end
- class << self; deprecate :call; end
-
# Returns a Rack endpoint for the given action name.
def self.action(name)
+ app = lambda { |env|
+ req = ActionDispatch::Request.new(env)
+ res = make_response! req
+ new.dispatch(name, req, res)
+ }
+
if middleware_stack.any?
- middleware_stack.build(name) do |env|
- req = ActionDispatch::Request.new(env)
- res = make_response! req
- new.dispatch(name, req, res)
- end
+ middleware_stack.build(name, app)
else
- lambda { |env|
- req = ActionDispatch::Request.new(env)
- res = make_response! req
- new.dispatch(name, req, res)
- }
+ app
end
end
- # Direct dispatch to the controller. Instantiates the controller, then
+ # Direct dispatch to the controller. Instantiates the controller, then
# executes the action named +name+.
def self.dispatch(name, req, res)
if middleware_stack.any?
diff --git a/actionpack/lib/action_controller/metal/basic_implicit_render.rb b/actionpack/lib/action_controller/metal/basic_implicit_render.rb
index cef65a362c..f9a758ff0e 100644
--- a/actionpack/lib/action_controller/metal/basic_implicit_render.rb
+++ b/actionpack/lib/action_controller/metal/basic_implicit_render.rb
@@ -1,10 +1,12 @@
+# frozen_string_literal: true
+
module ActionController
module BasicImplicitRender # :nodoc:
def send_action(method, *args)
super.tap { default_render unless performed? }
end
- def default_render(*args)
+ def default_render
head :no_content
end
end
diff --git a/actionpack/lib/action_controller/metal/conditional_get.rb b/actionpack/lib/action_controller/metal/conditional_get.rb
index 480e265e44..29d1919ec5 100644
--- a/actionpack/lib/action_controller/metal/conditional_get.rb
+++ b/actionpack/lib/action_controller/metal/conditional_get.rb
@@ -1,4 +1,4 @@
-require 'active_support/core_ext/hash/keys'
+# frozen_string_literal: true
module ActionController
module ConditionalGet
@@ -7,8 +7,7 @@ module ActionController
include Head
included do
- class_attribute :etaggers
- self.etaggers = []
+ class_attribute :etaggers, default: []
end
module ClassMethods
@@ -129,7 +128,7 @@ module ActionController
# * <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
+ # 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
@@ -227,25 +226,34 @@ module ActionController
# expires_in 3.hours, public: true, must_revalidate: true
#
# This method will overwrite an existing Cache-Control header.
- # See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html for more possibilities.
+ # 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
#
# The method will also ensure an HTTP Date header for client compatibility.
def expires_in(seconds, options = {})
response.cache_control.merge!(
- :max_age => seconds,
- :public => options.delete(:public),
- :must_revalidate => options.delete(:must_revalidate)
+ 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.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> so no caching should
- # occur by the browser or intermediate caches (like caching proxy servers).
+ # Sets an HTTP 1.1 Cache-Control header of <tt>no-cache</tt>. This means the
+ # resource will be marked as stale, so clients must always revalidate.
+ # Intermediate/browser caches may still store the asset.
def expires_now
- response.cache_control.replace(:no_cache => true)
+ response.cache_control.replace(no_cache: true)
end
# Cache or yield the block. The cache is supposed to never expire.
diff --git a/actionpack/lib/action_controller/metal/content_security_policy.rb b/actionpack/lib/action_controller/metal/content_security_policy.rb
new file mode 100644
index 0000000000..b8fab4ebe3
--- /dev/null
+++ b/actionpack/lib/action_controller/metal/content_security_policy.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+module ActionController #:nodoc:
+ module ContentSecurityPolicy
+ # TODO: Documentation
+ extend ActiveSupport::Concern
+
+ include AbstractController::Helpers
+ include AbstractController::Callbacks
+
+ included do
+ helper_method :content_security_policy?
+ helper_method :content_security_policy_nonce
+ end
+
+ module ClassMethods
+ def content_security_policy(enabled = true, **options, &block)
+ 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
+
+ def content_security_policy_report_only(report_only = true, **options)
+ before_action(options) do
+ request.content_security_policy_report_only = report_only
+ end
+ end
+ end
+
+ private
+
+ def content_security_policy?
+ request.content_security_policy
+ end
+
+ def content_security_policy_nonce
+ request.content_security_policy_nonce
+ end
+
+ def current_content_security_policy
+ request.content_security_policy.try(:clone) || ActionDispatch::ContentSecurityPolicy.new
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/metal/cookies.rb b/actionpack/lib/action_controller/metal/cookies.rb
index f8efb2b076..ff46966693 100644
--- a/actionpack/lib/action_controller/metal/cookies.rb
+++ b/actionpack/lib/action_controller/metal/cookies.rb
@@ -1,9 +1,11 @@
+# frozen_string_literal: true
+
module ActionController #:nodoc:
module Cookies
extend ActiveSupport::Concern
included do
- helper_method :cookies
+ helper_method :cookies if defined?(helper_method)
end
private
diff --git a/actionpack/lib/action_controller/metal/data_streaming.rb b/actionpack/lib/action_controller/metal/data_streaming.rb
index 957e7a3019..9ef4f50df1 100644
--- a/actionpack/lib/action_controller/metal/data_streaming.rb
+++ b/actionpack/lib/action_controller/metal/data_streaming.rb
@@ -1,4 +1,7 @@
-require 'action_controller/metal/exceptions'
+# frozen_string_literal: true
+
+require "action_controller/metal/exceptions"
+require "action_dispatch/http/content_disposition"
module ActionController #:nodoc:
# Methods for sending arbitrary data and for streaming files to the browser,
@@ -8,10 +11,10 @@ module ActionController #:nodoc:
include ActionController::Rendering
- DEFAULT_SEND_FILE_TYPE = 'application/octet-stream'.freeze #:nodoc:
- DEFAULT_SEND_FILE_DISPOSITION = 'attachment'.freeze #:nodoc:
+ DEFAULT_SEND_FILE_TYPE = "application/octet-stream" #:nodoc:
+ DEFAULT_SEND_FILE_DISPOSITION = "attachment" #:nodoc:
- protected
+ 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+.
@@ -25,14 +28,13 @@ module ActionController #:nodoc:
# * <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 register with
- # <tt>Mime::Type.register</tt>, for example :json
- # If omitted, type will be guessed from the file extension specified in <tt>:filename</tt>.
- # If no content type is registered for the extension, default type 'application/octet-stream' will be used.
+ # 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 guess the filename from
+ # * <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).
#
@@ -55,17 +57,17 @@ module ActionController #:nodoc:
#
# Read about the other Content-* HTTP headers if you'd like to
# provide the user with more information (such as Content-Description) in
- # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11.
+ # 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
- # http://www.mnot.net/cache_docs/ for an overview of web caching and
- # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9
+ # 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.
def send_file(path, options = {}) #:doc:
- raise MissingFile, "Cannot read file #{path}" unless File.file?(path) and File.readable?(path)
+ 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
@@ -79,14 +81,14 @@ module ActionController #:nodoc:
# <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 apparent file name, and other things.
+ # 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 register with <tt>Mime::Type.register</tt>, for example :json
- # If omitted, type will be guessed from the file extension specified in <tt>:filename</tt>.
- # If no content type is registered for the extension, default type 'application/octet-stream' will be used.
+ # * <tt>: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.
@@ -109,11 +111,13 @@ module ActionController #:nodoc:
render options.slice(:status, :content_type).merge(body: data)
end
- private
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)
@@ -123,21 +127,17 @@ module ActionController #:nodoc:
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
+ content_type = Mime::Type.lookup_by_extension(File.extname(options[:filename]).downcase.delete(".")) || content_type
end
self.content_type = content_type
end
disposition = options.fetch(:disposition, DEFAULT_SEND_FILE_DISPOSITION)
- unless disposition.nil?
- disposition = disposition.to_s
- disposition += %(; filename="#{options[:filename]}") if options[:filename]
- headers['Content-Disposition'] = disposition
+ if disposition
+ headers["Content-Disposition"] = ActionDispatch::Http::ContentDisposition.format(disposition: disposition, filename: options[:filename])
end
- headers['Content-Transfer-Encoding'] = 'binary'
-
- response.sending_file = true
+ 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),
diff --git a/actionpack/lib/action_controller/metal/default_headers.rb b/actionpack/lib/action_controller/metal/default_headers.rb
new file mode 100644
index 0000000000..eef0602fcd
--- /dev/null
+++ b/actionpack/lib/action_controller/metal/default_headers.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module ActionController
+ # Allows configuring default headers that will be automatically merged into
+ # each response.
+ module DefaultHeaders
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ def make_response!(request)
+ ActionDispatch::Response.create.tap do |res|
+ res.request = request
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/metal/etag_with_flash.rb b/actionpack/lib/action_controller/metal/etag_with_flash.rb
new file mode 100644
index 0000000000..38899e2f16
--- /dev/null
+++ b/actionpack/lib/action_controller/metal/etag_with_flash.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module ActionController
+ # When you're using the flash, it's generally used as a conditional on the view.
+ # This means the content of the view depends on the flash. Which in turn means
+ # that the ETag for a response should be computed with the content of the flash
+ # in mind. This does that by including the content of the flash as a component
+ # in the ETag that's generated for a response.
+ module EtagWithFlash
+ extend ActiveSupport::Concern
+
+ include ActionController::ConditionalGet
+
+ included do
+ etag { flash unless flash.empty? }
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/metal/etag_with_template_digest.rb b/actionpack/lib/action_controller/metal/etag_with_template_digest.rb
index 669cf55bca..2f1544c69c 100644
--- a/actionpack/lib/action_controller/metal/etag_with_template_digest.rb
+++ b/actionpack/lib/action_controller/metal/etag_with_template_digest.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionController
# When our views change, they should bubble up into HTTP cache freshness
# and bust browser caches. So the template digest for the current action
@@ -22,8 +24,7 @@ module ActionController
include ActionController::ConditionalGet
included do
- class_attribute :etag_with_template_digest
- self.etag_with_template_digest = true
+ class_attribute :etag_with_template_digest, default: true
ActiveSupport.on_load :action_view, yield: true do
etag do |options|
@@ -33,18 +34,24 @@ module ActionController
end
private
- def determine_template_etag(options)
- if template = pick_template_for_etag(options)
- lookup_and_digest_template(template)
+ def determine_template_etag(options)
+ if template = pick_template_for_etag(options)
+ lookup_and_digest_template(template)
+ end
end
- end
- def pick_template_for_etag(options)
- options.fetch(:template) { "#{controller_name}/#{action_name}" }
- end
+ # Pick the template digest to include in the ETag. If the +:template+ option
+ # is present, use the named template. If +:template+ is +nil+ or absent, use
+ # the default controller/action template. If +:template+ is false, omit the
+ # template digest from the ETag.
+ def pick_template_for_etag(options)
+ unless options[:template] == false
+ options[:template] || "#{controller_path}/#{action_name}"
+ end
+ end
- def lookup_and_digest_template(template)
- ActionView::Digestor.digest name: template, finder: lookup_context
- end
+ def lookup_and_digest_template(template)
+ ActionView::Digestor.digest name: template, format: nil, finder: lookup_context
+ end
end
end
diff --git a/actionpack/lib/action_controller/metal/exceptions.rb b/actionpack/lib/action_controller/metal/exceptions.rb
index 5c0ada37be..e1e0c6f456 100644
--- a/actionpack/lib/action_controller/metal/exceptions.rb
+++ b/actionpack/lib/action_controller/metal/exceptions.rb
@@ -1,22 +1,14 @@
+# frozen_string_literal: true
+
module ActionController
class ActionControllerError < StandardError #:nodoc:
end
class BadRequest < ActionControllerError #:nodoc:
- def initialize(msg = nil, e = nil)
- if e
- ActiveSupport::Deprecation.warn("Passing #original_exception is deprecated and has no effect. " \
- "Exceptions will automatically capture the original exception.", caller)
- end
-
+ def initialize(msg = nil)
super(msg)
set_backtrace $!.backtrace if $!
end
-
- def original_exception
- ActiveSupport::Deprecation.warn("#original_exception is deprecated. Use #cause instead.", caller)
- cause
- end
end
class RenderError < ActionControllerError #:nodoc:
@@ -24,32 +16,29 @@ module ActionController
class RoutingError < ActionControllerError #:nodoc:
attr_reader :failures
- def initialize(message, failures=[])
+ def initialize(message, failures = [])
super(message)
@failures = failures
end
end
- class ActionController::UrlGenerationError < ActionControllerError #:nodoc:
+ class UrlGenerationError < ActionControllerError #:nodoc:
end
class MethodNotAllowed < ActionControllerError #:nodoc:
def initialize(*allowed_methods)
- super("Only #{allowed_methods.to_sentence(:locale => :en)} requests are allowed.")
+ super("Only #{allowed_methods.to_sentence(locale: :en)} requests are allowed.")
end
end
class NotImplemented < MethodNotAllowed #:nodoc:
end
- class UnknownController < ActionControllerError #:nodoc:
- end
-
class MissingFile < ActionControllerError #:nodoc:
end
class SessionOverflowError < ActionControllerError #:nodoc:
- DEFAULT_MESSAGE = 'Your session data is larger than the data column in which it is to be stored. You must increase the size of your data column if you intend to store large data.'
+ DEFAULT_MESSAGE = "Your session data is larger than the data column in which it is to be stored. You must increase the size of your data column if you intend to store large data."
def initialize(message = nil)
super(message || DEFAULT_MESSAGE)
@@ -61,4 +50,25 @@ module ActionController
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
+ class RespondToMismatchError < ActionControllerError
+ DEFAULT_MESSAGE = "respond_to was called multiple times and matched with conflicting formats in this action. Please note that you may only call respond_to and match on a single format per action."
+
+ def initialize(message = nil)
+ super(message || DEFAULT_MESSAGE)
+ end
+ end
+
+ class MissingExactTemplate < UnknownFormat #:nodoc:
+ end
end
diff --git a/actionpack/lib/action_controller/metal/flash.rb b/actionpack/lib/action_controller/metal/flash.rb
index 65351284b9..a4861dc2c0 100644
--- a/actionpack/lib/action_controller/metal/flash.rb
+++ b/actionpack/lib/action_controller/metal/flash.rb
@@ -1,10 +1,11 @@
+# frozen_string_literal: true
+
module ActionController #:nodoc:
module Flash
extend ActiveSupport::Concern
included do
- class_attribute :_flash_types, instance_accessor: false
- self._flash_types = []
+ class_attribute :_flash_types, instance_accessor: false, default: []
delegate :flash, to: :request
add_flash_types(:alert, :notice)
@@ -35,26 +36,26 @@ module ActionController #:nodoc:
define_method(type) do
request.flash[type]
end
- helper_method type
+ helper_method(type) if respond_to?(:helper_method)
self._flash_types += [type]
end
end
end
- protected
- def redirect_to(options = {}, response_status_and_flash = {}) #:doc:
+ private
+ def redirect_to(options = {}, response_options_and_flash = {}) #:doc:
self.class._flash_types.each do |flash_type|
- if type = response_status_and_flash.delete(flash_type)
+ if type = response_options_and_flash.delete(flash_type)
flash[flash_type] = type
end
end
- if other_flashes = response_status_and_flash.delete(:flash)
+ if other_flashes = response_options_and_flash.delete(:flash)
flash.update(other_flashes)
end
- super(options, response_status_and_flash)
+ super(options, response_options_and_flash)
end
end
end
diff --git a/actionpack/lib/action_controller/metal/force_ssl.rb b/actionpack/lib/action_controller/metal/force_ssl.rb
index e31d65aac2..93fd57b640 100644
--- a/actionpack/lib/action_controller/metal/force_ssl.rb
+++ b/actionpack/lib/action_controller/metal/force_ssl.rb
@@ -1,19 +1,13 @@
-require 'active_support/core_ext/hash/except'
-require 'active_support/core_ext/hash/slice'
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/except"
+require "active_support/core_ext/hash/slice"
module ActionController
- # This module provides a method which will redirect browser to use HTTPS
- # protocol. This will ensure that user's sensitive information will be
- # transferred safely over the internet. You _should_ always force browser
- # to use HTTPS when you're transferring sensitive information such as
- # user authentication, account information, or credit card information.
- #
- # Note that if you are really concerned about your application security,
- # you might consider using +config.force_ssl+ in your config file instead.
- # That will ensure all the data transferred via HTTPS protocol and prevent
- # user from getting session hijacked when accessing the site under unsecured
- # HTTP protocol.
- module ForceSSL
+ # This module is deprecated in favor of +config.force_ssl+ in your environment
+ # config file. This will ensure all endpoints not explicitly marked otherwise
+ # will have all communication served over HTTPS.
+ module ForceSSL # :nodoc:
extend ActiveSupport::Concern
include AbstractController::Callbacks
@@ -21,45 +15,17 @@ module ActionController
URL_OPTIONS = [:protocol, :host, :domain, :subdomain, :port, :path]
REDIRECT_OPTIONS = [:status, :flash, :alert, :notice]
- module ClassMethods
- # Force the request to this particular controller or specified actions to be
- # under HTTPS protocol.
- #
- # If you need to disable this for any reason (e.g. development) then you can use
- # an +:if+ or +:unless+ condition.
- #
- # class AccountsController < ApplicationController
- # force_ssl if: :ssl_configured?
- #
- # def ssl_configured?
- # !Rails.env.development?
- # end
- # end
- #
- # ==== URL Options
- # You can pass any of the following options to affect the redirect url
- # * <tt>host</tt> - Redirect to a different host name
- # * <tt>subdomain</tt> - Redirect to a different subdomain
- # * <tt>domain</tt> - Redirect to a different domain
- # * <tt>port</tt> - Redirect to a non-standard port
- # * <tt>path</tt> - Redirect to a different path
- #
- # ==== Redirect Options
- # You can pass any of the following options to affect the redirect status and response
- # * <tt>status</tt> - Redirect with a custom status (default is 301 Moved Permanently)
- # * <tt>flash</tt> - Set a flash message when redirecting
- # * <tt>alert</tt> - Set an alert message when redirecting
- # * <tt>notice</tt> - Set a notice message when redirecting
- #
- # ==== Action Options
- # You can pass any of the following options to affect the before_action callback
- # * <tt>only</tt> - The callback should be run only for this action
- # * <tt>except</tt> - The callback should be run for all actions except this action
- # * <tt>if</tt> - A symbol naming an instance method or a proc; the
- # callback will be called only when it returns a true value.
- # * <tt>unless</tt> - A symbol naming an instance method or a proc; the
- # callback will be called only when it returns a false value.
+ module ClassMethods # :nodoc:
def force_ssl(options = {})
+ ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
+ Controller-level `force_ssl` is deprecated and will be removed from
+ Rails 6.1. Please enable `config.force_ssl` in your environment
+ configuration to enable the ActionDispatch::SSL middleware to more
+ fully enforce that your application communicate over HTTPS. If needed,
+ you can use `config.ssl_options` to exempt matching endpoints from
+ being redirected to HTTPS.
+ MESSAGE
+
action_options = options.slice(*ACTION_OPTIONS)
redirect_options = options.except(*ACTION_OPTIONS)
before_action(action_options) do
@@ -68,18 +34,13 @@ module ActionController
end
end
- # Redirect the existing request to use the HTTPS protocol.
- #
- # ==== Parameters
- # * <tt>host_or_options</tt> - Either a host name or any of the url &
- # redirect options available to the <tt>force_ssl</tt> method.
def force_ssl_redirect(host_or_options = nil)
unless request.ssl?
options = {
- :protocol => 'https://',
- :host => request.host,
- :path => request.fullpath,
- :status => :moved_permanently
+ protocol: "https://",
+ host: request.host,
+ path: request.fullpath,
+ status: :moved_permanently,
}
if host_or_options.is_a?(Hash)
@@ -89,7 +50,7 @@ module ActionController
end
secure_url = ActionDispatch::Http::URL.url_for(options.slice(*URL_OPTIONS))
- flash.keep if respond_to?(:flash)
+ flash.keep if respond_to?(:flash) && request.respond_to?(:flash)
redirect_to secure_url, options.slice(*REDIRECT_OPTIONS)
end
end
diff --git a/actionpack/lib/action_controller/metal/head.rb b/actionpack/lib/action_controller/metal/head.rb
index 5e9832fd4e..3c84bebb85 100644
--- a/actionpack/lib/action_controller/metal/head.rb
+++ b/actionpack/lib/action_controller/metal/head.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionController
module Head
# Returns a response that has no content (merely headers). The options
@@ -18,13 +20,7 @@ module ActionController
# See Rack::Utils::SYMBOL_TO_STATUS_CODE for a full list of valid +status+ symbols.
def head(status, options = {})
if status.is_a?(Hash)
- msg = status[:status] ? 'The :status option' : 'The implicit :ok status'
- options, status = status, status.delete(:status)
-
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- #{msg} on `head` has been deprecated and will be removed in Rails 5.1.
- Please pass the status as a separate parameter before the options, instead.
- MSG
+ raise ArgumentError, "#{status.inspect} is not a valid value for `status`."
end
status ||= :ok
@@ -33,7 +29,7 @@ module ActionController
content_type = options.delete(:content_type)
options.each do |key, value|
- headers[key.to_s.dasherize.split('-').each { |v| v[0] = v[0].chr.upcase }.join('-')] = value.to_s
+ headers[key.to_s.dasherize.split("-").each { |v| v[0] = v[0].chr.upcase }.join("-")] = value.to_s
end
self.status = status
@@ -41,24 +37,24 @@ module ActionController
self.response_body = ""
- if include_content?(self.response_code)
- self.content_type = content_type || (Mime[formats.first] if formats)
- self.response.charset = false
+ if include_content?(response_code)
+ self.content_type = content_type || (Mime[formats.first] if formats) || Mime[:html]
+ response.charset = false
end
true
end
private
- def include_content?(status)
- case status
- when 100..199
- false
- when 204, 205, 304
- false
- else
- true
+ def include_content?(status)
+ case status
+ when 100..199
+ false
+ when 204, 205, 304
+ false
+ else
+ true
+ end
end
- end
end
end
diff --git a/actionpack/lib/action_controller/metal/helpers.rb b/actionpack/lib/action_controller/metal/helpers.rb
index d3853e2e83..193b488f6c 100644
--- a/actionpack/lib/action_controller/metal/helpers.rb
+++ b/actionpack/lib/action_controller/metal/helpers.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionController
# The \Rails framework provides a large number of helpers for working with assets, dates, forms,
# numbers and model objects, to name a few. These helpers are available to all templates
@@ -5,7 +7,7 @@ module ActionController
#
# 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>
+ # 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
@@ -32,7 +34,7 @@ module ActionController
# end
# end
#
- # Then, in any view rendered by <tt>EventController</tt>, the <tt>format_time</tt> method can be called:
+ # Then, in any view rendered by <tt>EventsController</tt>, the <tt>format_time</tt> method can be called:
#
# <% @events.each do |event| -%>
# <p>
@@ -53,9 +55,8 @@ module ActionController
include AbstractController::Helpers
included do
- class_attribute :helpers_path, :include_all_helpers
- self.helpers_path ||= []
- self.include_all_helpers = true
+ class_attribute :helpers_path, default: []
+ class_attribute :include_all_helpers, default: true
end
module ClassMethods
@@ -71,10 +72,10 @@ module ActionController
attrs.flatten.each { |attr| helper_method(attr, "#{attr}=") }
end
- # Provides a proxy to access helpers methods from outside the view.
+ # Provides a proxy to access helper methods from outside the view.
def helpers
@helper_proxy ||= begin
- proxy = ActionView::Base.new
+ proxy = ActionView::Base.empty
proxy.config = config.inheritable_copy
proxy.extend(_helpers)
end
@@ -99,8 +100,7 @@ module ActionController
# # => ["application", "chart", "rubygems"]
def all_helpers_from_path(path)
helpers = Array(path).flat_map do |_path|
- extract = /^#{Regexp.quote(_path.to_s)}\/?(.*)_helper.rb$/
- names = Dir["#{_path}/**/*_helper.rb"].map { |file| file.sub(extract, '\1'.freeze) }
+ names = Dir["#{_path}/**/*_helper.rb"].map { |file| file[_path.to_s.size + 1..-"_helper.rb".size - 1] }
names.sort!
end
helpers.uniq!
@@ -108,10 +108,15 @@ module ActionController
end
private
- # Extract helper names from files in <tt>app/helpers/**/*_helper.rb</tt>
- def all_application_helpers
- all_helpers_from_path(helpers_path)
- end
+ # Extract helper names from files in <tt>app/helpers/**/*_helper.rb</tt>
+ def all_application_helpers
+ all_helpers_from_path(helpers_path)
+ end
+ end
+
+ # Provides a proxy to access helper methods from outside the view.
+ def helpers
+ @_helper_proxy ||= view_context
end
end
end
diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb
index 35be6d9300..6a274d35cb 100644
--- a/actionpack/lib/action_controller/metal/http_authentication.rb
+++ b/actionpack/lib/action_controller/metal/http_authentication.rb
@@ -1,5 +1,7 @@
-require 'base64'
-require 'active_support/security_utils'
+# frozen_string_literal: true
+
+require "base64"
+require "active_support/security_utils"
module ActionController
# Makes it dead easy to do HTTP Basic, Digest and Token authentication.
@@ -28,7 +30,7 @@ module ActionController
# class ApplicationController < ActionController::Base
# before_action :set_account, :authenticate
#
- # protected
+ # private
# def set_account
# @account = Account.find_by(url_name: request.subdomains.first)
# end
@@ -54,8 +56,9 @@ module ActionController
# In your integration tests, you can do something like this:
#
# def test_access_granted_from_xml
- # @request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials(users(:dhh).name, users(:dhh).password)
- # get "/notes/1.xml"
+ # 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
@@ -66,21 +69,20 @@ module ActionController
extend ActiveSupport::Concern
module ClassMethods
- def http_basic_authenticate_with(options = {})
- before_action(options.except(:name, :password, :realm)) do
- authenticate_or_request_with_http_basic(options[:realm] || "Application") do |name, password|
- # This comparison uses & so that it doesn't short circuit and
- # uses `variable_size_secure_compare` so that length information
- # isn't leaked.
- ActiveSupport::SecurityUtils.variable_size_secure_compare(name, options[:name]) &
- ActiveSupport::SecurityUtils.variable_size_secure_compare(password, options[:password])
- end
- end
+ def http_basic_authenticate_with(name:, password:, realm: nil, **options)
+ before_action(options) { http_basic_authenticate_or_request_with name: name, password: password, realm: realm }
+ end
+ end
+
+ 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
- def authenticate_or_request_with_http_basic(realm = "Application", message = nil, &login_procedure)
- authenticate_with_http_basic(&login_procedure) || request_http_basic_authentication(realm, message)
+ 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
def authenticate_with_http_basic(&login_procedure)
@@ -99,23 +101,23 @@ module ActionController
end
def has_basic_credentials?(request)
- request.authorization.present? && (auth_scheme(request).downcase == 'basic')
+ request.authorization.present? && (auth_scheme(request).downcase == "basic")
end
def user_name_and_password(request)
- decode_credentials(request).split(':', 2)
+ decode_credentials(request).split(":", 2)
end
def decode_credentials(request)
- ::Base64.decode64(auth_param(request) || '')
+ ::Base64.decode64(auth_param(request) || "")
end
def auth_scheme(request)
- request.authorization.to_s.split(' ', 2).first
+ request.authorization.to_s.split(" ", 2).first
end
def auth_param(request)
- request.authorization.to_s.split(' ', 2).second
+ request.authorization.to_s.split(" ", 2).second
end
def encode_credentials(user_name, password)
@@ -124,7 +126,7 @@ module ActionController
def authentication_request(controller, realm, message)
message ||= "HTTP Basic: Access denied.\n"
- controller.headers["WWW-Authenticate"] = %(Basic realm="#{realm.tr('"'.freeze, "".freeze)}")
+ controller.headers["WWW-Authenticate"] = %(Basic realm="#{realm.tr('"', "")}")
controller.status = 401
controller.response_body = message
end
@@ -208,7 +210,7 @@ module ActionController
password = password_procedure.call(credentials[:username])
return false unless password
- method = request.get_header('rack.methodoverride.original_method') || request.get_header('REQUEST_METHOD')
+ method = request.get_header("rack.methodoverride.original_method") || request.get_header("REQUEST_METHOD")
uri = credentials[:uri]
[true, false].any? do |trailing_question_mark|
@@ -224,19 +226,19 @@ module ActionController
# Returns the expected response for a request of +http_method+ to +uri+ with the decoded +credentials+ and the expected +password+
# Optional parameter +password_is_ha1+ is set to +true+ by default, since best practice is to store ha1 digest instead
# of a plain-text password.
- def expected_response(http_method, uri, credentials, password, password_is_ha1=true)
+ 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(':'))
+ ha2 = ::Digest::MD5.hexdigest([http_method.to_s.upcase, uri].join(":"))
+ ::Digest::MD5.hexdigest([ha1, credentials[:nonce], credentials[:nc], credentials[:cnonce], credentials[:qop], ha2].join(":"))
end
def ha1(credentials, password)
- ::Digest::MD5.hexdigest([credentials[:username], credentials[:realm], password].join(':'))
+ ::Digest::MD5.hexdigest([credentials[:username], credentials[:realm], password].join(":"))
end
def encode_credentials(http_method, credentials, password, password_is_ha1)
credentials[:response] = expected_response(http_method, credentials[:uri], credentials, password, password_is_ha1)
- "Digest " + credentials.sort_by {|x| x[0].to_s }.map {|v| "#{v[0]}='#{v[1]}'" }.join(', ')
+ "Digest " + credentials.sort_by { |x| x[0].to_s }.map { |v| "#{v[0]}='#{v[1]}'" }.join(", ")
end
def decode_credentials_header(request)
@@ -244,9 +246,9 @@ module ActionController
end
def decode_credentials(header)
- ActiveSupport::HashWithIndifferentAccess[header.to_s.gsub(/^Digest\s+/, '').split(',').map do |pair|
- key, value = pair.split('=', 2)
- [key.strip, value.to_s.gsub(/^"|"$/,'').delete('\'')]
+ 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
@@ -310,11 +312,11 @@ module ActionController
end
# Might want a shorter timeout depending on whether the request
- # is a PATCH, PUT, or POST, and if client is browser or web service.
+ # 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 user again for their
+ # allow a user to use new nonce without prompting the user again for their
# username and password.
- def validate_nonce(secret_key, request, value, seconds_to_timeout=5*60)
+ 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
@@ -324,7 +326,6 @@ module ActionController
def opaque(secret_key)
::Digest::MD5.hexdigest(secret_key)
end
-
end
# Makes it dead easy to do HTTP Token authentication.
@@ -347,7 +348,9 @@ module ActionController
# private
# def authenticate
# authenticate_or_request_with_http_token do |token, options|
- # token == TOKEN
+ # # Compare the tokens in a time-constant manner, to mitigate
+ # # timing attacks.
+ # ActiveSupport::SecurityUtils.secure_compare(token, TOKEN)
# end
# end
# end
@@ -359,7 +362,7 @@ module ActionController
# class ApplicationController < ActionController::Base
# before_action :set_account, :authenticate
#
- # protected
+ # private
# def set_account
# @account = Account.find_by(url_name: request.subdomains.first)
# end
@@ -386,10 +389,9 @@ module ActionController
# In your integration tests, you can do something like this:
#
# def test_access_granted_from_xml
- # get(
- # "/notes/1.xml", nil,
- # 'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Token.encode_credentials(users(:dhh).token)
- # )
+ # authorization = ActionController::HttpAuthentication::Token.encode_credentials(users(:dhh).token)
+ #
+ # get "/notes/1.xml", headers: { 'HTTP_AUTHORIZATION' => authorization }
#
# assert_equal 200, status
# end
@@ -401,7 +403,7 @@ module ActionController
#
# RewriteRule ^(.*)$ dispatch.fcgi [E=X-HTTP_AUTHORIZATION:%{HTTP:Authorization},QSA,L]
module Token
- TOKEN_KEY = 'token='
+ TOKEN_KEY = "token="
TOKEN_REGEX = /^(Token|Bearer)\s+/
AUTHN_PAIR_DELIMITERS = /(?:,|;|\t+)/
extend self
@@ -441,7 +443,7 @@ module ActionController
end
end
- # Parses the token and options out of the token authorization header.
+ # Parses the token and options out of the token Authorization header.
# The value 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"
@@ -471,14 +473,14 @@ module ActionController
# This removes the <tt>"</tt> characters wrapping the value.
def rewrite_param_values(array_params)
- array_params.each { |param| (param[1] || "").gsub! %r/^"|"$/, '' }
+ 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+.
def raw_params(auth)
- _raw_params = auth.sub(TOKEN_REGEX, '').split(/\s*#{AUTHN_PAIR_DELIMITERS}\s*/)
+ _raw_params = auth.sub(TOKEN_REGEX, "").split(/\s*#{AUTHN_PAIR_DELIMITERS}\s*/)
if !(_raw_params.first =~ %r{\A#{TOKEN_KEY}})
_raw_params[0] = "#{TOKEN_KEY}#{_raw_params.first}"
@@ -508,7 +510,7 @@ module ActionController
# Returns nothing.
def authentication_request(controller, realm, message = nil)
message ||= "HTTP Token: Access denied.\n"
- controller.headers["WWW-Authenticate"] = %(Token realm="#{realm.tr('"'.freeze, "".freeze)}")
+ controller.headers["WWW-Authenticate"] = %(Token realm="#{realm.tr('"', "")}")
controller.__send__ :render, plain: message, status: :unauthorized
end
end
diff --git a/actionpack/lib/action_controller/metal/implicit_render.rb b/actionpack/lib/action_controller/metal/implicit_render.rb
index 6192fc0f9c..8365ddca57 100644
--- a/actionpack/lib/action_controller/metal/implicit_render.rb
+++ b/actionpack/lib/action_controller/metal/implicit_render.rb
@@ -1,14 +1,14 @@
-require 'active_support/core_ext/string/strip'
+# frozen_string_literal: true
module ActionController
# Handles implicit rendering for a controller action that does not
# explicitly respond with +render+, +respond_to+, +redirect+, or +head+.
#
- # For API controllers, the implicit response is always 204 No Content.
+ # 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
- # 204 No Content:
+ # <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
@@ -25,15 +25,14 @@ module ActionController
# <tt>ActionView::UnknownFormat</tt> with an explanation.
#
# Finally, if we DON'T find a template AND the request isn't a browser page
- # load, then we implicitly respond with 204 No Content.
+ # load, then we implicitly respond with <tt>204 No Content</tt>.
module ImplicitRender
-
# :stopdoc:
include BasicImplicitRender
- def default_render(*args)
+ def default_render
if template_exists?(action_name.to_s, _prefixes, variants: request.variant)
- render(*args)
+ 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" \
@@ -42,18 +41,8 @@ module ActionController
raise ActionController::UnknownFormat, message
elsif interactive_browser_request?
- message = "#{self.class.name}\##{action_name} is missing a template " \
- "for this request format and variant.\n\n" \
- "request.formats: #{request.formats.map(&:to_s).inspect}\n" \
- "request.variant: #{request.variant.inspect}\n\n" \
- "NOTE! For XHR/Ajax or API requests, this action would normally " \
- "respond with 204 No Content: an empty white screen. Since you're " \
- "loading it in a web browser, we assume that you expected to " \
- "actually render a template, not… nothing, so we're showing an " \
- "error to be extra-clear. If you expect 204 No Content, carry on. " \
- "That's what you'll get from an XHR or API request. Give it a shot."
-
- raise ActionController::UnknownFormat, message
+ 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
@@ -62,8 +51,8 @@ module ActionController
def method_for_action(action_name)
super || if template_exists?(action_name.to_s, _prefixes)
- "default_render"
- end
+ "default_render"
+ end
end
private
diff --git a/actionpack/lib/action_controller/metal/instrumentation.rb b/actionpack/lib/action_controller/metal/instrumentation.rb
index 885ea3fefd..51fac08749 100644
--- a/actionpack/lib/action_controller/metal/instrumentation.rb
+++ b/actionpack/lib/action_controller/metal/instrumentation.rb
@@ -1,9 +1,11 @@
-require 'benchmark'
-require 'abstract_controller/logger'
+# frozen_string_literal: true
+
+require "benchmark"
+require "abstract_controller/logger"
module ActionController
# Adds instrumentation to several ends in ActionController::Base. It also provides
- # some hooks related with process_action, this allows an ORM like Active Record
+ # 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.
@@ -16,25 +18,23 @@ module ActionController
def process_action(*args)
raw_payload = {
- :controller => self.class.name,
- :action => self.action_name,
- :params => request.filtered_parameters,
- :headers => request.headers,
- :format => request.format.ref,
- :method => request.request_method,
- :path => request.fullpath
+ controller: self.class.name,
+ action: action_name,
+ params: request.filtered_parameters,
+ headers: request.headers,
+ format: request.format.ref,
+ method: request.request_method,
+ path: request.fullpath
}
ActiveSupport::Notifications.instrument("start_processing.action_controller", raw_payload.dup)
ActiveSupport::Notifications.instrument("process_action.action_controller", raw_payload) do |payload|
- begin
- result = super
+ super.tap do
payload[:status] = response.status
- result
- ensure
- append_info_to_payload(payload)
end
+ ensure
+ append_info_to_payload(payload)
end
end
@@ -46,9 +46,9 @@ module ActionController
render_output
end
- def send_file(path, options={})
+ def send_file(path, options = {})
ActiveSupport::Notifications.instrument("send_file.action_controller",
- options.merge(:path => path)) do
+ options.merge(path: path)) do
super
end
end
@@ -72,25 +72,22 @@ module ActionController
# A hook invoked every time a before callback is halted.
def halted_callback_hook(filter)
- ActiveSupport::Notifications.instrument("halted_callback.action_controller", :filter => filter)
+ ActiveSupport::Notifications.instrument("halted_callback.action_controller", filter: filter)
end
- # A hook which allows you to clean up any time taken into account in
- # views wrongly, like database querying time.
+ # A hook which allows you to clean up any time, wrongly taken into account in
+ # views, like database querying time.
#
# def cleanup_view_runtime
# super - time_taken_in_something_expensive
# end
- #
- # :api: plugin
- def cleanup_view_runtime #:nodoc:
+ 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.
- # :api: plugin
- def append_info_to_payload(payload) #:nodoc:
+ def append_info_to_payload(payload) # :doc:
payload[:view_runtime] = view_runtime
end
@@ -98,7 +95,6 @@ module ActionController
# A hook which allows other frameworks to log what happened during
# controller process action. This method should return an array
# with the messages to be added.
- # :api: plugin
def log_process_action(payload) #:nodoc:
messages, view_runtime = [], payload[:view_runtime]
messages << ("Views: %.1fms" % view_runtime.to_f) if view_runtime
diff --git a/actionpack/lib/action_controller/metal/live.rb b/actionpack/lib/action_controller/metal/live.rb
index fc20e7a421..dd69930e25 100644
--- a/actionpack/lib/action_controller/metal/live.rb
+++ b/actionpack/lib/action_controller/metal/live.rb
@@ -1,9 +1,11 @@
-require 'action_dispatch/http/response'
-require 'delegate'
-require 'active_support/json'
+# frozen_string_literal: true
+
+require "action_dispatch/http/response"
+require "delegate"
+require "active_support/json"
module ActionController
- # Mix this module in to your controller, and all actions in that controller
+ # 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
@@ -20,7 +22,7 @@ module ActionController
# end
# end
#
- # There are a few caveats with this use. You *cannot* write headers after the
+ # 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
@@ -84,8 +86,7 @@ module ActionController
# Note: SSEs are not currently supported by IE. However, they are supported
# by Chrome, Firefox, Opera, and Safari.
class SSE
-
- WHITELISTED_OPTIONS = %w( retry event id )
+ PERMITTED_OPTIONS = %w( retry event id )
def initialize(stream, options = {})
@stream = stream
@@ -110,13 +111,13 @@ module ActionController
def perform_write(json, options)
current_options = @options.merge(options).stringify_keys
- WHITELISTED_OPTIONS.each do |option_name|
+ 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".freeze, "\ndata: ".freeze)
+ message = json.gsub("\n", "\ndata: ")
@stream.write "data: #{message}\n\n"
end
end
@@ -145,7 +146,7 @@ module ActionController
def write(string)
unless @response.committed?
- @response.set_header "Cache-Control", "no-cache"
+ @response.headers["Cache-Control"] ||= "no-cache"
@response.delete_header "Content-Length"
end
@@ -163,14 +164,6 @@ module ActionController
end
end
- def each
- @response.sending!
- while str = @buf.pop
- yield str
- end
- @response.sent!
- end
-
# Write a 'close' event to the buffer; the producer/writing thread
# uses this to notify us that it's finished supplying content.
#
@@ -210,23 +203,36 @@ module ActionController
def call_on_error
@error_callback.call
end
+
+ private
+
+ def each_chunk(&block)
+ loop do
+ str = nil
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
+ str = @buf.pop
+ end
+ break unless str
+ yield str
+ end
+ end
end
class Response < ActionDispatch::Response #:nodoc: all
private
- def before_committed
- super
- jar = request.cookie_jar
- # The response can be committed multiple times
- jar.write self unless committed?
- end
+ def before_committed
+ super
+ jar = request.cookie_jar
+ # The response can be committed multiple times
+ jar.write self unless committed?
+ end
- def build_buffer(response, body)
- buf = Live::Buffer.new response
- body.each { |part| buf.write part }
- buf
- end
+ def build_buffer(response, body)
+ buf = Live::Buffer.new response
+ body.each { |part| buf.write part }
+ buf
+ end
end
def process(name)
@@ -235,15 +241,15 @@ module ActionController
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
+ # 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 }
+ locals.each { |k, v| t2[k] = v }
begin
super(name)
@@ -274,33 +280,35 @@ module ActionController
raise error if error
end
- # Spawn a new thread to serve up the controller in. This is to get
- # around the fact that Rack isn't based around IOs and we need to use
- # a thread to stream data from the response bodies. Nobody should call
- # this method except in Rails internals. Seriously!
- def new_controller_thread # :nodoc:
- Thread.new {
- t2 = Thread.current
- t2.abort_on_exception = true
- yield
- }
+ def response_body=(body)
+ super
+ response.close if response
end
- def log_error(exception)
- logger = ActionController::Base.logger
- return unless logger
+ private
- logger.fatal do
- message = "\n#{exception.class} (#{exception.message}):\n"
- message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code)
- message << " " << exception.backtrace.join("\n ")
- "#{message}\n\n"
+ # Spawn a new thread to serve up the controller in. This is to get
+ # around the fact that Rack isn't based around IOs and we need to use
+ # a thread to stream data from the response bodies. Nobody should call
+ # this method except in Rails internals. Seriously!
+ def new_controller_thread # :nodoc:
+ Thread.new {
+ t2 = Thread.current
+ t2.abort_on_exception = true
+ yield
+ }
end
- end
- def response_body=(body)
- super
- response.close if response
- end
+ 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
diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb
index 2e89af1a5e..bf5e7a433f 100644
--- a/actionpack/lib/action_controller/metal/mime_responds.rb
+++ b/actionpack/lib/action_controller/metal/mime_responds.rb
@@ -1,4 +1,6 @@
-require 'abstract_controller/collector'
+# frozen_string_literal: true
+
+require "abstract_controller/collector"
module ActionController #:nodoc:
module MimeResponds
@@ -9,7 +11,7 @@ module ActionController #:nodoc:
# @people = Person.all
# end
#
- # That action implicitly responds to all formats, but formats can also be whitelisted:
+ # That action implicitly responds to all formats, but formats can also be explicitly enumerated:
#
# def index
# @people = Person.all
@@ -103,7 +105,7 @@ module ActionController #:nodoc:
#
# Mime::Type.register "image/jpg", :jpg
#
- # Respond to also allows you to specify a common block for different formats by using +any+:
+ # +respond_to+ also allows you to specify a common block for different formats by using +any+:
#
# def index
# @people = Person.all
@@ -122,6 +124,14 @@ module ActionController #:nodoc:
#
# 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>,
@@ -181,8 +191,8 @@ module ActionController #:nodoc:
#
# request.variant = [:tablet, :phone]
#
- # which will work similarly to formats and MIME types negotiation. If there will be no
- # +:tablet+ variant declared, +:phone+ variant will be picked:
+ # 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
@@ -195,6 +205,9 @@ module ActionController #:nodoc:
yield collector if block_given?
if format = collector.negotiate_format(request)
+ if content_type && content_type != format
+ raise ActionController::RespondToMismatchError
+ end
_process_format(format)
_set_rendered_content_type format
response = collector.response
@@ -280,8 +293,8 @@ module ActionController #:nodoc:
def any(*args, &block)
if block_given?
- if args.any? && args.none?{ |a| a == @variant }
- args.each{ |v| @variants[v] = block }
+ if args.any? && args.none? { |a| a == @variant }
+ args.each { |v| @variants[v] = block }
else
@variants[:any] = block
end
diff --git a/actionpack/lib/action_controller/metal/parameter_encoding.rb b/actionpack/lib/action_controller/metal/parameter_encoding.rb
new file mode 100644
index 0000000000..7a45732d31
--- /dev/null
+++ b/actionpack/lib/action_controller/metal/parameter_encoding.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+module ActionController
+ # Specify binary encoding for parameters for a given action.
+ module ParameterEncoding
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ def inherited(klass) # :nodoc:
+ super
+ klass.setup_param_encode
+ end
+
+ def setup_param_encode # :nodoc:
+ @_parameter_encodings = {}
+ end
+
+ def binary_params_for?(action) # :nodoc:
+ @_parameter_encodings[action.to_s]
+ end
+
+ # Specify that a given action's parameters should all be encoded as
+ # ASCII-8BIT (it "skips" the encoding default of UTF-8).
+ #
+ # For example, a controller would use it like this:
+ #
+ # class RepositoryController < ActionController::Base
+ # skip_parameter_encoding :show
+ #
+ # def show
+ # @repo = Repository.find_by_filesystem_path params[:file_path]
+ #
+ # # `repo_name` is guaranteed to be UTF-8, but was ASCII-8BIT, so
+ # # tag it as such
+ # @repo_name = params[:repo_name].force_encoding 'UTF-8'
+ # end
+ #
+ # def index
+ # @repositories = Repository.all
+ # end
+ # end
+ #
+ # The show action in the above controller would have all parameter values
+ # encoded as ASCII-8BIT. This is useful in the case where an application
+ # must handle data but encoding of the data is unknown, like file system data.
+ def skip_parameter_encoding(action)
+ @_parameter_encodings[action.to_s] = true
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/metal/params_wrapper.rb b/actionpack/lib/action_controller/metal/params_wrapper.rb
index c38fc40b81..09716f7588 100644
--- a/actionpack/lib/action_controller/metal/params_wrapper.rb
+++ b/actionpack/lib/action_controller/metal/params_wrapper.rb
@@ -1,7 +1,9 @@
-require 'active_support/core_ext/hash/slice'
-require 'active_support/core_ext/hash/except'
-require 'active_support/core_ext/module/anonymous'
-require 'action_dispatch/http/mime_type'
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/slice"
+require "active_support/core_ext/hash/except"
+require "active_support/core_ext/module/anonymous"
+require "action_dispatch/http/mime_type"
module ActionController
# Wraps the parameters hash into a nested hash. This will allow clients to
@@ -71,7 +73,7 @@ module ActionController
EXCLUDE_PARAMETERS = %w(authenticity_token _method utf8)
- require 'mutex_m'
+ require "mutex_m"
class Options < Struct.new(:name, :format, :include, :exclude, :klass, :model) # :nodoc:
include Mutex_m
@@ -105,7 +107,19 @@ module ActionController
unless super || exclude
if m.respond_to?(:attribute_names) && m.attribute_names.any?
- self.include = m.attribute_names
+ 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
@@ -128,35 +142,34 @@ module ActionController
end
private
- # Determine the wrapper model from the controller's name. By convention,
- # this could be done by trying to find the defined model that has the
- # same singular name as the controller. For example, +UsersController+
- # will try to find if the +User+ model exists.
- #
- # This method also does namespace lookup. Foo::Bar::UsersController will
- # try to find Foo::Bar::User, Foo::User and finally User.
- def _default_wrap_model #:nodoc:
- return nil if klass.anonymous?
- model_name = klass.name.sub(/Controller$/, '').classify
-
- begin
- if model_klass = model_name.safe_constantize
- model_klass
- else
- namespaces = model_name.split("::")
- namespaces.delete_at(-2)
- break if namespaces.last == model_name
- model_name = namespaces.join("::")
- end
- end until model_klass
+ # Determine the wrapper model from the controller's name. By convention,
+ # this could be done by trying to find the defined model that has the
+ # same singular name as the controller. For example, +UsersController+
+ # will try to find if the +User+ model exists.
+ #
+ # This method also does namespace lookup. Foo::Bar::UsersController will
+ # try to find Foo::Bar::User, Foo::User and finally User.
+ def _default_wrap_model
+ return nil if klass.anonymous?
+ model_name = klass.name.sub(/Controller$/, "").classify
+
+ begin
+ if model_klass = model_name.safe_constantize
+ model_klass
+ else
+ namespaces = model_name.split("::")
+ namespaces.delete_at(-2)
+ break if namespaces.last == model_name
+ model_name = namespaces.join("::")
+ end
+ end until model_klass
- model_klass
- end
+ model_klass
+ end
end
included do
- class_attribute :_wrapper_options
- self._wrapper_options = Options.from_hash(format: [])
+ class_attribute :_wrapper_options, default: Options.from_hash(format: [])
end
module ClassMethods
@@ -198,14 +211,14 @@ module ActionController
when Hash
options = name_or_model_or_options
when false
- options = options.merge(:format => [])
+ options = options.merge(format: [])
when Symbol, String
- options = options.merge(:name => name_or_model_or_options)
+ options = options.merge(name: name_or_model_or_options)
else
model = name_or_model_or_options
end
- opts = Options.from_hash _wrapper_options.to_h.slice(:format).merge(options)
+ opts = Options.from_hash _wrapper_options.to_h.slice(:format).merge(options)
opts.model = model
opts.klass = self
@@ -213,7 +226,7 @@ module ActionController
end
# Sets the default wrapper key or model which will be used to determine
- # wrapper key and attribute names. Will be called automatically when the
+ # wrapper key and attribute names. Called automatically when the
# module is inherited.
def inherited(klass)
if klass._wrapper_options.format.any?
@@ -225,26 +238,10 @@ module ActionController
end
end
- # Performs parameters wrapping upon the request. Will be called automatically
+ # Performs parameters wrapping upon the request. Called automatically
# by the metal call stack.
def process_action(*args)
- if _wrapper_enabled?
- if request.parameters[_wrapper_key].present?
- wrapped_hash = _extract_parameters(request.parameters)
- else
- wrapped_hash = _wrap_parameters request.request_parameters
- end
-
- wrapped_keys = request.request_parameters.keys
- wrapped_filtered_hash = _wrap_parameters request.filtered_parameters.slice(*wrapped_keys)
-
- # This will make the wrapped hash accessible from controller and view
- request.parameters.merge! wrapped_hash
- request.request_parameters.merge! wrapped_hash
-
- # This will display the wrapped hash in the log file
- request.filtered_parameters.merge! wrapped_filtered_hash
- end
+ _perform_parameter_wrapping if _wrapper_enabled?
super
end
@@ -279,7 +276,22 @@ module ActionController
return false unless request.has_content_type?
ref = request.content_mime_type.ref
- _wrapper_formats.include?(ref) && _wrapper_key && !request.request_parameters[_wrapper_key]
+ _wrapper_formats.include?(ref) && _wrapper_key && !request.parameters.key?(_wrapper_key)
+ end
+
+ 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
diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb
index 3c7cc15627..67c198d150 100644
--- a/actionpack/lib/action_controller/metal/redirecting.rb
+++ b/actionpack/lib/action_controller/metal/redirecting.rb
@@ -1,12 +1,6 @@
-module ActionController
- class RedirectBackError < AbstractController::Error #:nodoc:
- DEFAULT_MESSAGE = 'No HTTP_REFERER was set in the request to this action, so redirect_to :back could not be called successfully. If this is a test, make sure to specify request.env["HTTP_REFERER"].'
-
- def initialize(message = nil)
- super(message || DEFAULT_MESSAGE)
- end
- end
+# frozen_string_literal: true
+module ActionController
module Redirecting
extend ActiveSupport::Concern
@@ -24,27 +18,27 @@ module ActionController
# === Examples:
#
# redirect_to action: "show", id: 5
- # redirect_to post
+ # redirect_to @post
# redirect_to "http://www.rubyonrails.org"
# redirect_to "/images/screenshot.jpg"
- # redirect_to articles_url
+ # redirect_to posts_url
# redirect_to proc { edit_post_url(@post) }
#
- # The redirection happens as a "302 Found" header unless otherwise specified using the <tt>:status</tt> option:
+ # The redirection happens as a <tt>302 Found</tt> header unless otherwise specified using the <tt>:status</tt> option:
#
# redirect_to post_url(@post), status: :found
# redirect_to action: 'atom', status: :moved_permanently
# redirect_to post_url(@post), status: 301
# redirect_to action: 'atom', status: 302
#
- # The status code can either be a standard {HTTP Status code}[http://www.iana.org/assignments/http-status-codes] as an
+ # 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
+ # 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
@@ -58,39 +52,45 @@ module ActionController
# redirect_to post_url(@post), status: 301, flash: { updated_post_id: @post.id }
# redirect_to({ action: 'atom' }, alert: "Something serious happened")
#
- def redirect_to(options = {}, response_status = {}) #:doc:
+ # Statements after +redirect_to+ in our controller get executed, so +redirect_to+ doesn't stop the execution of the function.
+ # To terminate the execution of the function immediately after the +redirect_to+, use return.
+ # redirect_to post_url(@post) and return
+ def redirect_to(options = {}, response_options = {})
raise ActionControllerError.new("Cannot redirect to nil!") unless options
raise AbstractController::DoubleRenderError if response_body
- self.status = _extract_redirect_to_status(options, response_status)
+ self.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(location)}\">redirected</a>.</body></html>"
+ 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 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: @post
# redirect_back fallback_location: "http://www.rubyonrails.org"
- # redirect_back fallback_location: "/images/screenshot.jpg"
- # redirect_back fallback_location: articles_url
- # redirect_back fallback_location: proc { edit_post_url(@post) }
+ # 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
#
- # All options that can be passed to <tt>redirect_to</tt> are accepted as
+ # ==== 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 <tt>redirect_to</tt> are accepted as
# options and the behavior is identical.
- def redirect_back(fallback_location:, **args)
- if referer = request.headers["Referer"]
- redirect_to referer, **args
- else
- redirect_to fallback_location, **args
- end
+ 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
def _compute_redirect_to_location(request, options) #:nodoc:
@@ -98,22 +98,14 @@ module ActionController
# The scheme name consist of a letter followed by any combination of
# letters, digits, and the plus ("+"), period ("."), or hyphen ("-")
# characters; and is terminated by a colon (":").
- # See http://tools.ietf.org/html/rfc3986#section-3.1
+ # 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 :back
- ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
- `redirect_to :back` is deprecated and will be removed from Rails 5.1.
- Please use `redirect_back(fallback_location: fallback_location)` where
- `fallback_location` represents the location to use if the request has
- no HTTP referer information.
- MESSAGE
- request.headers["Referer"] or raise RedirectBackError
when Proc
- _compute_redirect_to_location request, options.call
+ _compute_redirect_to_location request, instance_eval(&options)
else
url_for(options)
end.delete("\0\r\n")
@@ -122,14 +114,20 @@ module ActionController
public :_compute_redirect_to_location
private
- def _extract_redirect_to_status(options, response_status)
+ 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_status.key?(:status)
- Rack::Utils.status_code(response_status[:status])
+ elsif response_options.key?(:status)
+ Rack::Utils.status_code(response_options[:status])
else
302
end
end
+
+ def _url_host_allowed?(url)
+ URI(url.to_s).host == request.host
+ rescue ArgumentError, URI::Error
+ false
+ end
end
end
diff --git a/actionpack/lib/action_controller/metal/renderers.rb b/actionpack/lib/action_controller/metal/renderers.rb
index 90fb34e386..b81d3ef539 100644
--- a/actionpack/lib/action_controller/metal/renderers.rb
+++ b/actionpack/lib/action_controller/metal/renderers.rb
@@ -1,4 +1,6 @@
-require 'set'
+# frozen_string_literal: true
+
+require "set"
module ActionController
# See <tt>Renderers.add</tt>
@@ -26,8 +28,7 @@ module ActionController
RENDERERS = Set.new
included do
- class_attribute :_renderers
- self._renderers = Set.new.freeze
+ class_attribute :_renderers, default: Set.new.freeze
end
# Used in <tt>ActionController::Base</tt>
@@ -71,8 +72,6 @@ module ActionController
# format.csv { render csv: @csvable, filename: @csvable.name }
# end
# end
- # To use renderers and their mime types in more concise ways, see
- # <tt>ActionController::MimeResponds::ClassMethods.respond_to</tt>
def self.add(key, &block)
define_method(_render_with_renderer_method_name(key), &block)
RENDERERS << key.to_sym
@@ -86,7 +85,7 @@ module ActionController
def self.remove(key)
RENDERERS.delete(key.to_sym)
method_name = _render_with_renderer_method_name(key)
- remove_method(method_name) if method_defined?(method_name)
+ remove_possible_method(method_name)
end
def self._render_with_renderer_method_name(key)
@@ -94,7 +93,6 @@ module ActionController
end
module ClassMethods
-
# Adds, by name, a renderer or renderers to the +_renderers+ available
# to call within controller actions.
#
@@ -103,11 +101,11 @@ module ActionController
#
# Both <tt>ActionController::Base</tt> and <tt>ActionController::API</tt>
# include <tt>ActionController::Renderers::All</tt>, making all renderers
- # avaialable in the controller. See <tt>Renderers::RENDERERS</tt> and <tt>Renderers.add</tt>.
+ # 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 lest one renderer.
+ # 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
diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb
index cce6fe7787..7d0a944381 100644
--- a/actionpack/lib/action_controller/metal/rendering.rb
+++ b/actionpack/lib/action_controller/metal/rendering.rb
@@ -1,10 +1,10 @@
-require 'active_support/core_ext/string/filters'
+# frozen_string_literal: true
module ActionController
module Rendering
extend ActiveSupport::Concern
- RENDER_FORMATS_IN_PRIORITY = [:body, :text, :plain, :html]
+ RENDER_FORMATS_IN_PRIORITY = [:body, :plain, :html]
module ClassMethods
# Documentation at ActionController::Renderer#render
@@ -32,15 +32,15 @@ module ActionController
# Check for double render errors and set the content_type after rendering.
def render(*args) #:nodoc:
- raise ::AbstractController::DoubleRenderError if self.response_body
+ raise ::AbstractController::DoubleRenderError if response_body
super
end
- # Overwrite render_to_string because body can now be set to a rack body.
+ # Overwrite render_to_string because body can now be set to a Rack body.
def render_to_string(*)
result = super
if result.respond_to?(:each)
- string = ""
+ string = +""
result.each { |r| string << r }
string
else
@@ -49,84 +49,74 @@ module ActionController
end
def render_to_body(options = {})
- super || _render_in_priorities(options) || ' '
+ super || _render_in_priorities(options) || " "
end
private
- def _render_in_priorities(options)
- RENDER_FORMATS_IN_PRIORITY.each do |format|
- return options[format] if options.key?(format)
+ def _process_variant(options)
+ if defined?(request) && !request.nil? && request.variant.present?
+ options[:variant] = request.variant
+ end
end
- nil
- end
-
- def _set_html_content_type
- self.content_type = Mime[:html].to_s
- end
+ def _render_in_priorities(options)
+ RENDER_FORMATS_IN_PRIORITY.each do |format|
+ return options[format] if options.key?(format)
+ end
- def _set_rendered_content_type(format)
- unless response.content_type
- self.content_type = format.to_s
+ nil
end
- end
- # Normalize arguments by catching blocks and setting them on :update.
- def _normalize_args(action=nil, options={}, &blk) #:nodoc:
- options = super
- options[:update] = blk if block_given?
- options
- end
-
- # Normalize both text and status options.
- def _normalize_options(options) #:nodoc:
- _normalize_text(options)
-
- if options[:text]
- ActiveSupport::Deprecation.warn <<-WARNING.squish
- `render :text` is deprecated because it does not actually render a
- `text/plain` response. Switch to `render plain: 'plain text'` to
- render as `text/plain`, `render html: '<strong>HTML</strong>'` to
- render as `text/html`, or `render body: 'raw'` to match the deprecated
- behavior and render with the default Content-Type, which is
- `text/plain`.
- WARNING
+ def _set_html_content_type
+ self.content_type = Mime[:html].to_s
end
- if options[:html]
- options[:html] = ERB::Util.html_escape(options[:html])
+ def _set_rendered_content_type(format)
+ if format && !response.content_type
+ self.content_type = format.to_s
+ end
end
- if options.delete(:nothing)
- ActiveSupport::Deprecation.warn("`:nothing` option is deprecated and will be removed in Rails 5.1. Use `head` method to respond with empty response body.")
- options[:body] = nil
+ # Normalize arguments by catching blocks and setting them on :update.
+ def _normalize_args(action = nil, options = {}, &blk)
+ options = super
+ options[:update] = blk if block_given?
+ options
end
- if options[:status]
- options[:status] = Rack::Utils.status_code(options[:status])
- end
+ # Normalize both text and status options.
+ def _normalize_options(options)
+ _normalize_text(options)
- super
- end
+ if options[:html]
+ options[:html] = ERB::Util.html_escape(options[:html])
+ end
- def _normalize_text(options)
- RENDER_FORMATS_IN_PRIORITY.each do |format|
- if options.key?(format) && options[format].respond_to?(:to_text)
- options[format] = options[format].to_text
+ if options[:status]
+ options[:status] = Rack::Utils.status_code(options[:status])
end
+
+ super
end
- end
- # Process controller specific options, as status, content-type and location.
- def _process_options(options) #:nodoc:
- status, content_type, location = options.values_at(:status, :content_type, :location)
+ 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
- self.status = status if status
- self.content_type = content_type if content_type
- self.headers["Location"] = url_for(location) if location
+ # Process controller specific options, as status, content-type and location.
+ def _process_options(options)
+ status, content_type, location = options.values_at(:status, :content_type, :location)
- super
- end
+ self.status = status if status
+ self.content_type = content_type if content_type
+ headers["Location"] = url_for(location) if location
+
+ super
+ end
end
end
diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb
index b2f0b382b9..4bf8d90b69 100644
--- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb
+++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb
@@ -1,6 +1,8 @@
-require 'rack/session/abstract/id'
-require 'action_controller/metal/exceptions'
-require 'active_support/security_utils'
+# frozen_string_literal: true
+
+require "rack/session/abstract/id"
+require "action_controller/metal/exceptions"
+require "active_support/security_utils"
module ActionController #:nodoc:
class InvalidAuthenticityToken < ActionControllerError #:nodoc:
@@ -15,12 +17,12 @@ module ActionController #:nodoc:
# access. When a request reaches your application, \Rails verifies the received
# token with the token in the session. All requests are checked except GET requests
# as these should be idempotent. Keep in mind that all session-oriented requests
- # should be CSRF protected, including JavaScript and HTML requests.
+ # 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 `protect_from_forgery` method in our controllers.
+ # 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
@@ -28,16 +30,23 @@ module ActionController #:nodoc:
# URL on your site. When your JavaScript response loads on their site, it executes.
# With carefully crafted JavaScript on their end, sensitive data in your JavaScript
# response may be extracted. To prevent this, only XmlHttpRequest (known as XHR or
- # Ajax) requests are allowed to make GET requests for JavaScript responses.
+ # Ajax) requests are allowed to make requests for JavaScript responses.
#
- # It's important to remember that XML or JSON requests are also affected and if
- # you're building an API you should change forgery protection method in
+ # It's important to remember that XML or JSON requests are also checked by default. If
+ # you're building an API or an SPA you could change forgery protection method in
# <tt>ApplicationController</tt> (by default: <tt>:exception</tt>):
#
# class ApplicationController < ActionController::Base
# protect_from_forgery unless: -> { request.format.json? }
# end
#
+ # It is generally safe to exclude XHR requests from CSRF protection
+ # (like the code snippet above does), because XHR requests can only be made from
+ # the same origin. Note however that any cross-origin third party domain
+ # allowed via {CORS}[https://en.wikipedia.org/wiki/Cross-origin_resource_sharing]
+ # will also be able to create XHR requests. Be sure to check your
+ # CORS configuration before disabling forgery protection for XHR.
+ #
# CSRF protection is turned on with the <tt>protect_from_forgery</tt> method.
# By default <tt>protect_from_forgery</tt> protects your session with
# <tt>:null_session</tt> method, which provides an empty session
@@ -52,7 +61,7 @@ module ActionController #:nodoc:
# <tt>csrf_meta_tags</tt> in the HTML +head+.
#
# Learn more about CSRF attacks and securing your application in the
- # {Ruby on Rails Security Guide}[http://guides.rubyonrails.org/security.html].
+ # {Ruby on Rails Security Guide}[https://guides.rubyonrails.org/security.html].
module RequestForgeryProtection
extend ActiveSupport::Concern
@@ -85,6 +94,10 @@ module ActionController #:nodoc:
config_accessor :per_form_csrf_tokens
self.per_form_csrf_tokens = false
+ # Controls whether forgery protection is enabled by default.
+ config_accessor :default_protect_from_forgery
+ self.default_protect_from_forgery = false
+
helper_method :form_authenticity_token
helper_method :protect_against_forgery?
end
@@ -109,10 +122,10 @@ module ActionController #:nodoc:
# * <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).
+ # 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>.
+ # 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:
@@ -128,13 +141,22 @@ module ActionController #:nodoc:
append_after_action :verify_same_origin_request
end
+ # Turn off request forgery protection. This is a wrapper for:
+ #
+ # skip_before_action :verify_authenticity_token
+ #
+ # See +skip_before_action+ for allowed options.
+ def skip_forgery_protection(options = {})
+ skip_before_action :verify_authenticity_token, options
+ end
+
private
- def protection_method_class(name)
- ActionController::RequestForgeryProtection::ProtectionMethods.const_get(name.to_s.classify)
- rescue NameError
- raise ArgumentError, 'Invalid request forgery protection method, use :null_session, :exception, or :reset_session'
- end
+ def protection_method_class(name)
+ ActionController::RequestForgeryProtection::ProtectionMethods.const_get(name.to_s.classify)
+ rescue NameError
+ raise ArgumentError, "Invalid request forgery protection method, use :null_session, :exception, or :reset_session"
+ end
end
module ProtectionMethods
@@ -152,28 +174,28 @@ module ActionController #:nodoc:
request.cookie_jar = NullCookieJar.build(request, {})
end
- protected
+ private
- class NullSessionHash < Rack::Session::Abstract::SessionHash #:nodoc:
- def initialize(req)
- super(nil, req)
- @data = {}
- @loaded = true
- end
+ class NullSessionHash < Rack::Session::Abstract::SessionHash #:nodoc:
+ def initialize(req)
+ super(nil, req)
+ @data = {}
+ @loaded = true
+ end
- # no-op
- def destroy; end
+ # no-op
+ def destroy; end
- def exists?
- true
+ def exists?
+ true
+ end
end
- end
- class NullCookieJar < ActionDispatch::Cookies::CookieJar #:nodoc:
- def write(*)
- # nothing
+ class NullCookieJar < ActionDispatch::Cookies::CookieJar #:nodoc:
+ def write(*)
+ # nothing
+ end
end
- end
end
class ResetSession
@@ -197,29 +219,33 @@ module ActionController #:nodoc:
end
end
- protected
+ private
# The actual before_action that is used to verify the CSRF token.
# Don't override this directly. Provide your own forgery protection
# strategy instead. If you override, you'll disable same-origin
- # `<script>` verification.
+ # <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.
- def verify_authenticity_token
+ def verify_authenticity_token # :doc:
mark_for_same_origin_verification!
if !verified_request?
if logger && log_warning_on_csrf_failure
- logger.warn "Can't verify CSRF token authenticity"
+ if valid_request_origin?
+ logger.warn "Can't verify CSRF token authenticity."
+ else
+ logger.warn "HTTP Origin header (#{request.origin}) didn't match request.base_url (#{request.base_url})"
+ end
end
handle_unverified_request
end
end
- def handle_unverified_request
+ def handle_unverified_request # :doc:
forgery_protection_strategy.new(self).handle_unverified_request
end
@@ -229,54 +255,57 @@ module ActionController #:nodoc:
"If you know what you're doing, go ahead and disable forgery " \
"protection on this action to permit cross-origin JavaScript embedding."
private_constant :CROSS_ORIGIN_JAVASCRIPT_WARNING
+ # :startdoc:
- # If `verify_authenticity_token` was run (indicating that we have
+ # If +verify_authenticity_token+ was run (indicating that we have
# forgery protection enabled for this request) then also verify that
# we aren't serving an unauthorized cross-origin response.
- def verify_same_origin_request
+ def verify_same_origin_request # :doc:
if marked_for_same_origin_verification? && non_xhr_javascript_response?
- logger.warn CROSS_ORIGIN_JAVASCRIPT_WARNING if logger
+ if logger && log_warning_on_csrf_failure
+ logger.warn CROSS_ORIGIN_JAVASCRIPT_WARNING
+ end
raise ActionController::InvalidCrossOriginRequest, CROSS_ORIGIN_JAVASCRIPT_WARNING
end
end
# GET requests are checked for cross-origin JavaScript after rendering.
- def mark_for_same_origin_verification!
+ 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
+ # If the +verify_authenticity_token+ before_action ran, verify that
# JavaScript responses are only served to same-origin GET requests.
- def marked_for_same_origin_verification?
+ def marked_for_same_origin_verification? # :doc:
@marked_for_same_origin_verification ||= false
end
# Check for cross-origin JavaScript responses.
- def non_xhr_javascript_response?
- content_type =~ %r(\Atext/javascript) && !request.xhr?
+ def non_xhr_javascript_response? # :doc:
+ content_type =~ %r(\A(?:text|application)/javascript) && !request.xhr?
end
AUTHENTICITY_TOKEN_LENGTH = 32
# Returns true or false if a request is verified. Checks:
#
- # * Is it a GET or HEAD request? Gets should be safe and idempotent
+ # * Is it a GET or HEAD request? GETs should be safe and idempotent
# * Does the form_authenticity_token match the given token value from the params?
- # * Does the X-CSRF-Token header match the form_authenticity_token
- def verified_request?
+ # * Does the X-CSRF-Token header match the form_authenticity_token?
+ def verified_request? # :doc:
!protect_against_forgery? || request.get? || request.head? ||
(valid_request_origin? && any_authenticity_token_valid?)
end
# Checks if any of the authenticity tokens from the request are valid.
- def any_authenticity_token_valid?
+ def any_authenticity_token_valid? # :doc:
request_authenticity_tokens.any? do |token|
valid_authenticity_token?(session, token)
end
end
# Possible authenticity tokens sent in the request.
- def request_authenticity_tokens
+ def request_authenticity_tokens # :doc:
[form_authenticity_param, request.x_csrf_token]
end
@@ -288,7 +317,7 @@ module ActionController #:nodoc:
# Creates a masked version of the authenticity token that varies
# on each request. The masking is used to mitigate SSL attacks
# like BREACH.
- def masked_authenticity_token(session, form_options: {})
+ 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
@@ -307,7 +336,7 @@ module ActionController #:nodoc:
# Checks the client's masked token to see if it matches the
# session token. Essentially the inverse of
# +masked_authenticity_token+.
- def valid_authenticity_token?(session, encoded_masked_token)
+ 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
@@ -325,7 +354,7 @@ module ActionController #:nodoc:
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
+ # happening shortly after installing this gem.
compare_with_real_token masked_token, session
elsif masked_token.length == AUTHENTICITY_TOKEN_LENGTH * 2
@@ -334,23 +363,23 @@ module ActionController #:nodoc:
compare_with_real_token(csrf_token, session) ||
valid_per_form_csrf_token?(csrf_token, session)
else
- false # Token is malformed
+ false # Token is malformed.
end
end
- def unmask_token(masked_token)
+ def unmask_token(masked_token) # :doc:
# Split the token into the one-time pad and the encrypted
- # value and decrypt it
+ # value and decrypt it.
one_time_pad = masked_token[0...AUTHENTICITY_TOKEN_LENGTH]
encrypted_csrf_token = masked_token[AUTHENTICITY_TOKEN_LENGTH..-1]
xor_byte_strings(one_time_pad, encrypted_csrf_token)
end
- def compare_with_real_token(token, session)
- ActiveSupport::SecurityUtils.secure_compare(token, real_csrf_token(session))
+ def compare_with_real_token(token, session) # :doc:
+ ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, real_csrf_token(session))
end
- def valid_per_form_csrf_token?(token, session)
+ def valid_per_form_csrf_token?(token, session) # :doc:
if per_form_csrf_tokens
correct_token = per_form_csrf_token(
session,
@@ -358,18 +387,18 @@ module ActionController #:nodoc:
request.request_method
)
- ActiveSupport::SecurityUtils.secure_compare(token, correct_token)
+ ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, correct_token)
else
false
end
end
- def real_csrf_token(session)
+ def real_csrf_token(session) # :doc:
session[:_csrf_token] ||= SecureRandom.base64(AUTHENTICITY_TOKEN_LENGTH)
Base64.strict_decode64(session[:_csrf_token])
end
- def per_form_csrf_token(session, action_path, method)
+ def per_form_csrf_token(session, action_path, method) # :doc:
OpenSSL::HMAC.digest(
OpenSSL::Digest::SHA256.new,
real_csrf_token(session),
@@ -377,35 +406,51 @@ module ActionController #:nodoc:
)
end
- def xor_byte_strings(s1, s2)
- s2_bytes = s2.bytes
- s1.each_byte.with_index { |c1, i| s2_bytes[i] ^= c1 }
- s2_bytes.pack('C*')
+ 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.
- def form_authenticity_param
+ def form_authenticity_param # :doc:
params[request_forgery_protection_token]
end
# Checks if the controller allows forgery protection.
- def protect_against_forgery?
+ def protect_against_forgery? # :doc:
allow_forgery_protection
end
+ 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.
- def valid_request_origin?
+ 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
- def normalize_action_path(action_path)
- action_path.split('?').first.to_s.chomp('/')
+ def normalize_action_path(action_path) # :doc:
+ uri = URI.parse(action_path)
+ uri.path.chomp("/")
end
end
end
diff --git a/actionpack/lib/action_controller/metal/rescue.rb b/actionpack/lib/action_controller/metal/rescue.rb
index 0621a7368c..44f7fb7a07 100644
--- a/actionpack/lib/action_controller/metal/rescue.rb
+++ b/actionpack/lib/action_controller/metal/rescue.rb
@@ -1,27 +1,18 @@
+# frozen_string_literal: true
+
module ActionController #:nodoc:
- # This module is responsible to provide `rescue_from` helpers
- # to controllers and configure when detailed exceptions must be
+ # This module is responsible for providing +rescue_from+ helpers
+ # to controllers and configuring when detailed exceptions must be
# shown.
module Rescue
extend ActiveSupport::Concern
include ActiveSupport::Rescuable
- def rescue_with_handler(exception)
- if exception.cause
- handler_index = index_of_handler_for_rescue(exception) || Float::INFINITY
- cause_handler_index = index_of_handler_for_rescue(exception.cause)
- if cause_handler_index && cause_handler_index <= handler_index
- exception = exception.cause
- end
- end
- super(exception)
- end
-
# Override this method if you want to customize when detailed
# exceptions must be shown. This method is only called when
- # consider_all_requests_local is false. By default, it returns
- # false, but someone may set it to `request.local?` so local
- # requests in production still shows the detailed exception pages.
+ # +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.
def show_detailed_exceptions?
false
end
@@ -30,8 +21,8 @@ module ActionController #:nodoc:
def process_action(*args)
super
rescue Exception => exception
- request.env['action_dispatch.show_detailed_exceptions'] ||= show_detailed_exceptions?
- rescue_with_handler(exception) || raise(exception)
+ request.env["action_dispatch.show_detailed_exceptions"] ||= show_detailed_exceptions?
+ rescue_with_handler(exception) || raise
end
end
end
diff --git a/actionpack/lib/action_controller/metal/streaming.rb b/actionpack/lib/action_controller/metal/streaming.rb
index a6115674aa..8dc01a5eb9 100644
--- a/actionpack/lib/action_controller/metal/streaming.rb
+++ b/actionpack/lib/action_controller/metal/streaming.rb
@@ -1,9 +1,11 @@
-require 'rack/chunked'
+# frozen_string_literal: true
+
+require "rack/chunked"
module ActionController #:nodoc:
# Allows views to be streamed back to the client as they are rendered.
#
- # The default way Rails renders views is by first rendering the template
+ # 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.
#
@@ -181,7 +183,7 @@ module ActionController #:nodoc:
# unicorn_rails --config-file unicorn.config.rb
#
# You may also want to configure other parameters like <tt>:tcp_nodelay</tt>.
- # Please check its documentation for more information: http://unicorn.bogomips.org/Unicorn/Configurator.html#method-i-listen
+ # 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.
@@ -193,10 +195,10 @@ module ActionController #:nodoc:
module Streaming
extend ActiveSupport::Concern
- protected
+ private
# Set proper cache control and transfer encoding when streaming
- def _process_options(options) #:nodoc:
+ def _process_options(options)
super
if options[:stream]
if request.version == "HTTP/1.0"
@@ -210,7 +212,7 @@ module ActionController #:nodoc:
end
# Call render_body if we are streaming instead of usual +render+.
- def _render_template(options) #:nodoc:
+ def _render_template(options)
if options.delete(:stream)
Rack::Chunked::Body.new view_renderer.render_body(view_context, options)
else
diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb
index 64672de57e..815f82a1f2 100644
--- a/actionpack/lib/action_controller/metal/strong_parameters.rb
+++ b/actionpack/lib/action_controller/metal/strong_parameters.rb
@@ -1,12 +1,15 @@
-require 'active_support/core_ext/hash/indifferent_access'
-require 'active_support/core_ext/hash/transform_values'
-require 'active_support/core_ext/array/wrap'
-require 'active_support/core_ext/string/filters'
-require 'active_support/rescuable'
-require 'action_dispatch/http/upload'
-require 'rack/test'
-require 'stringio'
-require 'set'
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/indifferent_access"
+require "active_support/core_ext/array/wrap"
+require "active_support/core_ext/string/filters"
+require "active_support/core_ext/object/to_query"
+require "active_support/rescuable"
+require "action_dispatch/http/upload"
+require "rack/test"
+require "stringio"
+require "set"
+require "yaml"
module ActionController
# Raised when a required parameter is missing.
@@ -31,19 +34,31 @@ module ActionController
#
# params = ActionController::Parameters.new(a: "123", b: "456")
# params.permit(:c)
- # # => ActionController::UnpermittedParameters: found unpermitted parameters: a, b
+ # # => ActionController::UnpermittedParameters: found unpermitted parameters: :a, :b
class UnpermittedParameters < IndexError
attr_reader :params # :nodoc:
def initialize(params) # :nodoc:
@params = params
- super("found unpermitted parameter#{'s' if params.size > 1 }: #{params.join(", ")}")
+ super("found unpermitted parameter#{'s' if params.size > 1 }: #{params.map { |e| ":#{e}" }.join(", ")}")
+ end
+ end
+
+ # Raised when a Parameters instance is not marked as permitted and
+ # an operation to transform it to hash is called.
+ #
+ # params = ActionController::Parameters.new(a: "123", b: "456")
+ # params.to_h
+ # # => ActionController::UnfilteredParameters: unable to convert unpermitted parameters to hash
+ class UnfilteredParameters < ArgumentError
+ def initialize # :nodoc:
+ super("unable to convert unpermitted parameters to hash")
end
end
# == Action Controller \Parameters
#
- # Allows to choose which attributes should be whitelisted for mass updating
+ # 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
@@ -51,15 +66,14 @@ module ActionController
#
# params = ActionController::Parameters.new({
# person: {
- # name: 'Francesco',
+ # name: "Francesco",
# age: 22,
- # role: 'admin'
+ # role: "admin"
# }
# })
#
# permitted = params.require(:person).permit(:name, :age)
- # permitted # => {"name"=>"Francesco", "age"=>22}
- # permitted.class # => ActionController::Parameters
+ # permitted # => <ActionController::Parameters {"name"=>"Francesco", "age"=>22} permitted: true>
# permitted.permitted? # => true
#
# Person.first.update!(permitted)
@@ -70,8 +84,8 @@ module ActionController
# * +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 <tt>:log</tt> to
- # write a message on the logger or <tt>:raise</tt> to raise
+ # 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.
#
@@ -87,7 +101,7 @@ module ActionController
#
# params = ActionController::Parameters.new(a: "123", b: "456")
# params.permit(:c)
- # # => {}
+ # # => <ActionController::Parameters {} permitted: true>
#
# ActionController::Parameters.action_on_unpermitted_parameters = :raise
#
@@ -102,15 +116,104 @@ module ActionController
# 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 = ActionController::Parameters.new(key: "value")
# params[:key] # => "value"
# params["key"] # => "value"
class Parameters
- cattr_accessor :permit_all_parameters, instance_accessor: false
+ cattr_accessor :permit_all_parameters, instance_accessor: false, default: false
+
cattr_accessor :action_on_unpermitted_parameters, instance_accessor: false
+ ##
+ # :method: as_json
+ #
+ # :call-seq:
+ # as_json(options=nil)
+ #
+ # Returns a hash that can be used as the JSON representation for the parameters.
+
+ ##
+ # :method: 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: keys
+ #
+ # :call-seq:
+ # keys()
+ #
+ # Returns a new array of the keys of the parameters.
+
+ ##
+ # :method: to_s
+ #
+ # :call-seq:
+ # to_s()
+ #
+ # Returns the content of the parameters as a string.
+
+ ##
+ # :method: value?
+ #
+ # :call-seq:
+ # value?(value)
+ #
+ # Returns true if the given value is present for some key in the parameters.
+
+ ##
+ # :method: values
+ #
+ # :call-seq:
+ # values()
+ #
+ # Returns a new array of the values of the parameters.
delegate :keys, :key?, :has_key?, :values, :has_value?, :value?, :empty?, :include?,
- :as_json, to: :@parameters
+ :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'
@@ -119,8 +222,7 @@ module ActionController
# config. For instance:
#
# config.always_permitted_parameters = %w( controller action format )
- cattr_accessor :always_permitted_parameters
- self.always_permitted_parameters = %w( controller action )
+ cattr_accessor :always_permitted_parameters, default: %w( controller action )
# Returns a new instance of <tt>ActionController::Parameters</tt>.
# Also, sets the +permitted+ attribute to the default value of
@@ -129,13 +231,13 @@ module ActionController
# class Person < ActiveRecord::Base
# end
#
- # params = ActionController::Parameters.new(name: 'Francesco')
+ # 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 = ActionController::Parameters.new(name: "Francesco")
# params.permitted? # => true
# Person.new(params) # => #<Person id: nil, name: "Francesco">
def initialize(parameters = {})
@@ -147,29 +249,21 @@ module ActionController
# permitted flag.
def ==(other)
if other.respond_to?(:permitted?)
- self.permitted? == other.permitted? && self.parameters == other.parameters
- elsif other.is_a?(Hash)
- ActiveSupport::Deprecation.warn <<-WARNING.squish
- Comparing equality between `ActionController::Parameters` and a
- `Hash` is deprecated and will be removed in Rails 5.1. Please only do
- comparisons between instances of `ActionController::Parameters`. If
- you need to compare to a hash, first convert it using
- `ActionController::Parameters#new`.
- WARNING
- @parameters == other.with_indifferent_access
+ permitted? == other.permitted? && parameters == other.parameters
else
@parameters == other
end
end
# Returns a safe <tt>ActiveSupport::HashWithIndifferentAccess</tt>
- # representation of this parameter with all unpermitted keys removed.
+ # representation of the parameters with all unpermitted keys removed.
#
# params = ActionController::Parameters.new({
- # name: 'Senjougahara Hitagi',
- # oddity: 'Heavy stone crab'
+ # name: "Senjougahara Hitagi",
+ # oddity: "Heavy stone crab"
# })
- # params.to_h # => {}
+ # params.to_h
+ # # => ActionController::UnfilteredParameters: unable to convert unpermitted parameters to hash
#
# safe_params = params.permit(:name)
# safe_params.to_h # => {"name"=>"Senjougahara Hitagi"}
@@ -177,17 +271,66 @@ module ActionController
if permitted?
convert_parameters_to_hashes(@parameters, :to_h)
else
- slice(*self.class.always_permitted_parameters).permit!.to_h
+ raise UnfilteredParameters
end
end
+ # Returns a safe <tt>Hash</tt> representation of the parameters
+ # with all unpermitted keys removed.
+ #
+ # params = ActionController::Parameters.new({
+ # name: "Senjougahara Hitagi",
+ # oddity: "Heavy stone crab"
+ # })
+ # params.to_hash
+ # # => ActionController::UnfilteredParameters: unable to convert unpermitted parameters to hash
+ #
+ # safe_params = params.permit(:name)
+ # safe_params.to_hash # => {"name"=>"Senjougahara Hitagi"}
+ def to_hash
+ to_h.to_hash
+ end
+
+ # Returns a string representation of the receiver suitable for use as a URL
+ # query string:
+ #
+ # params = ActionController::Parameters.new({
+ # name: "David",
+ # nationality: "Danish"
+ # })
+ # params.to_query
+ # # => ActionController::UnfilteredParameters: unable to convert unpermitted parameters to hash
+ #
+ # safe_params = params.permit(:name, :nationality)
+ # safe_params.to_query
+ # # => "name=David&nationality=Danish"
+ #
+ # An optional namespace can be passed to enclose key names:
+ #
+ # params = ActionController::Parameters.new({
+ # name: "David",
+ # nationality: "Danish"
+ # })
+ # safe_params = params.permit(:name, :nationality)
+ # safe_params.to_query("user")
+ # # => "user%5Bname%5D=David&user%5Bnationality%5D=Danish"
+ #
+ # The string pairs "key=value" that conform the query string
+ # are sorted lexicographically in ascending order.
+ #
+ # This method is also aliased as +to_param+.
+ def to_query(*args)
+ to_h.to_query(*args)
+ end
+ alias_method :to_param, :to_query
+
# Returns an unsafe, unfiltered
- # <tt>ActiveSupport::HashWithIndifferentAccess</tt> representation of this
- # parameter.
+ # <tt>ActiveSupport::HashWithIndifferentAccess</tt> representation of the
+ # parameters.
#
# params = ActionController::Parameters.new({
- # name: 'Senjougahara Hitagi',
- # oddity: 'Heavy stone crab'
+ # name: "Senjougahara Hitagi",
+ # oddity: "Heavy stone crab"
# })
# params.to_unsafe_h
# # => {"name"=>"Senjougahara Hitagi", "oddity" => "Heavy stone crab"}
@@ -196,15 +339,23 @@ module ActionController
end
alias_method :to_unsafe_hash, :to_unsafe_h
- # Convert all hashes in values into parameters, then yield each pair like
- # the same way as <tt>Hash#each_pair</tt>
+ # Convert all hashes in values into parameters, then yield each pair in
+ # the same way as <tt>Hash#each_pair</tt>.
def each_pair(&block)
@parameters.each_pair do |key, value|
- yield key, convert_hashes_to_parameters(key, value)
+ yield [key, convert_hashes_to_parameters(key, value)]
end
end
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>.
+ def each_value(&block)
+ @parameters.each_pair do |key, value|
+ yield convert_hashes_to_parameters(key, value)
+ end
+ 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.
@@ -232,7 +383,7 @@ module ActionController
# class Person < ActiveRecord::Base
# end
#
- # params = ActionController::Parameters.new(name: 'Francesco')
+ # params = ActionController::Parameters.new(name: "Francesco")
# params.permitted? # => false
# Person.new(params) # => ActiveModel::ForbiddenAttributesError
# params.permit!
@@ -240,7 +391,7 @@ module ActionController
# Person.new(params) # => #<Person id: nil, name: "Francesco">
def permit!
each_pair do |key, value|
- Array.wrap(value).each do |v|
+ Array.wrap(value).flatten.each do |v|
v.permit! if v.respond_to? :permit!
end
end
@@ -254,8 +405,8 @@ module ActionController
# 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)
- # # => {"name"=>"Francesco"}
+ # ActionController::Parameters.new(person: { name: "Francesco" }).require(:person)
+ # # => <ActionController::Parameters {"name"=>"Francesco"} permitted: false>
#
# Otherwise raises <tt>ActionController::ParameterMissing</tt>:
#
@@ -276,18 +427,18 @@ module ActionController
# returned:
#
# params = ActionController::Parameters.new(user: { ... }, profile: { ... })
- # user_params, profile_params = params.require(:user, :profile)
+ # user_params, profile_params = params.require([:user, :profile])
#
- # Otherwise, the method reraises the first exception found:
+ # Otherwise, the method re-raises the first exception found:
#
# params = ActionController::Parameters.new(user: {}, profile: {})
- # user_params, profile_params = params.require(: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' })
+ # 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:
@@ -317,7 +468,7 @@ module ActionController
# 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' })
+ # 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
@@ -337,18 +488,27 @@ module ActionController
# 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 = 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',
+ # name: "Francesco",
# age: 22,
# pets: [{
- # name: 'Purplish',
- # category: 'dogs'
+ # name: "Purplish",
+ # category: "dogs"
# }]
# }
# })
@@ -362,25 +522,25 @@ module ActionController
#
# Note that if you use +permit+ in a key that points to a hash,
# it won't allow all the hash. You also need to specify which
- # attributes inside the hash should be whitelisted.
+ # attributes inside the hash should be permitted.
#
# params = ActionController::Parameters.new({
# person: {
# contact: {
- # email: 'none@test.com',
- # phone: '555-1234'
+ # email: "none@test.com",
+ # phone: "555-1234"
# }
# }
# })
#
# params.require(:person).permit(:contact)
- # # => {}
+ # # => <ActionController::Parameters {} permitted: true>
#
# params.require(:person).permit(contact: :phone)
- # # => {"contact"=>{"phone"=>"555-1234"}}
+ # # => <ActionController::Parameters {"contact"=><ActionController::Parameters {"phone"=>"555-1234"} permitted: true>} permitted: true>
#
# params.require(:person).permit(contact: [ :email, :phone ])
- # # => {"contact"=>{"email"=>"none@test.com", "phone"=>"555-1234"}}
+ # # => <ActionController::Parameters {"contact"=><ActionController::Parameters {"email"=>"none@test.com", "phone"=>"555-1234"} permitted: true>} permitted: true>
def permit(*filters)
params = self.class.new
@@ -388,7 +548,7 @@ module ActionController
case filter
when Symbol, String
permitted_scalar_filter(params, filter)
- when Hash then
+ when Hash
hash_filter(params, filter)
end
end
@@ -401,8 +561,8 @@ module ActionController
# Returns a parameter for the given +key+. If not found,
# returns +nil+.
#
- # params = ActionController::Parameters.new(person: { name: 'Francesco' })
- # params[:person] # => {"name"=>"Francesco"}
+ # params = ActionController::Parameters.new(person: { name: "Francesco" })
+ # params[:person] # => <ActionController::Parameters {"name"=>"Francesco"} permitted: false>
# params[:none] # => nil
def [](key)
convert_hashes_to_parameters(key, @parameters[key])
@@ -417,14 +577,16 @@ module ActionController
# Returns a parameter for the given +key+. If the +key+
# can't be found, there are several options: With no other arguments,
# it will raise an <tt>ActionController::ParameterMissing</tt> error;
- # if more arguments are given, then that will be returned; if a block
+ # 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) # => {"name"=>"Francesco"}
+ # params = ActionController::Parameters.new(person: { name: "Francesco" })
+ # params.fetch(:person) # => <ActionController::Parameters {"name"=>"Francesco"} permitted: false>
# params.fetch(:none) # => ActionController::ParameterMissing: param is missing or the value is empty: none
- # params.fetch(:none, 'Francesco') # => "Francesco"
- # params.fetch(:none) { 'Francesco' } # => "Francesco"
+ # params.fetch(:none, {}) # => <ActionController::Parameters {} permitted: false>
+ # params.fetch(:none, "Francesco") # => "Francesco"
+ # params.fetch(:none) { "Francesco" } # => "Francesco"
def fetch(key, *args)
convert_value_to_parameters(
@parameters.fetch(key) {
@@ -437,19 +599,18 @@ module ActionController
)
end
- if Hash.method_defined?(:dig)
- # Extracts the nested parameter from the given +keys+ by calling +dig+
- # at each step. Returns +nil+ if any intermediate step is +nil+.
- #
- # params = ActionController::Parameters.new(foo: { bar: { baz: 1 } })
- # params.dig(:foo, :bar, :baz) # => 1
- # params.dig(:foo, :zot, :xyz) # => nil
- #
- # params2 = ActionController::Parameters.new(foo: [10, 11, 12])
- # params2.dig(:foo, 1) # => 11
- def dig(*keys)
- convert_value_to_parameters(@parameters.dig(*keys))
- end
+ # Extracts the nested parameter from the given +keys+ by calling +dig+
+ # at each step. Returns +nil+ if any intermediate step is +nil+.
+ #
+ # params = ActionController::Parameters.new(foo: { bar: { baz: 1 } })
+ # params.dig(:foo, :bar, :baz) # => 1
+ # params.dig(:foo, :zot, :xyz) # => nil
+ #
+ # params2 = ActionController::Parameters.new(foo: [10, 11, 12])
+ # params2.dig(:foo, 1) # => 11
+ def dig(*keys)
+ convert_hashes_to_parameters(keys.first, @parameters[keys.first])
+ @parameters.dig(*keys)
end
# Returns a new <tt>ActionController::Parameters</tt> instance that
@@ -457,8 +618,8 @@ module ActionController
# don't exist, returns an empty hash.
#
# params = ActionController::Parameters.new(a: 1, b: 2, c: 3)
- # params.slice(:a, :b) # => {"a"=>1, "b"=>2}
- # params.slice(:d) # => {}
+ # params.slice(:a, :b) # => <ActionController::Parameters {"a"=>1, "b"=>2} permitted: false>
+ # params.slice(:d) # => <ActionController::Parameters {} permitted: false>
def slice(*keys)
new_instance_with_inherited_permitted_status(@parameters.slice(*keys))
end
@@ -474,8 +635,8 @@ module ActionController
# filters out the given +keys+.
#
# params = ActionController::Parameters.new(a: 1, b: 2, c: 3)
- # params.except(:a, :b) # => {"c"=>3}
- # params.except(:d) # => {"a"=>1,"b"=>2,"c"=>3}
+ # params.except(:a, :b) # => <ActionController::Parameters {"c"=>3} permitted: false>
+ # params.except(:d) # => <ActionController::Parameters {"a"=>1, "b"=>2, "c"=>3} permitted: false>
def except(*keys)
new_instance_with_inherited_permitted_status(@parameters.except(*keys))
end
@@ -483,8 +644,8 @@ module ActionController
# 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) # => {"a"=>1, "b"=>2}
- # params # => {"c"=>3}
+ # params.extract!(:a, :b) # => <ActionController::Parameters {"a"=>1, "b"=>2} permitted: false>
+ # params # => <ActionController::Parameters {"c"=>3} permitted: false>
def extract!(*keys)
new_instance_with_inherited_permitted_status(@parameters.extract!(*keys))
end
@@ -494,21 +655,19 @@ module ActionController
#
# params = ActionController::Parameters.new(a: 1, b: 2, c: 3)
# params.transform_values { |x| x * 2 }
- # # => {"a"=>2, "b"=>4, "c"=>6}
- def transform_values(&block)
- if block
- new_instance_with_inherited_permitted_status(
- @parameters.transform_values(&block)
- )
- else
- @parameters.transform_values
- end
+ # # => <ActionController::Parameters {"a"=>2, "b"=>4, "c"=>6} permitted: false>
+ 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.
- def transform_values!(&block)
- @parameters.transform_values!(&block)
+ def transform_values!
+ return to_enum(:transform_values!) unless block_given?
+ @parameters.transform_values! { |v| yield convert_value_to_parameters(v) }
self
end
@@ -531,12 +690,12 @@ module ActionController
self
end
- # Deletes and returns a key-value pair from +Parameters+ whose key is equal
- # to key. If the key is not found, returns the default value. If the
- # optional code block is given and the key is not found, pass in the key
- # and return the result of block.
- def delete(key)
- convert_value_to_parameters(@parameters.delete(key))
+ # Deletes a key-value pair from +Parameters+ and returns the value. If
+ # +key+ is not found, returns +nil+ (or, with optional code block, yields
+ # +key+ and returns the result). Cf. +#extract!+, which returns the
+ # corresponding +ActionController::Parameters+ object.
+ def delete(key, &block)
+ convert_value_to_parameters(@parameters.delete(key, &block))
end
# Returns a new instance of <tt>ActionController::Parameters</tt> with only
@@ -545,7 +704,7 @@ module ActionController
new_instance_with_inherited_permitted_status(@parameters.select(&block))
end
- # Equivalent to Hash#keep_if, but returns nil if no changes were made.
+ # Equivalent to Hash#keep_if, but returns +nil+ if no changes were made.
def select!(&block)
@parameters.select!(&block)
self
@@ -571,27 +730,37 @@ module ActionController
convert_value_to_parameters(@parameters.values_at(*keys))
end
- # Returns an exact copy of the <tt>ActionController::Parameters</tt>
- # instance. +permitted+ state is kept on the duped object.
- #
- # params = ActionController::Parameters.new(a: 1)
- # params.permit!
- # params.permitted? # => true
- # copy_params = params.dup # => {"a"=>1}
- # copy_params.permitted? # => true
- def dup
- super.tap do |duplicate|
- duplicate.permitted = @permitted
- end
+ # Returns a new <tt>ActionController::Parameters</tt> with all keys from
+ # +other_hash+ merged into current hash.
+ def merge(other_hash)
+ new_instance_with_inherited_permitted_status(
+ @parameters.merge(other_hash.to_h)
+ )
+ end
+
+ # Returns current <tt>ActionController::Parameters</tt> instance with
+ # +other_hash+ merged into current hash.
+ def merge!(other_hash)
+ @parameters.merge!(other_hash.to_h)
+ self
end
# Returns a new <tt>ActionController::Parameters</tt> with all keys from
- # +other_hash+ merges into current hash.
- def merge(other_hash)
+ # current hash merged into +other_hash+.
+ def reverse_merge(other_hash)
new_instance_with_inherited_permitted_status(
- @parameters.merge(other_hash)
+ other_hash.to_h.merge(@parameters)
)
end
+ alias_method :with_defaults, :reverse_merge
+
+ # Returns current <tt>ActionController::Parameters</tt> instance with
+ # current hash merged into +other_hash+.
+ def reverse_merge!(other_hash)
+ @parameters.merge!(other_hash.to_h) { |key, left, right| left }
+ self
+ end
+ alias_method :with_defaults!, :reverse_merge!
# This is required by ActiveModel attribute assignment, so that user can
# pass +Parameters+ to a mass assignment methods in a model. It should not
@@ -604,30 +773,44 @@ module ActionController
"<#{self.class} #{@parameters} permitted: #{@permitted}>"
end
- def method_missing(method_sym, *args, &block)
- if @parameters.respond_to?(method_sym)
- message = <<-DEPRECATE.squish
- Method #{method_sym} is deprecated and will be removed in Rails 5.1,
- as `ActionController::Parameters` no longer inherits from
- hash. Using this deprecated behavior exposes potential security
- problems. If you continue to use this method you may be creating
- a security vulnerability in your app that can be exploited. Instead,
- consider using one of these documented methods which are not
- deprecated: http://api.rubyonrails.org/v#{ActionPack.version}/classes/ActionController/Parameters.html
- DEPRECATE
- ActiveSupport::Deprecation.warn(message)
- @parameters.public_send(method_sym, *args, &block)
- else
- super
+ def self.hook_into_yaml_loading # :nodoc:
+ # Wire up YAML format compatibility with Rails 4.2 and Psych 2.0.8 and 2.0.9+.
+ # Makes the YAML parser call `init_with` when it encounters the keys below
+ # instead of trying its own parsing routines.
+ YAML.load_tags["!ruby/hash-with-ivars:ActionController::Parameters"] = name
+ YAML.load_tags["!ruby/hash:ActionController::Parameters"] = name
+ end
+ hook_into_yaml_loading
+
+ def init_with(coder) # :nodoc:
+ case coder.tag
+ when "!ruby/hash:ActionController::Parameters"
+ # YAML 2.0.8's format where hash instance variables weren't stored.
+ @parameters = coder.map.with_indifferent_access
+ @permitted = false
+ when "!ruby/hash-with-ivars:ActionController::Parameters"
+ # YAML 2.0.9's Hash subclass format where keys and values
+ # were stored under an elements hash and `permitted` within an ivars hash.
+ @parameters = coder.map["elements"].with_indifferent_access
+ @permitted = coder.map["ivars"][:@permitted]
+ when "!ruby/object:ActionController::Parameters"
+ # YAML's Object format. Only needed because of the format
+ # 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.
+ def deep_dup
+ self.class.new(@parameters.deep_dup).tap do |duplicate|
+ duplicate.permitted = @permitted
end
end
protected
attr_reader :parameters
- def permitted=(new_permitted)
- @permitted = new_permitted
- end
+ attr_writer :permitted
def fields_for_style?
@parameters.all? { |k, v| k =~ /\A-?\d+\z/ && (v.is_a?(Hash) || v.is_a?(Parameters)) }
@@ -682,7 +865,7 @@ module ActionController
when Parameters
if object.fields_for_style?
hash = object.class.new
- object.each { |k,v| hash[k] = yield v }
+ object.each { |k, v| hash[k] = yield v }
hash
else
yield object
@@ -704,7 +887,7 @@ module ActionController
end
def unpermitted_keys(params)
- self.keys - params.keys - self.always_permitted_parameters
+ keys - params.keys - always_permitted_parameters
end
#
@@ -735,28 +918,46 @@ module ActionController
]
def permitted_scalar?(value)
- PERMITTED_SCALAR_TYPES.any? {|type| value.is_a?(type)}
+ PERMITTED_SCALAR_TYPES.any? { |type| value.is_a?(type) }
end
- def permitted_scalar_filter(params, key)
- if has_key?(key) && permitted_scalar?(self[key])
- params[key] = self[key]
+ # 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"]
+ 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
- keys.grep(/\A#{Regexp.escape(key)}\(\d+[if]?\)\z/) do |k|
- if permitted_scalar?(self[k])
- params[k] = self[k]
- 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
def array_of_permitted_scalars?(value)
- if value.is_a?(Array) && value.all? {|element| permitted_scalar?(element)}
+ if value.is_a?(Array) && value.all? { |element| permitted_scalar?(element) }
yield value
end
end
+ def non_scalar?(value)
+ value.is_a?(Array) || value.is_a?(Parameters)
+ end
+
EMPTY_ARRAY = []
+ EMPTY_HASH = {}
def hash_filter(params, filter)
filter = filter.with_indifferent_access
@@ -770,7 +971,12 @@ module ActionController
array_of_permitted_scalars?(self[key]) do |val|
params[key] = val
end
- else
+ 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]))
@@ -778,22 +984,59 @@ module ActionController
end
end
end
+
+ def permit_any_in_parameters(params)
+ self.class.new.tap do |sanitized|
+ params.each do |key, value|
+ case value
+ when ->(v) { permitted_scalar?(v) }
+ sanitized[key] = value
+ when Array
+ sanitized[key] = permit_any_in_array(value)
+ when Parameters
+ sanitized[key] = permit_any_in_parameters(value)
+ else
+ # Filter this one out.
+ end
+ end
+ end
+ end
+
+ def permit_any_in_array(array)
+ [].tap do |sanitized|
+ array.each do |element|
+ case element
+ when ->(e) { permitted_scalar?(e) }
+ sanitized << element
+ when Parameters
+ sanitized << permit_any_in_parameters(element)
+ else
+ # Filter this one out.
+ end
+ end
+ end
+ end
+
+ def initialize_copy(source)
+ super
+ @parameters = @parameters.dup
+ end
end
# == Strong \Parameters
#
# It provides an interface for protecting attributes from end-user
# assignment. This makes Action Controller parameters forbidden
- # to be used in Active Model mass assignment until they have been
- # whitelisted.
+ # 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 400 Bad Request with no
+ # 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::ForbiddenAttributes exception because it'd
+ # # ActiveModel::ForbiddenAttributesError exception because it'd
# # be using mass assignment without an explicit permit step.
# # This is the recommended form:
# def create
@@ -801,7 +1044,7 @@ module ActionController
# end
#
# # This will pass with flying colors as long as there's a person key in the
- # # parameters, otherwise it'll raise an ActionController::MissingParameter
+ # # 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
@@ -812,7 +1055,7 @@ module ActionController
#
# private
# # Using a private method to encapsulate the permissible parameters is
- # # just a good pattern since you'll be able to reuse the same permit
+ # # 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
@@ -821,7 +1064,7 @@ module ActionController
# end
#
# In order to use <tt>accepts_nested_attributes_for</tt> with Strong \Parameters, you
- # will need to specify which nested attributes should be whitelisted. You might want
+ # 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
@@ -839,7 +1082,7 @@ module ActionController
# private
#
# def person_params
- # # It's mandatory to specify the nested attributes that should be whitelisted.
+ # # 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 ])
diff --git a/actionpack/lib/action_controller/metal/testing.rb b/actionpack/lib/action_controller/metal/testing.rb
index ac37b00010..6e8a95040f 100644
--- a/actionpack/lib/action_controller/metal/testing.rb
+++ b/actionpack/lib/action_controller/metal/testing.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionController
module Testing
extend ActiveSupport::Concern
@@ -10,11 +12,5 @@ module ActionController
self.params = nil
end
end
-
- module ClassMethods
- def before_filters
- _process_action_callbacks.find_all{|x| x.kind == :before}.map(&:name)
- end
- end
end
end
diff --git a/actionpack/lib/action_controller/metal/url_for.rb b/actionpack/lib/action_controller/metal/url_for.rb
index dbf7241a14..f077e765ab 100644
--- a/actionpack/lib/action_controller/metal/url_for.rb
+++ b/actionpack/lib/action_controller/metal/url_for.rb
@@ -1,9 +1,11 @@
+# frozen_string_literal: true
+
module ActionController
# Includes +url_for+ into the host class. The class has to provide a +RouteSet+ by implementing
# the <tt>_routes</tt> method. Otherwise, an exception will be raised.
#
# In addition to <tt>AbstractController::UrlFor</tt>, this module accesses the HTTP layer to define
- # url options like the +host+. In order to do so, this module requires the host class
+ # 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
@@ -27,10 +29,10 @@ module ActionController
def url_options
@_url_options ||= {
- :host => request.host,
- :port => request.optional_port,
- :protocol => request.protocol,
- :_recall => request.path_parameters
+ host: request.host,
+ port: request.optional_port,
+ protocol: request.protocol,
+ _recall: request.path_parameters
}.merge!(super).freeze
if (same_origin = _routes.equal?(request.routes)) ||
@@ -42,7 +44,7 @@ module ActionController
options[:original_script_name] = original_script_name
else
if same_origin
- options[:script_name] = request.script_name.empty? ? "".freeze : request.script_name.dup
+ options[:script_name] = request.script_name.empty? ? "" : request.script_name.dup
else
options[:script_name] = script_name
end
diff --git a/actionpack/lib/action_controller/railtie.rb b/actionpack/lib/action_controller/railtie.rb
index 28b20052b5..7d42f5d931 100644
--- a/actionpack/lib/action_controller/railtie.rb
+++ b/actionpack/lib/action_controller/railtie.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "rails"
require "action_controller"
require "action_dispatch/railtie"
@@ -11,7 +13,7 @@ module ActionController
config.eager_load_namespaces << ActionController
- initializer "action_controller.assets_config", :group => :all do |app|
+ initializer "action_controller.assets_config", group: :all do |app|
app.config.action_controller.assets_dir ||= app.config.paths["public"].first
end
@@ -22,13 +24,15 @@ module ActionController
initializer "action_controller.parameters_config" do |app|
options = app.config.action_controller
- 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
+ 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
@@ -42,7 +46,7 @@ module ActionController
options.javascripts_dir ||= paths["public/javascripts"].first
options.stylesheets_dir ||= paths["public/stylesheets"].first
- # Ensure readers methods get compiled
+ # Ensure readers methods get compiled.
options.asset_host ||= app.config.asset_host
options.relative_url_root ||= app.config.relative_url_root
@@ -51,7 +55,7 @@ module ActionController
extend ::AbstractController::Railties::RoutesHelpers.with(app.routes)
extend ::ActionController::Railties::Helpers
- options.each do |k,v|
+ options.each do |k, v|
k = "#{k}="
if respond_to?(k)
send(k, v)
@@ -67,5 +71,19 @@ module ActionController
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
diff --git a/actionpack/lib/action_controller/railties/helpers.rb b/actionpack/lib/action_controller/railties/helpers.rb
index 3985c6b273..75938108d6 100644
--- a/actionpack/lib/action_controller/railties/helpers.rb
+++ b/actionpack/lib/action_controller/railties/helpers.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionController
module Railties
module Helpers
@@ -5,7 +7,7 @@ module ActionController
super
return unless klass.respond_to?(:helpers_path=)
- if namespace = klass.parents.detect { |m| m.respond_to?(:railtie_helpers_paths) }
+ if namespace = klass.module_parents.detect { |m| m.respond_to?(:railtie_helpers_paths) }
paths = namespace.railtie_helpers_paths
else
paths = ActionController::Helpers.helpers_path
diff --git a/actionpack/lib/action_controller/renderer.rb b/actionpack/lib/action_controller/renderer.rb
index e4d19e9dba..8c16308ce7 100644
--- a/actionpack/lib/action_controller/renderer.rb
+++ b/actionpack/lib/action_controller/renderer.rb
@@ -1,11 +1,11 @@
-require 'active_support/core_ext/hash/keys'
+# frozen_string_literal: true
module ActionController
- # ActionController::Renderer allows to render arbitrary templates
+ # 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,
+ # For example:
#
# ApplicationController.renderer
#
@@ -13,12 +13,12 @@ module ActionController
#
# ApplicationController.renderer.render template: '...'
#
- # You can use a shortcut on controller to replace previous example with:
+ # You can use this shortcut in a controller, instead of the previous example:
#
# ApplicationController.render template: '...'
#
- # #render method allows you to use any options as when rendering in controller.
- # For example,
+ # #render allows you to use the same options that you can use when rendering in a controller.
+ # For example:
#
# FooController.render :action, locals: { ... }, assigns: { ... }
#
@@ -37,15 +37,15 @@ module ActionController
attr_reader :defaults, :controller
DEFAULTS = {
- http_host: 'example.org',
+ http_host: "example.org",
https: false,
- method: 'get',
- script_name: '',
- input: ''
+ method: "get",
+ script_name: "",
+ input: ""
}.freeze
# Create a new renderer instance for a specific controller class.
- def self.for(controller, env = {}, defaults = DEFAULTS)
+ def self.for(controller, env = {}, defaults = DEFAULTS.dup)
new(controller, env, defaults)
end
@@ -56,11 +56,12 @@ module ActionController
# Create a new renderer for the same controller but with new defaults.
def with_defaults(defaults)
- self.class.new controller, env, self.defaults.merge(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 ActionController::Renderer.defaults
+ # It will be merged with the default Rack environment defined by
+ # +ActionController::Renderer::DEFAULTS+.
def initialize(controller, env, defaults)
@controller = controller
@defaults = defaults
@@ -68,8 +69,23 @@ module ActionController
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, the default is
+ # to render a partial and use the second parameter as the locals hash.
def render(*args)
- raise 'missing controller' unless controller
+ raise "missing controller" unless controller
request = ActionDispatch::Request.new @env
request.routes = controller._routes
@@ -83,26 +99,29 @@ module ActionController
private
def normalize_keys(env)
new_env = {}
- env.each_pair { |k,v| new_env[rack_key_for(k)] = rack_value_for(k, v) }
+ env.each_pair { |k, v| new_env[rack_key_for(k)] = rack_value_for(k, v) }
+ new_env["rack.url_scheme"] = new_env["HTTPS"] == "on" ? "https" : "http"
new_env
end
RACK_KEY_TRANSLATION = {
- http_host: 'HTTP_HOST',
- https: 'HTTPS',
- method: 'REQUEST_METHOD',
- script_name: 'SCRIPT_NAME',
- input: 'rack.input'
+ http_host: "HTTP_HOST",
+ https: "HTTPS",
+ method: "REQUEST_METHOD",
+ script_name: "SCRIPT_NAME",
+ input: "rack.input"
}
IDENTITY = ->(_) { _ }
RACK_VALUE_TRANSLATION = {
- https: ->(v) { v ? 'on' : 'off' },
+ https: ->(v) { v ? "on" : "off" },
method: ->(v) { v.upcase },
}
- def rack_key_for(key); RACK_KEY_TRANSLATION[key]; end
+ def rack_key_for(key)
+ RACK_KEY_TRANSLATION.fetch(key, key.to_s)
+ end
def rack_value_for(key, value)
RACK_VALUE_TRANSLATION.fetch(key, IDENTITY).call value
diff --git a/actionpack/lib/action_controller/template_assertions.rb b/actionpack/lib/action_controller/template_assertions.rb
index 0179f4afcd..dd83c1a283 100644
--- a/actionpack/lib/action_controller/template_assertions.rb
+++ b/actionpack/lib/action_controller/template_assertions.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionController
module TemplateAssertions
def assert_template(options = {}, message = nil)
diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb
index ecd21f29ce..57921f32b7 100644
--- a/actionpack/lib/action_controller/test_case.rb
+++ b/actionpack/lib/action_controller/test_case.rb
@@ -1,44 +1,49 @@
-require 'rack/session/abstract/id'
-require 'active_support/core_ext/hash/conversions'
-require 'active_support/core_ext/object/to_query'
-require 'active_support/core_ext/module/anonymous'
-require 'active_support/core_ext/hash/keys'
-require 'action_controller/template_assertions'
-require 'rails-dom-testing'
+# frozen_string_literal: true
+
+require "rack/session/abstract/id"
+require "active_support/core_ext/hash/conversions"
+require "active_support/core_ext/object/to_query"
+require "active_support/core_ext/module/anonymous"
+require "active_support/core_ext/module/redefine_method"
+require "active_support/core_ext/hash/keys"
+require "active_support/testing/constant_lookup"
+require "action_controller/template_assertions"
+require "rails-dom-testing"
module ActionController
- # :stopdoc:
class Metal
include Testing::Functional
end
module Live
- # Disable controller / rendering threads in tests. User tests can access
+ # 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
- remove_method :new_controller_thread
+ # that's only visible to the main thread's txn. This is the problem in #23483.
+ silence_redefinition_of_method :new_controller_thread
def new_controller_thread # :nodoc:
yield
end
end
- # ActionController::TestCase will be deprecated and moved to a gem in Rails 5.1.
+ # ActionController::TestCase will be deprecated and moved to a gem in the future.
# Please use ActionDispatch::IntegrationTest going forward.
class TestRequest < ActionDispatch::TestRequest #:nodoc:
DEFAULT_ENV = ActionDispatch::TestRequest::DEFAULT_ENV.dup
- DEFAULT_ENV.delete 'PATH_INFO'
+ DEFAULT_ENV.delete "PATH_INFO"
def self.new_session
TestSession.new
end
- # Create a new test request with default `env` values
- def self.create
+ attr_reader :controller_class
+
+ # Create a new test request with default `env` values.
+ 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)
+ new(default_env.merge(env), new_session, controller_class)
end
def self.default_env
@@ -46,13 +51,14 @@ module ActionController
end
private_class_method :default_env
- def initialize(env, session)
+ def initialize(env, session, controller_class)
super(env)
self.session = session
- self.session_options = TestSession::DEFAULT_OPTIONS
+ self.session_options = TestSession::DEFAULT_OPTIONS.dup
+ @controller_class = controller_class
@custom_param_parsers = {
- xml: lambda { |raw_post| Hash.from_xml(raw_post)['hash'] }
+ xml: lambda { |raw_post| Hash.from_xml(raw_post)["hash"] }
}
end
@@ -61,7 +67,7 @@ module ActionController
end
def content_type=(type)
- set_header 'CONTENT_TYPE', type
+ set_header "CONTENT_TYPE", type
end
def assign_parameters(routes, controller_path, action, parameters, generated_path, query_string_keys)
@@ -83,7 +89,7 @@ module ActionController
end
if get?
- if self.query_string.blank?
+ if query_string.blank?
self.query_string = non_path_parameters.to_query
end
else
@@ -91,8 +97,8 @@ module ActionController
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'
+ fetch_header("CONTENT_TYPE") do |k|
+ set_header k, "application/x-www-form-urlencoded"
end
case content_mime_type.to_sym
@@ -110,8 +116,9 @@ module ActionController
end
end
- set_header 'CONTENT_LENGTH', data.length.to_s
- set_header 'rack.input', StringIO.new(data)
+ 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|
@@ -127,7 +134,7 @@ module ActionController
include Rack::Test::Utils
def should_multipart?(params)
- # FIXME: lifted from Rack-Test. We should push this separation upstream
+ # FIXME: lifted from Rack-Test. We should push this separation upstream.
multipart = false
query = lambda { |value|
case value
@@ -152,9 +159,9 @@ module ActionController
private
- def params_parsers
- super.merge @custom_param_parsers
- end
+ def params_parsers
+ super.merge @custom_param_parsers
+ end
end
class LiveTestResponse < Live::Response
@@ -208,10 +215,18 @@ module ActionController
end
# Superclass for ActionController functional tests. Functional tests allow you to
- # test a single controller action per test method. This should not be confused with
- # integration tests (see ActionDispatch::IntegrationTest), which are more like
- # "stories" that can involve multiple controllers and multiple actions (i.e. multiple
- # different HTTP requests).
+ # 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
#
@@ -241,7 +256,7 @@ module ActionController
#
# def test_create
# json = {book: { title: "Love Hina" }}.to_json
- # post :create, json
+ # post :create, body: json
# end
#
# == Special instance variables
@@ -261,9 +276,6 @@ module ActionController
# after calling +post+. If the various assert methods are not sufficient, then you
# may use this object to inspect the HTTP response in detail.
#
- # (Earlier versions of \Rails required each functional test to subclass
- # Test::Unit::TestCase and define @controller, @request, @response in +setup+.)
- #
# == Controller is automatically inferred
#
# ActionController::TestCase will automatically infer the controller under test
@@ -288,7 +300,7 @@ module ActionController
# 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>.
+ # 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.
@@ -320,7 +332,6 @@ module ActionController
attr_reader :response, :request
module ClassMethods
-
# Sets the controller class name. Useful if the name can't be inferred from test class.
# Normalizes +controller_class+ before using.
#
@@ -343,7 +354,7 @@ module ActionController
end
def controller_class
- if current_controller_class = self._controller_class
+ if current_controller_class = _controller_class
current_controller_class
else
self.controller_class = determine_default_controller_class(name)
@@ -377,57 +388,42 @@ module ActionController
#
# Note that the request method is not verified. The different methods are
# available to make the tests more expressive.
- def get(action, *args)
- res = process_with_kwargs("GET", action, *args)
+ 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.
- def post(action, *args)
- process_with_kwargs("POST", action, *args)
+ 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.
- def patch(action, *args)
- process_with_kwargs("PATCH", action, *args)
+ 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.
- def put(action, *args)
- process_with_kwargs("PUT", action, *args)
+ 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.
- def delete(action, *args)
- process_with_kwargs("DELETE", action, *args)
+ 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.
- def head(action, *args)
- process_with_kwargs("HEAD", action, *args)
+ def head(action, **args)
+ process(action, method: "HEAD", **args)
end
- def xml_http_request(*args)
- ActiveSupport::Deprecation.warn(<<-MSG.strip_heredoc)
- xhr and xml_http_request methods are deprecated in favor of
- `get :index, xhr: true` and `post :create, xhr: true`
- MSG
-
- @request.env['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest'
- @request.env['HTTP_ACCEPT'] ||= [Mime[:js], Mime[:html], Mime[:xml], 'text/xml', '*/*'].join(', ')
- __send__(*args).tap do
- @request.env.delete 'HTTP_X_REQUESTED_WITH'
- @request.env.delete 'HTTP_ACCEPT'
- end
- end
- alias xhr :xml_http_request
-
# Simulate an HTTP request to +action+ by specifying request method,
# parameters and set/volley the response.
#
@@ -440,6 +436,8 @@ module ActionController
# - +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:
#
@@ -456,57 +454,40 @@ module ActionController
# respectively which will make tests more expressive.
#
# Note that the request method is not verified.
- def process(action, *args)
+ def process(action, method: "GET", params: nil, session: nil, body: nil, flash: {}, format: nil, xhr: false, as: nil)
check_required_ivars
- if kwarg_request?(args)
- parameters, session, body, flash, http_method, format, xhr = args[0].values_at(:params, :session, :body, :flash, :method, :format, :xhr)
- else
- http_method, parameters, session, flash = args
- format = nil
+ http_method = method.to_s.upcase
- if parameters.is_a?(String) && http_method != 'HEAD'
- body = parameters
- parameters = nil
- end
+ @html_document = nil
- if parameters || session || flash
- non_kwarg_request_warning
- end
- end
+ 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
+ @request.set_header "RAW_POST_DATA", body
end
- if http_method
- http_method = http_method.to_s.upcase
- else
- http_method = "GET"
+ @request.set_header "REQUEST_METHOD", http_method
+
+ if as
+ @request.content_type = Mime[as].to_s
+ format ||= as
end
- parameters ||= {}
+ parameters = (params || {}).symbolize_keys
if format
parameters[:format] = format
end
- @html_document = nil
-
- self.cookies.update @request.cookies
- self.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
- @response = build_response @response_klass
- @response.request = @request
- @controller.recycle!
-
- @request.set_header 'REQUEST_METHOD', http_method
-
- parameters = parameters.symbolize_keys
-
generated_extras = @routes.generate_extras(parameters.merge(controller: controller_class_name, action: action.to_s))
generated_path = generated_path(generated_extras)
query_string_keys = query_parameter_names(generated_extras)
@@ -517,9 +498,9 @@ module ActionController
@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(', ')
+ @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
@@ -527,32 +508,35 @@ module ActionController
@request.set_header k, @controller.config.relative_url_root
end
- @controller.recycle!
- @controller.dispatch(action, @request, @response)
- @request = @controller.request
- @response = @controller.response
-
- @request.delete_header 'HTTP_COOKIE'
+ 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 @request.have_cookie_jar?
- unless @request.cookie_jar.committed?
- @request.cookie_jar.write(@response)
- self.cookies.update(@request.cookie_jar.instance_variable_get(:@cookies))
+ if flash_value = @request.flash.to_session_value
+ @request.session["flash"] = flash_value
+ else
+ @request.session.delete("flash")
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 = ""
- if xhr
- @request.delete_header 'HTTP_X_REQUESTED_WITH'
- @request.delete_header 'HTTP_ACCEPT'
+ @response.sent!
end
- @request.query_string = ''
@response
end
@@ -587,7 +571,7 @@ module ActionController
end
end
- @request = TestRequest.create
+ @request = TestRequest.create(@controller.class)
@response = build_response @response_klass
@response.request = @request
@@ -606,71 +590,37 @@ module ActionController
include ActionDispatch::Assertions
class_attribute :_controller_class
setup :setup_controller_request_and_response
+ ActiveSupport.run_load_hooks(:action_controller_test_case, self)
end
private
- def scrub_env!(env)
- env.delete_if { |k, v| k =~ /^(action_dispatch|rack)\.request/ }
- env.delete_if { |k, v| k =~ /^action_dispatch\.rescue/ }
- env.delete 'action_dispatch.request.query_parameters'
- env.delete 'action_dispatch.request.request_parameters'
- env
- end
-
- def process_with_kwargs(http_method, action, *args)
- if kwarg_request?(args)
- args.first.merge!(method: http_method)
- process(action, *args)
- else
- non_kwarg_request_warning if args.any?
-
- args = args.unshift(http_method)
- process(action, *args)
+ def scrub_env!(env)
+ env.delete_if { |k, v| k =~ /^(action_dispatch|rack)\.request/ }
+ env.delete_if { |k, v| k =~ /^action_dispatch\.rescue/ }
+ env.delete "action_dispatch.request.query_parameters"
+ env.delete "action_dispatch.request.request_parameters"
+ env["rack.input"] = StringIO.new
+ env.delete "CONTENT_LENGTH"
+ env.delete "RAW_POST_DATA"
+ env
end
- end
-
- REQUEST_KWARGS = %i(params session flash method body xhr)
- def kwarg_request?(args)
- args[0].respond_to?(:keys) && (
- (args[0].key?(:format) && args[0].keys.size == 1) ||
- args[0].keys.any? { |k| REQUEST_KWARGS.include?(k) }
- )
- end
-
- def non_kwarg_request_warning
- ActiveSupport::Deprecation.warn(<<-MSG.strip_heredoc)
- ActionController::TestCase HTTP request methods will accept only
- keyword arguments in future Rails versions.
-
- Examples:
-
- get :show, params: { id: 1 }, session: { user_id: 1 }
- process :update, method: :post, params: { id: 1 }
- MSG
- end
- def document_root_element
- html_document.root
- end
+ def document_root_element
+ html_document.root
+ end
- 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."
+ 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
-
- def html_format?(parameters)
- return true unless parameters.key?(:format)
- Mime.fetch(parameters[:format]) { Mime['html'] }.html?
- end
end
include Behavior
end
- # :startdoc:
end
diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb
index 01d49475de..8f39b88d56 100644
--- a/actionpack/lib/action_dispatch.rb
+++ b/actionpack/lib/action_dispatch.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
#--
-# Copyright (c) 2004-2016 David Heinemeier Hansson
+# Copyright (c) 2004-2019 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@@ -21,15 +23,15 @@
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++
-require 'active_support'
-require 'active_support/rails'
-require 'active_support/core_ext/module/attribute_accessors'
+require "active_support"
+require "active_support/rails"
+require "active_support/core_ext/module/attribute_accessors"
-require 'action_pack'
-require 'rack'
+require "action_pack"
+require "rack"
module Rack
- autoload :Test, 'rack/test'
+ autoload :Test, "rack/test"
end
module ActionDispatch
@@ -39,21 +41,24 @@ module ActionDispatch
end
eager_autoload do
- autoload_under 'http' do
+ autoload_under "http" do
+ autoload :ContentSecurityPolicy
autoload :Request
autoload :Response
end
end
- autoload_under 'middleware' do
+ autoload_under "middleware" do
+ autoload :HostAuthorization
autoload :RequestId
autoload :Callbacks
autoload :Cookies
autoload :DebugExceptions
+ autoload :DebugLocks
+ autoload :DebugView
autoload :ExceptionWrapper
autoload :Executor
autoload :Flash
- autoload :ParamsParser
autoload :PublicExceptions
autoload :Reloader
autoload :RemoteIp
@@ -63,7 +68,7 @@ module ActionDispatch
end
autoload :Journey
- autoload :MiddlewareStack, 'action_dispatch/middleware/stack'
+ autoload :MiddlewareStack, "action_dispatch/middleware/stack"
autoload :Routing
module Http
@@ -75,31 +80,33 @@ module ActionDispatch
autoload :Parameters
autoload :ParameterFilter
autoload :Upload
- autoload :UploadedFile, 'action_dispatch/http/upload'
+ autoload :UploadedFile, "action_dispatch/http/upload"
autoload :URL
end
module Session
- autoload :AbstractStore, 'action_dispatch/middleware/session/abstract_store'
- autoload :CookieStore, 'action_dispatch/middleware/session/cookie_store'
- autoload :MemCacheStore, 'action_dispatch/middleware/session/mem_cache_store'
- autoload :CacheStore, 'action_dispatch/middleware/session/cache_store'
+ autoload :AbstractStore, "action_dispatch/middleware/session/abstract_store"
+ autoload :CookieStore, "action_dispatch/middleware/session/cookie_store"
+ autoload :MemCacheStore, "action_dispatch/middleware/session/mem_cache_store"
+ autoload :CacheStore, "action_dispatch/middleware/session/cache_store"
end
mattr_accessor :test_app
- autoload_under 'testing' do
+ autoload_under "testing" do
autoload :Assertions
autoload :Integration
- autoload :IntegrationTest, 'action_dispatch/testing/integration'
+ autoload :IntegrationTest, "action_dispatch/testing/integration"
autoload :TestProcess
autoload :TestRequest
autoload :TestResponse
autoload :AssertionResponse
end
+
+ autoload :SystemTestCase, "action_dispatch/system_test_case"
end
-autoload :Mime, 'action_dispatch/http/mime_type'
+autoload :Mime, "action_dispatch/http/mime_type"
ActiveSupport.on_load(:action_view) do
ActionView::Base.default_formats ||= Mime::SET.symbols
diff --git a/actionpack/lib/action_dispatch/http/cache.rb b/actionpack/lib/action_dispatch/http/cache.rb
index 9fa2e38ae3..8cc84ff36c 100644
--- a/actionpack/lib/action_dispatch/http/cache.rb
+++ b/actionpack/lib/action_dispatch/http/cache.rb
@@ -1,10 +1,11 @@
+# frozen_string_literal: true
+
module ActionDispatch
module Http
module Cache
module Request
-
- HTTP_IF_MODIFIED_SINCE = 'HTTP_IF_MODIFIED_SINCE'.freeze
- HTTP_IF_NONE_MATCH = 'HTTP_IF_NONE_MATCH'.freeze
+ HTTP_IF_MODIFIED_SINCE = "HTTP_IF_MODIFIED_SINCE"
+ HTTP_IF_NONE_MATCH = "HTTP_IF_NONE_MATCH"
def if_modified_since
if since = get_header(HTTP_IF_MODIFIED_SINCE)
@@ -27,7 +28,7 @@ module ActionDispatch
def etag_matches?(etag)
if etag
validators = if_none_match_etags
- validators.include?(etag) || validators.include?('*')
+ validators.include?(etag) || validators.include?("*")
end
end
@@ -96,17 +97,17 @@ module ActionDispatch
# 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.
+ # Check out #strong_etag= to provide a strong ETag validator.
def etag=(weak_validators)
self.weak_etag = weak_validators
end
def weak_etag=(weak_validators)
- set_header 'ETag', generate_weak_etag(weak_validators)
+ set_header "ETag", generate_weak_etag(weak_validators)
end
def strong_etag=(strong_validators)
- set_header 'ETag', generate_strong_etag(strong_validators)
+ set_header "ETag", generate_strong_etag(strong_validators)
end
def etag?; etag; end
@@ -123,8 +124,8 @@ module ActionDispatch
private
- DATE = 'Date'.freeze
- LAST_MODIFIED = "Last-Modified".freeze
+ DATE = "Date"
+ LAST_MODIFIED = "Last-Modified"
SPECIAL_KEYS = Set.new(%w[extras no-cache max-age public private must-revalidate])
def generate_weak_etag(validators)
@@ -132,12 +133,12 @@ module ActionDispatch
end
def generate_strong_etag(validators)
- %("#{Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key(validators))}")
+ %("#{ActiveSupport::Digest.hexdigest(ActiveSupport::Cache.expand_cache_key(validators))}")
end
def cache_control_segments
if cache_control = _cache_control
- cache_control.delete(' ').split(',')
+ cache_control.delete(" ").split(",")
else
[]
end
@@ -147,10 +148,10 @@ module ActionDispatch
cache_control = {}
cache_control_segments.each do |segment|
- directive, argument = segment.split('=', 2)
+ directive, argument = segment.split("=", 2)
if SPECIAL_KEYS.include? directive
- key = directive.tr('-', '_')
+ key = directive.tr("-", "_")
cache_control[key.to_sym] = argument || true
else
cache_control[:extras] ||= []
@@ -165,19 +166,23 @@ module ActionDispatch
@cache_control = cache_control_headers
end
+ DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate"
+ NO_CACHE = "no-cache"
+ PUBLIC = "public"
+ PRIVATE = "private"
+ MUST_REVALIDATE = "must-revalidate"
+
def handle_conditional_get!
- if etag? || last_modified? || !@cache_control.empty?
- set_conditional_cache_control!(@cache_control)
+ # 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
- DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate".freeze
- NO_CACHE = "no-cache".freeze
- PUBLIC = "public".freeze
- PRIVATE = "private".freeze
- MUST_REVALIDATE = "must-revalidate".freeze
-
- def set_conditional_cache_control!(cache_control)
+ def merge_and_normalize_cache_control!(cache_control)
control = {}
cc_headers = cache_control_headers
if extras = cc_headers.delete(:extras)
@@ -190,20 +195,26 @@ module ActionDispatch
control.merge! cache_control
if control.empty?
- self._cache_control = DEFAULT_CACHE_CONTROL
+ # Let middleware handle default behavior
elsif control[:no_cache]
- self._cache_control = NO_CACHE
- if control[:extras]
- self._cache_control = _cache_control + ", #{control[:extras].join(', ')}"
- end
+ 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]
+ 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(", ")
diff --git a/actionpack/lib/action_dispatch/http/content_disposition.rb b/actionpack/lib/action_dispatch/http/content_disposition.rb
new file mode 100644
index 0000000000..58164c1522
--- /dev/null
+++ b/actionpack/lib/action_dispatch/http/content_disposition.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module ActionDispatch
+ module Http
+ class ContentDisposition # :nodoc:
+ def self.format(disposition:, filename:)
+ new(disposition: disposition, filename: filename).to_s
+ end
+
+ attr_reader :disposition, :filename
+
+ def initialize(disposition:, filename:)
+ @disposition = disposition
+ @filename = filename
+ end
+
+ TRADITIONAL_ESCAPED_CHAR = /[^ A-Za-z0-9!#$+.^_`|~-]/
+
+ def ascii_filename
+ 'filename="' + percent_escape(I18n.transliterate(filename), TRADITIONAL_ESCAPED_CHAR) + '"'
+ end
+
+ RFC_5987_ESCAPED_CHAR = /[^A-Za-z0-9!#$&+.^_`|~-]/
+
+ def utf8_filename
+ "filename*=UTF-8''" + percent_escape(filename, RFC_5987_ESCAPED_CHAR)
+ end
+
+ def to_s
+ if filename
+ "#{disposition}; #{ascii_filename}; #{utf8_filename}"
+ else
+ "#{disposition}"
+ end
+ end
+
+ private
+ def percent_escape(string, pattern)
+ string.gsub(pattern) do |char|
+ char.bytes.map { |byte| "%%%02X" % byte }.join
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/http/content_security_policy.rb b/actionpack/lib/action_dispatch/http/content_security_policy.rb
new file mode 100644
index 0000000000..b1e5a28be5
--- /dev/null
+++ b/actionpack/lib/action_dispatch/http/content_security_policy.rb
@@ -0,0 +1,273 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/object/deep_dup"
+
+module ActionDispatch #:nodoc:
+ class ContentSecurityPolicy
+ class Middleware
+ CONTENT_TYPE = "Content-Type"
+ POLICY = "Content-Security-Policy"
+ POLICY_REPORT_ONLY = "Content-Security-Policy-Report-Only"
+
+ def initialize(app)
+ @app = app
+ end
+
+ 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
+ context = request.controller_instance || request
+ headers[header_name(request)] = policy.build(context, nonce)
+ end
+
+ response
+ end
+
+ private
+
+ def html_response?(headers)
+ if content_type = headers[CONTENT_TYPE]
+ content_type =~ /html/
+ end
+ end
+
+ def header_name(request)
+ if request.content_security_policy_report_only
+ POLICY_REPORT_ONLY
+ else
+ POLICY
+ end
+ end
+
+ def policy_present?(headers)
+ headers[POLICY] || headers[POLICY_REPORT_ONLY]
+ end
+ end
+
+ module Request
+ POLICY = "action_dispatch.content_security_policy"
+ POLICY_REPORT_ONLY = "action_dispatch.content_security_policy_report_only"
+ NONCE_GENERATOR = "action_dispatch.content_security_policy_nonce_generator"
+ NONCE = "action_dispatch.content_security_policy_nonce"
+
+ def content_security_policy
+ get_header(POLICY)
+ end
+
+ def content_security_policy=(policy)
+ set_header(POLICY, policy)
+ end
+
+ def content_security_policy_report_only
+ get_header(POLICY_REPORT_ONLY)
+ end
+
+ def content_security_policy_report_only=(value)
+ set_header(POLICY_REPORT_ONLY, value)
+ end
+
+ def content_security_policy_nonce_generator
+ get_header(NONCE_GENERATOR)
+ end
+
+ def content_security_policy_nonce_generator=(generator)
+ set_header(NONCE_GENERATOR, generator)
+ end
+
+ 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
+
+ private
+
+ def generate_content_security_policy_nonce
+ content_security_policy_nonce_generator.call(self)
+ end
+ end
+
+ 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
+
+ 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",
+ style_src: "style-src",
+ worker_src: "worker-src"
+ }.freeze
+
+ NONCE_DIRECTIVES = %w[script-src style-src].freeze
+
+ private_constant :MAPPINGS, :DIRECTIVES, :NONCE_DIRECTIVES
+
+ attr_reader :directives
+
+ def initialize
+ @directives = {}
+ yield self if block_given?
+ end
+
+ def initialize_copy(other)
+ @directives = other.directives.deep_dup
+ end
+
+ DIRECTIVES.each do |name, directive|
+ define_method(name) do |*sources|
+ if sources.first
+ @directives[directive] = apply_mappings(sources)
+ else
+ @directives.delete(directive)
+ end
+ end
+ end
+
+ def block_all_mixed_content(enabled = true)
+ if enabled
+ @directives["block-all-mixed-content"] = true
+ else
+ @directives.delete("block-all-mixed-content")
+ end
+ end
+
+ def plugin_types(*types)
+ if types.first
+ @directives["plugin-types"] = types
+ else
+ @directives.delete("plugin-types")
+ end
+ end
+
+ def report_uri(uri)
+ @directives["report-uri"] = [uri]
+ end
+
+ def require_sri_for(*types)
+ if types.first
+ @directives["require-sri-for"] = types
+ else
+ @directives.delete("require-sri-for")
+ end
+ end
+
+ def sandbox(*values)
+ if values.empty?
+ @directives["sandbox"] = true
+ elsif values.first
+ @directives["sandbox"] = values
+ else
+ @directives.delete("sandbox")
+ end
+ end
+
+ def upgrade_insecure_requests(enabled = true)
+ if enabled
+ @directives["upgrade-insecure-requests"] = true
+ else
+ @directives.delete("upgrade-insecure-requests")
+ end
+ end
+
+ def build(context = nil, nonce = nil)
+ build_directives(context, nonce).compact.join("; ")
+ end
+
+ private
+ def apply_mappings(sources)
+ sources.map do |source|
+ case source
+ when Symbol
+ apply_mapping(source)
+ when String, Proc
+ source
+ else
+ raise ArgumentError, "Invalid content security policy source: #{source.inspect}"
+ end
+ end
+ end
+
+ def apply_mapping(source)
+ MAPPINGS.fetch(source) do
+ raise ArgumentError, "Unknown content security policy source mapping: #{source.inspect}"
+ end
+ end
+
+ def build_directives(context, nonce)
+ @directives.map do |directive, sources|
+ if sources.is_a?(Array)
+ if nonce && nonce_directive?(directive)
+ "#{directive} #{build_directive(sources, context).join(' ')} 'nonce-#{nonce}'"
+ else
+ "#{directive} #{build_directive(sources, context).join(' ')}"
+ end
+ elsif sources
+ directive
+ else
+ nil
+ end
+ end
+ end
+
+ def build_directive(sources, context)
+ sources.map { |source| resolve_source(source, context) }
+ end
+
+ 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
+
+ def nonce_directive?(directive)
+ NONCE_DIRECTIVES.include?(directive)
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/http/filter_parameters.rb b/actionpack/lib/action_dispatch/http/filter_parameters.rb
index 041eca48ca..cbb772175c 100644
--- a/actionpack/lib/action_dispatch/http/filter_parameters.rb
+++ b/actionpack/lib/action_dispatch/http/filter_parameters.rb
@@ -1,4 +1,6 @@
-require 'action_dispatch/http/parameter_filter'
+# frozen_string_literal: true
+
+require "active_support/parameter_filter"
module ActionDispatch
module Http
@@ -7,8 +9,8 @@ module ActionDispatch
# 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 is passed to it, the value or key can be replaced using
- # String#replace or similar method.
+ # 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]"
@@ -26,8 +28,8 @@ module ActionDispatch
# => reverses the value to all keys matching /secret/i
module FilterParameters
ENV_MATCH = [/RAW_POST_DATA/, "rack.request.form_vars"] # :nodoc:
- NULL_PARAM_FILTER = ParameterFilter.new # :nodoc:
- NULL_ENV_FILTER = ParameterFilter.new ENV_MATCH # :nodoc:
+ NULL_PARAM_FILTER = ActiveSupport::ParameterFilter.new # :nodoc:
+ NULL_ENV_FILTER = ActiveSupport::ParameterFilter.new ENV_MATCH # :nodoc:
def initialize
super
@@ -39,6 +41,8 @@ module ActionDispatch
# Returns a hash of parameters with all sensitive data replaced.
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.
@@ -46,35 +50,35 @@ module ActionDispatch
@filtered_env ||= env_filter.filter(@env)
end
- # Reconstructed a path with all sensitive GET parameters replaced.
+ # Reconstructs a path with all sensitive GET parameters replaced.
def filtered_path
@filtered_path ||= query_string.empty? ? path : "#{path}?#{filtered_query_string}"
end
- protected
+ private
- def parameter_filter
+ def parameter_filter # :doc:
parameter_filter_for fetch_header("action_dispatch.parameter_filter") {
return NULL_PARAM_FILTER
}
end
- def env_filter
+ 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
- def parameter_filter_for(filters)
- ParameterFilter.new(filters)
+ def parameter_filter_for(filters) # :doc:
+ ActiveSupport::ParameterFilter.new(filters)
end
- KV_RE = '[^&;=]+'
+ KV_RE = "[^&;=]+"
PAIR_RE = %r{(#{KV_RE})=(#{KV_RE})}
- def filtered_query_string
+ def filtered_query_string # :doc:
query_string.gsub(PAIR_RE) do |_|
- parameter_filter.filter([[$1, $2]]).first.join("=")
+ parameter_filter.filter($1 => $2).first.join("=")
end
end
end
diff --git a/actionpack/lib/action_dispatch/http/filter_redirect.rb b/actionpack/lib/action_dispatch/http/filter_redirect.rb
index f4b806b8b5..8c4e852235 100644
--- a/actionpack/lib/action_dispatch/http/filter_redirect.rb
+++ b/actionpack/lib/action_dispatch/http/filter_redirect.rb
@@ -1,8 +1,9 @@
+# frozen_string_literal: true
+
module ActionDispatch
module Http
module FilterRedirect
-
- FILTERED = '[FILTERED]'.freeze # :nodoc:
+ FILTERED = "[FILTERED]" # :nodoc:
def filtered_location # :nodoc:
if location_filter_match?
@@ -16,7 +17,7 @@ module ActionDispatch
def location_filters
if request
- request.get_header('action_dispatch.redirect_filter') || []
+ request.get_header("action_dispatch.redirect_filter") || []
else
[]
end
@@ -31,7 +32,6 @@ module ActionDispatch
end
end
end
-
end
end
end
diff --git a/actionpack/lib/action_dispatch/http/headers.rb b/actionpack/lib/action_dispatch/http/headers.rb
index 69a934b7cd..6c7d24d2d0 100644
--- a/actionpack/lib/action_dispatch/http/headers.rb
+++ b/actionpack/lib/action_dispatch/http/headers.rb
@@ -1,9 +1,11 @@
+# frozen_string_literal: true
+
module ActionDispatch
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.new(env)
+ # headers = ActionDispatch::Http::Headers.from_hash(env)
# headers["Content-Type"] # => "text/plain"
# headers["User-Agent"] # => "curl/7.43.0"
#
@@ -86,7 +88,7 @@ module ActionDispatch
@req.fetch_header(env_name(key)) do
return default unless default == DEFAULT
return yield if block_given?
- raise NameError, key
+ raise KeyError, key
end
end
@@ -115,16 +117,16 @@ module ActionDispatch
private
- # Converts an HTTP header name to an environment variable name if it is
- # not contained within the headers hash.
- def env_name(key)
- key = key.to_s
- if key =~ HTTP_HEADER
- key = key.upcase.tr('-', '_')
- key = "HTTP_" + key unless CGI_VARIABLES.include?(key)
+ # Converts an HTTP header name to an environment variable name if it is
+ # not contained within the headers hash.
+ def env_name(key)
+ key = key.to_s
+ if HTTP_HEADER.match?(key)
+ key = key.upcase.tr("-", "_")
+ key = "HTTP_" + key unless CGI_VARIABLES.include?(key)
+ end
+ key
end
- key
- end
end
end
end
diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb
index e9b25339dc..4e81ba12a5 100644
--- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb
+++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb
@@ -1,22 +1,25 @@
-require 'active_support/core_ext/module/attribute_accessors'
+# frozen_string_literal: true
+
+require "active_support/core_ext/module/attribute_accessors"
module ActionDispatch
module Http
module MimeNegotiation
extend ActiveSupport::Concern
+ RESCUABLE_MIME_FORMAT_ERRORS = [
+ ActionController::BadRequest,
+ ActionDispatch::Http::Parameters::ParseError,
+ ]
+
included do
- mattr_accessor :ignore_accept_header
- self.ignore_accept_header = false
+ mattr_accessor :ignore_accept_header, default: false
end
# The MIME type of the HTTP request, such as Mime[:xml].
- #
- # For backward compatibility, the post \format is extracted from the
- # X-Post-Data-Format HTTP header if present.
def content_mime_type
fetch_header("action_dispatch.request.content_type") do |k|
- v = if get_header('CONTENT_TYPE') =~ /^([^,\;]*)/
+ v = if get_header("CONTENT_TYPE") =~ /^([^,\;]*)/
Mime::Type.lookup($1.strip.downcase)
else
nil
@@ -29,14 +32,14 @@ module ActionDispatch
content_mime_type && content_mime_type.to_s
end
- def has_content_type?
- has_header? 'CONTENT_TYPE'
+ def has_content_type? # :nodoc:
+ get_header "CONTENT_TYPE"
end
# Returns the accepted MIME type for the request.
def accepts
fetch_header("action_dispatch.request.accepts") do |k|
- header = get_header('HTTP_ACCEPT').to_s.strip
+ header = get_header("HTTP_ACCEPT").to_s.strip
v = if header.empty?
[content_mime_type]
@@ -61,7 +64,7 @@ module ActionDispatch
fetch_header("action_dispatch.request.formats") do |k|
params_readable = begin
parameters[:format]
- rescue ActionController::BadRequest
+ rescue *RESCUABLE_MIME_FORMAT_ERRORS
false
end
@@ -76,6 +79,11 @@ module ActionDispatch
else
[Mime[:html]]
end
+
+ v = v.select do |format|
+ format.symbol || format.ref == "*/*"
+ end
+
set_header k, v
end
end
@@ -87,10 +95,7 @@ module ActionDispatch
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. " \
- "For security reasons, never directly set the variant to a user-provided value, " \
- "like params[:variant].to_sym. Check user-provided value against a whitelist first, " \
- "then set the variant: request.variant = :tablet if params[:variant] == 'tablet'"
+ raise ArgumentError, "request.variant must be set to a Symbol or an Array of Symbols."
end
end
@@ -135,9 +140,7 @@ module ActionDispatch
}
end
- # Receives an array of mimes and return the first user sent mime that
- # matches the order array.
- #
+ # Returns the first MIME type that matches the provided array of MIME types.
def negotiate_mime(order)
formats.each do |priority|
if priority == Mime::ALL
@@ -150,25 +153,25 @@ module ActionDispatch
order.include?(Mime::ALL) ? format : nil
end
- protected
+ private
- BROWSER_LIKE_ACCEPTS = /,\s*\*\/\*|\*\/\*\s*,/
+ BROWSER_LIKE_ACCEPTS = /,\s*\*\/\*|\*\/\*\s*,/
- def valid_accept_header
- (xhr? && (accept.present? || content_mime_type)) ||
- (accept.present? && accept !~ BROWSER_LIKE_ACCEPTS)
- end
+ def valid_accept_header # :doc:
+ (xhr? && (accept.present? || content_mime_type)) ||
+ (accept.present? && accept !~ BROWSER_LIKE_ACCEPTS)
+ end
- def use_accept_header
- !self.class.ignore_accept_header
- end
+ def use_accept_header # :doc:
+ !self.class.ignore_accept_header
+ end
- def format_from_path_extension
- path = @env['action_dispatch.original_path'] || @env['PATH_INFO']
- if match = path && path.match(/\.(\w+)\z/)
- Mime[match.captures.first]
+ 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
end
diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb
index 4672ea7199..88b3a93211 100644
--- a/actionpack/lib/action_dispatch/http/mime_type.rb
+++ b/actionpack/lib/action_dispatch/http/mime_type.rb
@@ -1,8 +1,7 @@
-# -*- frozen-string-literal: true -*-
+# frozen_string_literal: true
-require 'singleton'
-require 'active_support/core_ext/module/attribute_accessors'
-require 'active_support/core_ext/string/starts_ends_with'
+require "singleton"
+require "active_support/core_ext/string/starts_ends_with"
module Mime
class Mimes
@@ -45,35 +44,9 @@ module Mime
return type if type.is_a?(Type)
EXTENSION_LOOKUP.fetch(type.to_s) { |k| yield k }
end
-
- def const_missing(sym)
- ext = sym.downcase
- if Mime[ext]
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- Accessing mime types via constants is deprecated.
- Please change `Mime::#{sym}` to `Mime[:#{ext}]`.
- MSG
- Mime[ext]
- else
- super
- end
- end
-
- def const_defined?(sym, inherit = true)
- ext = sym.downcase
- if Mime[ext]
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- Accessing mime types via constants is deprecated.
- Please change `Mime.const_defined?(#{sym})` to `Mime[:#{ext}]`.
- MSG
- true
- else
- super
- end
- end
end
- # Encapsulates the notion of a mime type. Can be used at render time, for example, with:
+ # Encapsulates the notion of a MIME type. Can be used at render time, for example, with:
#
# class PostsController < ActionController::Base
# def show
@@ -91,7 +64,7 @@ module Mime
@register_callbacks = []
- # A simple helper class used in parsing the accept header
+ # A simple helper class used in parsing the accept header.
class AcceptItem #:nodoc:
attr_accessor :index, :name, :q
alias :to_s :name
@@ -99,7 +72,7 @@ module Mime
def initialize(index, name, q = nil)
@index = index
@name = name
- q ||= 0.0 if @name == '*/*'.freeze # default wildcard match to end of list
+ q ||= 0.0 if @name == "*/*" # Default wildcard match to end of list.
@q = ((q || 1.0).to_f * 100).to_i
end
@@ -114,25 +87,25 @@ module Mime
def self.sort!(list)
list.sort!
- text_xml_idx = find_item_by_name list, 'text/xml'
+ 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
+ # 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
+ 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
+ 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
+ # 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
@@ -141,7 +114,7 @@ module Mime
type = list[idx]
break if type.q < app_xml.q
- if type.name.ends_with? '+xml'
+ if type.name.ends_with? "+xml"
list[app_xml_idx], list[idx] = list[idx], app_xml
app_xml_idx = idx
end
@@ -174,7 +147,7 @@ module Mime
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
+ # 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.
def register_alias(string, symbol, extension_synonyms = [])
register(string, symbol, [], extension_synonyms, true)
@@ -195,12 +168,13 @@ module Mime
end
def parse(accept_header)
- if !accept_header.include?(',')
+ 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|
+ accept_header.split(",").each do |header|
params, q = header.split(PARAMETER_SEPARATOR_REGEXP)
next unless params
@@ -248,7 +222,18 @@ module Mime
attr_reader :hash
+ MIME_NAME = "[a-zA-Z0-9][a-zA-Z0-9#{Regexp.escape('!#$&-^_.+')}]{0,126}"
+ MIME_PARAMETER_KEY = "[a-zA-Z0-9][a-zA-Z0-9#{Regexp.escape('!#$&-^_.+')}]{0,126}"
+ MIME_PARAMETER_VALUE = "#{Regexp.escape('"')}?[a-zA-Z0-9][a-zA-Z0-9#{Regexp.escape('!#$&-^_.+')}]{0,126}#{Regexp.escape('"')}?"
+ MIME_PARAMETER = "\s*\;\s+#{MIME_PARAMETER_KEY}(?:\=#{MIME_PARAMETER_VALUE})?"
+ MIME_REGEXP = /\A(?:\*\/\*|#{MIME_NAME}\/(?:\*|#{MIME_NAME})(?:\s*#{MIME_PARAMETER}\s*)*)\z/
+
+ class InvalidMimeType < StandardError; end
+
def initialize(string, symbol = nil, synonyms = [])
+ unless MIME_REGEXP.match?(string)
+ raise InvalidMimeType, "#{string.inspect} is not a valid MIME type"
+ end
@symbol, @synonyms = symbol, synonyms
@string = string
@hash = [@string, @synonyms, @symbol].hash
@@ -306,31 +291,31 @@ module Mime
protected
- attr_reader :string, :synonyms
+ attr_reader :string, :synonyms
private
- def to_ary; end
- def to_a; end
+ def to_ary; end
+ def to_a; end
- def method_missing(method, *args)
- if method.to_s.ends_with? '?'
- method[0..-2].downcase.to_sym == to_sym
- else
- super
+ def method_missing(method, *args)
+ if method.to_s.ends_with? "?"
+ method[0..-2].downcase.to_sym == to_sym
+ else
+ super
+ end
end
- end
- def respond_to_missing?(method, include_private = false) #:nodoc:
- method.to_s.ends_with? '?'
- end
+ def respond_to_missing?(method, include_private = false)
+ (method.to_s.ends_with? "?") || super
+ end
end
class AllType < Type
include Singleton
def initialize
- super '*/*', :all
+ super "*/*", nil
end
def all?; true; end
@@ -351,15 +336,15 @@ module Mime
def ref; end
- def respond_to_missing?(method, include_private = false)
- method.to_s.ends_with? '?'
- end
-
private
- def method_missing(method, *args)
- false if method.to_s.ends_with? '?'
- end
+ def respond_to_missing?(method, _)
+ method.to_s.ends_with? "?"
+ end
+
+ def method_missing(method, *args)
+ false if method.to_s.ends_with? "?"
+ end
end
end
-require 'action_dispatch/http/mime_types'
+require "action_dispatch/http/mime_types"
diff --git a/actionpack/lib/action_dispatch/http/mime_types.rb b/actionpack/lib/action_dispatch/http/mime_types.rb
index 66cea88256..342e6de312 100644
--- a/actionpack/lib/action_dispatch/http/mime_types.rb
+++ b/actionpack/lib/action_dispatch/http/mime_types.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
# Build list of Mime types for HTTP responses
-# http://www.iana.org/assignments/media-types/
+# https://www.iana.org/assignments/media-types/
Mime::Type.register "text/html", :html, %w( application/xhtml+xml ), %w( xhtml )
Mime::Type.register "text/plain", :text, [], %w(txt)
@@ -8,6 +10,7 @@ Mime::Type.register "text/css", :css
Mime::Type.register "text/calendar", :ics
Mime::Type.register "text/csv", :csv
Mime::Type.register "text/vcard", :vcf
+Mime::Type.register "text/vtt", :vtt, %w(vtt)
Mime::Type.register "image/png", :png, [], %w(png)
Mime::Type.register "image/jpeg", :jpeg, [], %w(jpg jpeg jpe pjpeg)
@@ -18,15 +21,27 @@ Mime::Type.register "image/svg+xml", :svg
Mime::Type.register "video/mpeg", :mpeg, [], %w(mpg mpeg mpe)
+Mime::Type.register "audio/mpeg", :mp3, [], %w(mp1 mp2 mp3)
+Mime::Type.register "audio/ogg", :ogg, [], %w(oga ogg spx opus)
+Mime::Type.register "audio/aac", :m4a, %w( audio/mp4 ), %w(m4a mpg4 aac)
+
+Mime::Type.register "video/webm", :webm, [], %w(webm)
+Mime::Type.register "video/mp4", :mp4, [], %w(mp4 m4v)
+
+Mime::Type.register "font/otf", :otf, [], %w(otf)
+Mime::Type.register "font/ttf", :ttf, [], %w(ttf)
+Mime::Type.register "font/woff", :woff, [], %w(woff)
+Mime::Type.register "font/woff2", :woff2, [], %w(woff2)
+
Mime::Type.register "application/xml", :xml, %w( text/xml application/x-xml )
Mime::Type.register "application/rss+xml", :rss
Mime::Type.register "application/atom+xml", :atom
-Mime::Type.register "application/x-yaml", :yaml, %w( text/yaml )
+Mime::Type.register "application/x-yaml", :yaml, %w( text/yaml ), %w(yml yaml)
Mime::Type.register "multipart/form-data", :multipart_form
Mime::Type.register "application/x-www-form-urlencoded", :url_encoded_form
-# http://www.ietf.org/rfc/rfc4627.txt
+# https://www.ietf.org/rfc/rfc4627.txt
# http://www.json.org/JSONRequest.html
Mime::Type.register "application/json", :json, %w( text/x-json application/jsonrequest )
diff --git a/actionpack/lib/action_dispatch/http/parameter_filter.rb b/actionpack/lib/action_dispatch/http/parameter_filter.rb
index e826551f4b..ddeb3d81e2 100644
--- a/actionpack/lib/action_dispatch/http/parameter_filter.rb
+++ b/actionpack/lib/action_dispatch/http/parameter_filter.rb
@@ -1,82 +1,12 @@
-module ActionDispatch
- module Http
- class ParameterFilter
- FILTERED = '[FILTERED]'.freeze # :nodoc:
-
- def initialize(filters = [])
- @filters = filters
- end
-
- def filter(params)
- compiled_filter.call(params)
- end
-
- private
-
- def compiled_filter
- @compiled_filter ||= CompiledFilter.compile(@filters)
- end
-
- class CompiledFilter # :nodoc:
- def self.compile(filters)
- return lambda { |params| params.dup } if filters.empty?
-
- strings, regexps, blocks = [], [], []
-
- filters.each do |item|
- case item
- when Proc
- blocks << item
- when Regexp
- regexps << item
- else
- strings << Regexp.escape(item.to_s)
- end
- end
+# frozen_string_literal: true
- deep_regexps, regexps = regexps.partition { |r| r.to_s.include?("\\.".freeze) }
- deep_strings, strings = strings.partition { |s| s.include?("\\.".freeze) }
+require "active_support/deprecation/constant_accessor"
+require "active_support/parameter_filter"
- regexps << Regexp.new(strings.join('|'.freeze), true) unless strings.empty?
- deep_regexps << Regexp.new(deep_strings.join('|'.freeze), true) unless deep_strings.empty?
-
- new regexps, deep_regexps, blocks
- end
-
- attr_reader :regexps, :deep_regexps, :blocks
-
- def initialize(regexps, deep_regexps, blocks)
- @regexps = regexps
- @deep_regexps = deep_regexps.any? ? deep_regexps : nil
- @blocks = blocks
- end
-
- def call(original_params, parents = [])
- filtered_params = {}
-
- original_params.each do |key, value|
- parents.push(key) if deep_regexps
- if regexps.any? { |r| key =~ r }
- value = FILTERED
- elsif deep_regexps && (joined = parents.join('.')) && deep_regexps.any? { |r| joined =~ r }
- value = FILTERED
- elsif value.is_a?(Hash)
- value = call(value, parents)
- elsif value.is_a?(Array)
- value = value.map { |v| v.is_a?(Hash) ? call(v, parents) : v }
- elsif blocks.any?
- key = key.dup if key.duplicable?
- value = value.dup if value.duplicable?
- blocks.each { |b| b.call(key, value) }
- end
- parents.pop if deep_regexps
-
- filtered_params[key] = value
- end
-
- filtered_params
- end
- end
- end
+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
diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb
index ff5031d7d5..13d0963a33 100644
--- a/actionpack/lib/action_dispatch/http/parameters.rb
+++ b/actionpack/lib/action_dispatch/http/parameters.rb
@@ -1,19 +1,30 @@
+# frozen_string_literal: true
+
module ActionDispatch
module Http
module Parameters
extend ActiveSupport::Concern
- PARAMETERS_KEY = 'action_dispatch.request.path_parameters'
+ PARAMETERS_KEY = "action_dispatch.request.path_parameters"
DEFAULT_PARSERS = {
Mime[:json].symbol => -> (raw_post) {
data = ActiveSupport::JSON.decode(raw_post)
- data.is_a?(Hash) ? data : {:_json => data}
+ 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.
+ class ParseError < StandardError
+ def initialize
+ super($!.message)
+ end
+ end
+
included do
class << self
+ # Returns the parameter parsers.
attr_reader :parameter_parsers
end
@@ -21,7 +32,16 @@ module ActionDispatch
end
module ClassMethods
- def parameter_parsers=(parsers) # :nodoc:
+ # 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
+ def parameter_parsers=(parsers)
@parameter_parsers = parsers.transform_keys { |key| key.respond_to?(:symbol) ? key.symbol : key }
end
end
@@ -37,14 +57,23 @@ module ActionDispatch
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
alias :params :parameters
def path_parameters=(parameters) #:nodoc:
- delete_header('action_dispatch.request.parameters')
+ 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.
@@ -57,24 +86,51 @@ module ActionDispatch
private
- def parse_formatted_parameters(parsers)
- return yield if content_length.zero?
+ 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) do |param|
+ param.force_encoding ::Encoding::ASCII_8BIT
+ end
+ end
+ params
+ end
+
+ def binary_params_for?(controller, action)
+ controller_class_for(controller).binary_params_for?(action)
+ rescue NameError
+ false
+ end
- strategy = parsers.fetch(content_mime_type.symbol) { return yield }
+ def parse_formatted_parameters(parsers)
+ return yield if content_length.zero? || content_mime_type.nil?
- begin
- strategy.call(raw_post)
- rescue # JSON or Ruby code block errors
- my_logger = logger || ActiveSupport::Logger.new($stderr)
- my_logger.debug "Error occurred while parsing request parameters.\nContents:\n\n#{raw_post}"
+ strategy = parsers.fetch(content_mime_type.symbol) { return yield }
- raise ParamsParser::ParseError
+ begin
+ strategy.call(raw_post)
+ rescue # JSON or Ruby code block errors.
+ log_parse_error_once
+ raise ParseError
+ end
end
- end
- def params_parsers
- ActionDispatch::Request.parameter_parsers
- end
+ 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
+
+ def params_parsers
+ ActionDispatch::Request.parameter_parsers
+ end
end
end
end
diff --git a/actionpack/lib/action_dispatch/http/rack_cache.rb b/actionpack/lib/action_dispatch/http/rack_cache.rb
index 003ae4029d..3e2d01aea3 100644
--- a/actionpack/lib/action_dispatch/http/rack_cache.rb
+++ b/actionpack/lib/action_dispatch/http/rack_cache.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "rack/cache"
require "rack/cache/context"
require "active_support/cache"
diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb
index b0ed681623..44f23940d3 100644
--- a/actionpack/lib/action_dispatch/http/request.rb
+++ b/actionpack/lib/action_dispatch/http/request.rb
@@ -1,16 +1,18 @@
-require 'stringio'
-
-require 'active_support/inflector'
-require 'action_dispatch/http/headers'
-require 'action_controller/metal/exceptions'
-require 'rack/request'
-require 'action_dispatch/http/cache'
-require 'action_dispatch/http/mime_negotiation'
-require 'action_dispatch/http/parameters'
-require 'action_dispatch/http/filter_parameters'
-require 'action_dispatch/http/upload'
-require 'action_dispatch/http/url'
-require 'active_support/core_ext/array/conversions'
+# frozen_string_literal: true
+
+require "stringio"
+
+require "active_support/inflector"
+require "action_dispatch/http/headers"
+require "action_controller/metal/exceptions"
+require "rack/request"
+require "action_dispatch/http/cache"
+require "action_dispatch/http/mime_negotiation"
+require "action_dispatch/http/parameters"
+require "action_dispatch/http/filter_parameters"
+require "action_dispatch/http/upload"
+require "action_dispatch/http/url"
+require "active_support/core_ext/array/conversions"
module ActionDispatch
class Request
@@ -20,10 +22,11 @@ module ActionDispatch
include ActionDispatch::Http::Parameters
include ActionDispatch::Http::FilterParameters
include ActionDispatch::Http::URL
+ include ActionDispatch::ContentSecurityPolicy::Request
include Rack::Request::Env
- autoload :Session, 'action_dispatch/request/session'
- autoload :Utils, 'action_dispatch/request/utils'
+ autoload :Session, "action_dispatch/request/session"
+ autoload :Utils, "action_dispatch/request/utils"
LOCALHOST = Regexp.union [/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/, /^::1$/, /^0:0:0:0:0:0:0:1(%.*)?$/]
@@ -66,29 +69,21 @@ module ActionDispatch
def commit_cookie_jar! # :nodoc:
end
- def check_path_parameters!
- # If any of the path parameters has an invalid encoding then
- # raise since it's likely to trigger errors further on.
- path_parameters.each do |key, value|
- next unless value.respond_to?(:valid_encoding?)
- unless value.valid_encoding?
- raise ActionController::BadRequest, "Invalid parameter encoding: #{key} => #{value.inspect}"
- end
- end
- end
-
PASS_NOT_FOUND = Class.new { # :nodoc:
def self.action(_); self; end
- def self.call(_); [404, {'X-Cascade' => 'pass'}, []]; end
+ def self.call(_); [404, { "X-Cascade" => "pass" }, []]; end
+ def self.binary_params_for?(action); false; end
}
def controller_class
- check_path_parameters!
params = path_parameters
+ params[:action] ||= "index"
+ controller_class_for(params[:controller])
+ end
- if params.key?(:controller)
- controller_param = params[:controller].underscore
- params[:action] ||= 'index'
+ def controller_class_for(name)
+ if name
+ controller_param = name.underscore
const_name = "#{controller_param.camelize}Controller"
ActiveSupport::Dependencies.constantize(const_name)
else
@@ -96,19 +91,22 @@ module ActionDispatch
end
end
+ # Returns true if the request has a header matching the given key parameter.
+ #
+ # request.key? :ip_spoofing_check # => true
def key?(key)
has_header? key
end
# List of HTTP request methods from the following RFCs:
- # Hypertext Transfer Protocol -- HTTP/1.1 (http://www.ietf.org/rfc/rfc2616.txt)
- # HTTP Extensions for Distributed Authoring -- WEBDAV (http://www.ietf.org/rfc/rfc2518.txt)
- # Versioning Extensions to WebDAV (http://www.ietf.org/rfc/rfc3253.txt)
- # Ordered Collections Protocol (WebDAV) (http://www.ietf.org/rfc/rfc3648.txt)
- # Web Distributed Authoring and Versioning (WebDAV) Access Control Protocol (http://www.ietf.org/rfc/rfc3744.txt)
- # Web Distributed Authoring and Versioning (WebDAV) SEARCH (http://www.ietf.org/rfc/rfc5323.txt)
- # Calendar Extensions to WebDAV (http://www.ietf.org/rfc/rfc4791.txt)
- # PATCH Method for HTTP (http://www.ietf.org/rfc/rfc5789.txt)
+ # 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)
RFC2616 = %w(OPTIONS GET HEAD POST PUT DELETE TRACE CONNECT)
RFC2518 = %w(PROPFIND PROPPATCH MKCOL COPY MOVE LOCK UNLOCK)
RFC3253 = %w(VERSION-CONTROL REPORT CHECKOUT CHECKIN UNCHECKOUT MKWORKSPACE UPDATE LABEL MERGE BASELINE-CONTROL MKACTIVITY)
@@ -122,7 +120,7 @@ module ActionDispatch
HTTP_METHOD_LOOKUP = {}
- # Populate the HTTP method lookup cache
+ # Populate the HTTP method lookup cache.
HTTP_METHODS.each { |method|
HTTP_METHOD_LOOKUP[method] = method.underscore.to_sym
}
@@ -138,11 +136,11 @@ module ActionDispatch
end
def routes # :nodoc:
- get_header("action_dispatch.routes".freeze)
+ get_header("action_dispatch.routes")
end
def routes=(routes) # :nodoc:
- set_header("action_dispatch.routes".freeze, routes)
+ set_header("action_dispatch.routes", routes)
end
def engine_script_name(_routes) # :nodoc:
@@ -160,11 +158,11 @@ module ActionDispatch
end
def controller_instance # :nodoc:
- get_header('action_controller.instance'.freeze)
+ get_header("action_controller.instance")
end
def controller_instance=(controller) # :nodoc:
- set_header('action_controller.instance'.freeze, controller)
+ set_header("action_controller.instance", controller)
end
def http_auth_salt
@@ -173,12 +171,12 @@ module ActionDispatch
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
+ # `true`. This logic should be extracted to `env_config` and calculated
# once.
- !(get_header('action_dispatch.show_exceptions'.freeze) == false)
+ !(get_header("action_dispatch.show_exceptions") == false)
end
- # Returns a symbol form of the #request_method
+ # Returns a symbol form of the #request_method.
def request_method_symbol
HTTP_METHOD_LOOKUP[request_method]
end
@@ -187,10 +185,10 @@ module ActionDispatch
# even if it was overridden by middleware. See #request_method for
# more information.
def method
- @method ||= check_method(get_header("rack.methodoverride.original_method") || get_header('REQUEST_METHOD'))
+ @method ||= check_method(get_header("rack.methodoverride.original_method") || get_header("REQUEST_METHOD"))
end
- # Returns a symbol form of the #method
+ # Returns a symbol form of the #method.
def method_symbol
HTTP_METHOD_LOOKUP[method]
end
@@ -202,6 +200,23 @@ module ActionDispatch
@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.
+ 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'
@@ -249,7 +264,7 @@ module ActionDispatch
# (case-insensitive), which may need to be manually added depending on the
# choice of JavaScript libraries and frameworks.
def xml_http_request?
- get_header('HTTP_X_REQUESTED_WITH') =~ /XMLHttpRequest/i
+ get_header("HTTP_X_REQUESTED_WITH") =~ /XMLHttpRequest/i
end
alias :xhr? :xml_http_request?
@@ -265,17 +280,17 @@ module ActionDispatch
end
def remote_ip=(remote_ip)
- set_header "action_dispatch.remote_ip".freeze, remote_ip
+ set_header "action_dispatch.remote_ip", remote_ip
end
- ACTION_DISPATCH_REQUEST_ID = "action_dispatch.request_id".freeze # :nodoc:
+ ACTION_DISPATCH_REQUEST_ID = "action_dispatch.request_id" # :nodoc:
# Returns the unique request id, which is based on either the X-Request-Id header that can
# 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.
+ # This relies on the Rack variable set by the ActionDispatch::RequestId middleware.
def request_id
get_header ACTION_DISPATCH_REQUEST_ID
end
@@ -288,25 +303,25 @@ module ActionDispatch
# Returns the lowercase name of the HTTP server software.
def server_software
- (get_header('SERVER_SOFTWARE') && /^([a-zA-Z]+)/ =~ get_header('SERVER_SOFTWARE')) ? $1.downcase : nil
+ (get_header("SERVER_SOFTWARE") && /^([a-zA-Z]+)/ =~ get_header("SERVER_SOFTWARE")) ? $1.downcase : nil
end
# Read the request \body. This is useful for web services that need to
# work with raw requests directly.
def raw_post
- unless has_header? 'RAW_POST_DATA'
+ unless has_header? "RAW_POST_DATA"
raw_post_body = body
- set_header('RAW_POST_DATA', raw_post_body.read(content_length))
+ 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'
+ 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.
def body
- if raw_post = get_header('RAW_POST_DATA')
- raw_post.force_encoding(Encoding::BINARY)
+ if raw_post = get_header("RAW_POST_DATA")
+ raw_post = raw_post.dup.force_encoding(Encoding::BINARY)
StringIO.new(raw_post)
else
body_stream
@@ -326,7 +341,7 @@ module ActionDispatch
end
def body_stream #:nodoc:
- get_header('rack.input')
+ get_header("rack.input")
end
# TODO This should be broken apart into AD::Request::Session and probably
@@ -347,7 +362,7 @@ module ActionDispatch
Session::Options.set self, options
end
- # Override Rack's GET method to support indifferent access
+ # Override Rack's GET method to support indifferent access.
def GET
fetch_header("action_dispatch.request.query_parameters") do |k|
rack_query_params = super || {}
@@ -360,7 +375,7 @@ module ActionDispatch
end
alias :query_parameters :GET
- # Override Rack's POST method to support indifferent access
+ # Override Rack's POST method to support indifferent access.
def POST
fetch_header("action_dispatch.request.request_parameters") do
pr = parse_formatted_parameters(params_parsers) do |params|
@@ -368,9 +383,6 @@ module ActionDispatch
end
self.request_parameters = Request::Utils.normalize_encode_params(pr)
end
- rescue ParamsParser::ParseError # one of the parse strategies blew up
- self.request_parameters = Request::Utils.normalize_encode_params(super || {})
- raise
rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e
raise ActionController::BadRequest.new("Invalid request parameters: #{e.message}")
end
@@ -379,10 +391,10 @@ module ActionDispatch
# Returns the authorization header regardless of whether it was specified directly or through one of the
# proxy alternatives.
def authorization
- get_header('HTTP_AUTHORIZATION') ||
- get_header('X-HTTP_AUTHORIZATION') ||
- get_header('X_HTTP_AUTHORIZATION') ||
- get_header('REDIRECT_X_HTTP_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.
@@ -392,18 +404,18 @@ module ActionDispatch
def request_parameters=(params)
raise if params.nil?
- set_header("action_dispatch.request.request_parameters".freeze, params)
+ set_header("action_dispatch.request.request_parameters", params)
end
def logger
- get_header("action_dispatch.logger".freeze)
+ get_header("action_dispatch.logger")
end
def commit_flash
end
def ssl?
- super || scheme == 'wss'.freeze
+ super || scheme == "wss"
end
private
diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb
index fa4c54701a..69798f99e0 100644
--- a/actionpack/lib/action_dispatch/http/response.rb
+++ b/actionpack/lib/action_dispatch/http/response.rb
@@ -1,7 +1,9 @@
-require 'active_support/core_ext/module/attribute_accessors'
-require 'action_dispatch/http/filter_redirect'
-require 'action_dispatch/http/cache'
-require 'monitor'
+# frozen_string_literal: true
+
+require "active_support/core_ext/module/attribute_accessors"
+require "action_dispatch/http/filter_redirect"
+require "action_dispatch/http/cache"
+require "monitor"
module ActionDispatch # :nodoc:
# Represents an HTTP response generated by a controller action. Use it to
@@ -39,9 +41,9 @@ module ActionDispatch # :nodoc:
super(header)
end
- def []=(k,v)
+ def []=(k, v)
if @response.sending? || @response.sent?
- raise ActionDispatch::IllegalStateError, 'header already sent'
+ raise ActionDispatch::IllegalStateError, "header already sent"
end
super
@@ -67,19 +69,26 @@ module ActionDispatch # :nodoc:
alias_method :headers, :header
- delegate :[], :[]=, :to => :@header
- delegate :each, :to => :@stream
+ delegate :[], :[]=, to: :@header
+
+ def each(&block)
+ sending!
+ x = @stream.each(&block)
+ sent!
+ x
+ end
- CONTENT_TYPE = "Content-Type".freeze
- SET_COOKIE = "Set-Cookie".freeze
- LOCATION = "Location".freeze
+ CONTENT_TYPE = "Content-Type"
+ SET_COOKIE = "Set-Cookie"
+ LOCATION = "Location"
NO_CONTENT_CODES = [100, 101, 102, 204, 205, 304]
+ CONTENT_TYPE_PARSER = /\A(?<type>[^;\s]+)?(?:.*;\s*charset=(?<quote>"?)(?<charset>[^;\s]+)\k<quote>)?/ # :nodoc:
- cattr_accessor(:default_charset) { "utf-8" }
- cattr_accessor(:default_headers)
+ cattr_accessor :default_charset, default: "utf-8"
+ cattr_accessor :default_headers
include Rack::Response::Helpers
- # Aliasing these off because AD::Http::Cache::Response defines them
+ # Aliasing these off because AD::Http::Cache::Response defines them.
alias :_cache_control :cache_control
alias :_cache_control= :cache_control=
@@ -97,10 +106,10 @@ module ActionDispatch # :nodoc:
def body
@str_body ||= begin
- buf = ''
- each { |chunk| buf << chunk }
- buf
- end
+ buf = +""
+ each { |chunk| buf << chunk }
+ buf
+ end
end
def write(string)
@@ -112,10 +121,13 @@ module ActionDispatch # :nodoc:
end
def each(&block)
- @response.sending!
- x = @buf.each(&block)
- @response.sent!
- x
+ if @str_body
+ return enum_for(:each) unless block_given?
+
+ yield @str_body
+ else
+ each_chunk(&block)
+ end
end
def abort
@@ -129,6 +141,12 @@ module ActionDispatch # :nodoc:
def closed?
@closed
end
+
+ private
+
+ def each_chunk(&block)
+ @buf.each(&block)
+ end
end
def self.create(status = 200, header = {}, body = [], default_headers: self.default_headers)
@@ -207,12 +225,6 @@ module ActionDispatch # :nodoc:
@status = Rack::Utils.status_code(status)
end
- # Sets the HTTP content type.
- def content_type=(content_type)
- header_info = parse_content_type
- set_content_type content_type.to_s, header_info.charset || self.class.default_charset
- end
-
# Sets the HTTP response's content MIME type. For example, in the controller
# you could write this:
#
@@ -221,9 +233,19 @@ module ActionDispatch # :nodoc:
# 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.
+ 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.
+ # It returns just MIME type and does NOT contain charset part.
def content_type
- parse_content_type.mime_type
+ parsed_content_type_header.mime_type
end
def sending_file=(v)
@@ -232,17 +254,16 @@ module ActionDispatch # :nodoc:
end
end
- # Sets the HTTP character set. In case of nil parameter
- # it sets the charset to utf-8.
+ # 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'
def charset=(charset)
- header_info = parse_content_type
+ content_type = parsed_content_type_header.mime_type
if false == charset
- set_header CONTENT_TYPE, header_info.mime_type
+ set_content_type content_type, nil
else
- content_type = header_info.mime_type
set_content_type content_type, charset || self.class.default_charset
end
end
@@ -250,7 +271,7 @@ module ActionDispatch # :nodoc:
# 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.
def charset
- header_info = parse_content_type
+ header_info = parsed_content_type_header
header_info.charset || self.class.default_charset
end
@@ -314,7 +335,7 @@ module ActionDispatch # :nodoc:
# Stream the file's contents if Rack::Sendfile isn't present.
def each
- File.open(to_path, 'rb') do |file|
+ File.open(to_path, "rb") do |file|
while chunk = file.read(16384)
yield chunk
end
@@ -374,7 +395,7 @@ module ActionDispatch # :nodoc:
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
+ if pair = cookie.split(";").first
key, value = pair.split("=").map { |v| Rack::Utils.unescape(v) }
cookies[key] = value
end
@@ -388,26 +409,30 @@ module ActionDispatch # :nodoc:
ContentTypeHeader = Struct.new :mime_type, :charset
NullContentTypeHeader = ContentTypeHeader.new nil, nil
- def parse_content_type
- content_type = get_header CONTENT_TYPE
- if content_type
- type, charset = content_type.split(/;\s*charset=/)
- type = nil if type.empty?
- ContentTypeHeader.new(type, charset)
+ def parse_content_type(content_type)
+ if content_type && match = CONTENT_TYPE_PARSER.match(content_type)
+ ContentTypeHeader.new(match[:type], match[:charset])
else
NullContentTypeHeader
end
end
+ # Small internal convenience method to get the parsed version of the current
+ # content type header.
+ def parsed_content_type_header
+ parse_content_type(get_header(CONTENT_TYPE))
+ end
+
def set_content_type(content_type, charset)
- type = (content_type || '').dup
- type << "; charset=#{charset}" if charset
+ type = (content_type || "").dup
+ type << "; charset=#{charset.to_s.downcase}" if charset
set_header CONTENT_TYPE, type
end
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
@@ -435,7 +460,7 @@ module ActionDispatch # :nodoc:
def assign_default_content_type_and_charset!
return if content_type
- ct = parse_content_type
+ ct = parsed_content_type_header
set_content_type(ct.mime_type || Mime[:html].to_s,
ct.charset || self.class.default_charset)
end
@@ -460,7 +485,7 @@ module ActionDispatch # :nodoc:
end
def respond_to?(method, include_private = false)
- if method.to_s == 'to_path'
+ if method.to_s == "to_path"
@response.stream.respond_to?(method)
else
super
@@ -479,7 +504,7 @@ module ActionDispatch # :nodoc:
def handle_no_content!
if NO_CONTENT_CODES.include?(@status)
@header.delete CONTENT_TYPE
- @header.delete 'Content-Length'
+ @header.delete "Content-Length"
end
end
diff --git a/actionpack/lib/action_dispatch/http/upload.rb b/actionpack/lib/action_dispatch/http/upload.rb
index a221f4c5af..0da8f5c14e 100644
--- a/actionpack/lib/action_dispatch/http/upload.rb
+++ b/actionpack/lib/action_dispatch/http/upload.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionDispatch
module Http
# Models uploaded files.
@@ -18,29 +20,32 @@ module ActionDispatch
# A +Tempfile+ object with the actual uploaded file. Note that some of
# its interface is available directly.
attr_accessor :tempfile
- alias :to_io :tempfile
# A string with the headers of the multipart request.
attr_accessor :headers
def initialize(hash) # :nodoc:
- @tempfile = hash[:tempfile]
- raise(ArgumentError, ':tempfile is required') unless @tempfile
+ @tempfile = hash[:tempfile]
+ raise(ArgumentError, ":tempfile is required") unless @tempfile
+
+ if hash[:filename]
+ @original_filename = hash[:filename].dup
- @original_filename = hash[:filename]
- if @original_filename
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+.
- def read(length=nil, buffer=nil)
+ def read(length = nil, buffer = nil)
@tempfile.read(length, buffer)
end
@@ -50,7 +55,7 @@ module ActionDispatch
end
# Shortcut for +tempfile.close+.
- def close(unlink_now=false)
+ def close(unlink_now = false)
@tempfile.close(unlink_now)
end
@@ -59,6 +64,11 @@ module ActionDispatch
@tempfile.path
end
+ # Shortcut for +tempfile.to_path+.
+ def to_path
+ @tempfile.to_path
+ end
+
# Shortcut for +tempfile.rewind+.
def rewind
@tempfile.rewind
@@ -73,6 +83,10 @@ module ActionDispatch
def eof?
@tempfile.eof?
end
+
+ def to_io
+ @tempfile.to_io
+ end
end
end
end
diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb
index 37f41ae988..8227749986 100644
--- a/actionpack/lib/action_dispatch/http/url.rb
+++ b/actionpack/lib/action_dispatch/http/url.rb
@@ -1,4 +1,6 @@
-require 'active_support/core_ext/module/attribute_accessors'
+# frozen_string_literal: true
+
+require "active_support/core_ext/module/attribute_accessors"
module ActionDispatch
module Http
@@ -7,8 +9,7 @@ module ActionDispatch
HOST_REGEXP = /(^[^:]+:\/\/)?(\[[^\]]+\]|[^:]+)(?::(\d+$))?/
PROTOCOL_REGEXP = /^([^:]+)(:)?(\/\/)?$/
- mattr_accessor :tld_length
- self.tld_length = 1
+ mattr_accessor :tld_length, default: 1
class << self
# Returns the domain part of a host given the domain level.
@@ -42,7 +43,7 @@ module ActionDispatch
# # Second-level domain example
# extract_subdomain('dev.www.example.co.uk', 2) # => "dev.www"
def extract_subdomain(host, tld_length)
- extract_subdomains(host, tld_length).join('.')
+ extract_subdomains(host, tld_length).join(".")
end
def url_for(options)
@@ -59,14 +60,14 @@ module ActionDispatch
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'
+ 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
def path_for(options)
- path = options[:script_name].to_s.chomp("/".freeze)
+ path = options[:script_name].to_s.chomp("/")
path << options[:path] if options.key?(:path)
add_trailing_slash(path) if options[:trailing_slash]
@@ -78,110 +79,108 @@ module ActionDispatch
private
- 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
-
- def add_anchor(path, anchor)
- if anchor
- path << "##{Journey::Router::Utils.escape_fragment(anchor.to_param)}"
+ 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
- end
- def extract_domain_from(host, tld_length)
- host.split('.').last(1 + tld_length).join('.')
- end
+ def add_anchor(path, anchor)
+ if anchor
+ path << "##{Journey::Router::Utils.escape_fragment(anchor.to_param)}"
+ end
+ end
- def extract_subdomains_from(host, tld_length)
- parts = host.split('.')
- parts[0..-(tld_length + 2)]
- end
+ def extract_domain_from(host, tld_length)
+ host.split(".").last(1 + tld_length).join(".")
+ end
- def add_trailing_slash(path)
- # includes querysting
- if path.include?('?')
- path.sub!(/\?/, '/\&')
- # does not have a .format
- elsif !path.include?(".")
- path.sub!(/[^\/]\z|\A\z/, '\&/')
+ def extract_subdomains_from(host, tld_length)
+ parts = host.split(".")
+ parts[0..-(tld_length + 2)]
end
- end
- 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
+ def add_trailing_slash(path)
+ if path.include?("?")
+ path.sub!(/\?/, '/\&')
+ elsif !path.include?(".")
+ path.sub!(/[^\/]\z|\A\z/, '\&/')
+ end
end
- protocol = normalize_protocol protocol
- host = normalize_host(host, options)
+ 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
- result = protocol.dup
+ protocol = normalize_protocol protocol
+ host = normalize_host(host, options)
- if options[:user] && options[:password]
- result << "#{Rack::Utils.escape(options[:user])}:#{Rack::Utils.escape(options[:password])}@"
- end
+ result = protocol.dup
- result << host
- normalize_port(port, protocol) { |normalized_port|
- result << ":#{normalized_port}"
- }
+ if options[:user] && options[:password]
+ result << "#{Rack::Utils.escape(options[:user])}:#{Rack::Utils.escape(options[:password])}@"
+ end
- result.concat path
- end
+ result << host
+ normalize_port(port, protocol) { |normalized_port|
+ result << ":#{normalized_port}"
+ }
- def named_host?(host)
- IP_HOST_REGEXP !~ host
- end
+ result.concat path
+ end
- def normalize_protocol(protocol)
- case protocol
- when nil
- "http://"
- when false, "//"
- "//"
- when PROTOCOL_REGEXP
- "#{$1}://"
- else
- raise ArgumentError, "Invalid :protocol option: #{protocol.inspect}"
+ def named_host?(host)
+ IP_HOST_REGEXP !~ host
+ end
+
+ def normalize_protocol(protocol)
+ case protocol
+ when nil
+ "http://"
+ when false, "//"
+ "//"
+ when PROTOCOL_REGEXP
+ "#{$1}://"
+ else
+ raise ArgumentError, "Invalid :protocol option: #{protocol.inspect}"
+ end
end
- end
- def normalize_host(_host, options)
- return _host unless named_host?(_host)
+ 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]
+ 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 = +""
+ if subdomain == true
+ return _host if domain.nil?
- host << extract_subdomains_from(_host, tld_length).join('.')
- elsif subdomain
- host << subdomain.to_param
+ 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
- host << "." unless host.empty?
- host << (domain || extract_domain_from(_host, tld_length))
- host
- end
- def normalize_port(port, protocol)
- return unless port
+ 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
+ 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
end
def initialize
@@ -192,11 +191,7 @@ module ActionDispatch
# Returns the complete URL used for this request.
#
- # class Request < Rack::Request
- # include ActionDispatch::Http::URL
- # end
- #
- # req = Request.new 'HTTP_HOST' => 'example.com'
+ # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com'
# req.url # => "http://example.com"
def url
protocol + host_with_port + fullpath
@@ -204,61 +199,52 @@ module ActionDispatch
# Returns 'https://' if this is an SSL request and 'http://' otherwise.
#
- # class Request < Rack::Request
- # include ActionDispatch::Http::URL
- # end
- #
- # req = Request.new 'HTTP_HOST' => 'example.com'
+ # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com'
# req.protocol # => "http://"
#
- # req = Request.new 'HTTP_HOST' => 'example.com', 'HTTPS' => 'on'
+ # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com', 'HTTPS' => 'on'
# req.protocol # => "https://"
def protocol
- @protocol ||= ssl? ? 'https://' : 'http://'
+ @protocol ||= ssl? ? "https://" : "http://"
end
- # Returns the \host for this request, such as "example.com".
- #
- # class Request < Rack::Request
- # include ActionDispatch::Http::URL
- # end
+ # Returns the \host and port for this request, such as "example.com:8080".
#
- # req = Request.new 'HTTP_HOST' => 'example.com'
+ # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com'
# req.raw_host_with_port # => "example.com"
#
- # req = Request.new 'HTTP_HOST' => 'example.com:8080'
+ # 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"
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')}"
+ get_header("HTTP_HOST") || "#{server_name || server_addr}:#{get_header('SERVER_PORT')}"
end
end
- # Returns the host for this request, such as example.com.
- #
- # class Request < Rack::Request
- # include ActionDispatch::Http::URL
- # end
+ # Returns the host for this request, such as "example.com".
#
- # req = Request.new 'HTTP_HOST' => 'example.com:8080'
+ # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080'
# req.host # => "example.com"
def host
- raw_host_with_port.sub(/:\d+$/, ''.freeze)
+ raw_host_with_port.sub(/:\d+$/, "")
end
# Returns a \host:\port string for this request, such as "example.com" or
- # "example.com:8080".
+ # "example.com:8080". Port is only included if it is not a default port
+ # (80 or 443)
#
- # class Request < Rack::Request
- # include ActionDispatch::Http::URL
- # end
+ # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com'
+ # req.host_with_port # => "example.com"
#
- # req = Request.new 'HTTP_HOST' => 'example.com:80'
+ # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:80'
# req.host_with_port # => "example.com"
#
- # req = Request.new 'HTTP_HOST' => 'example.com:8080'
+ # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080'
# req.host_with_port # => "example.com:8080"
def host_with_port
"#{host}#{port_string}"
@@ -266,14 +252,10 @@ module ActionDispatch
# Returns the port number of this request as an integer.
#
- # class Request < Rack::Request
- # include ActionDispatch::Http::URL
- # end
- #
- # req = Request.new 'HTTP_HOST' => 'example.com'
+ # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com'
# req.port # => 80
#
- # req = Request.new 'HTTP_HOST' => 'example.com:8080'
+ # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080'
# req.port # => 8080
def port
@port ||= begin
@@ -287,29 +269,21 @@ module ActionDispatch
# Returns the standard \port number for this request's protocol.
#
- # class Request < Rack::Request
- # include ActionDispatch::Http::URL
- # end
- #
- # req = Request.new 'HTTP_HOST' => 'example.com:8080'
+ # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080'
# req.standard_port # => 80
def standard_port
case protocol
- when 'https://' then 443
- else 80
+ when "https://" then 443
+ else 80
end
end
# Returns whether this request is using the standard port
#
- # class Request < Rack::Request
- # include ActionDispatch::Http::URL
- # end
- #
- # req = Request.new 'HTTP_HOST' => 'example.com:80'
+ # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:80'
# req.standard_port? # => true
#
- # req = Request.new 'HTTP_HOST' => 'example.com:8080'
+ # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080'
# req.standard_port? # => false
def standard_port?
port == standard_port
@@ -318,14 +292,10 @@ module ActionDispatch
# 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.
#
- # class Request < Rack::Request
- # include ActionDispatch::Http::URL
- # end
- #
- # req = Request.new 'HTTP_HOST' => 'example.com:80'
+ # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:80'
# req.optional_port # => nil
#
- # req = Request.new 'HTTP_HOST' => 'example.com:8080'
+ # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080'
# req.optional_port # => 8080
def optional_port
standard_port? ? nil : port
@@ -334,21 +304,24 @@ module ActionDispatch
# 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.
#
- # class Request < Rack::Request
- # include ActionDispatch::Http::URL
- # end
- #
- # req = Request.new 'HTTP_HOST' => 'example.com:80'
+ # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:80'
# req.port_string # => ""
#
- # req = Request.new 'HTTP_HOST' => 'example.com:8080'
+ # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080'
# req.port_string # => ":8080"
def port_string
- standard_port? ? '' : ":#{port}"
+ 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
def server_port
- get_header('SERVER_PORT').to_i
+ 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
diff --git a/actionpack/lib/action_dispatch/journey.rb b/actionpack/lib/action_dispatch/journey.rb
index ad42713482..2852efa6ae 100644
--- a/actionpack/lib/action_dispatch/journey.rb
+++ b/actionpack/lib/action_dispatch/journey.rb
@@ -1,5 +1,7 @@
-require 'action_dispatch/journey/router'
-require 'action_dispatch/journey/gtg/builder'
-require 'action_dispatch/journey/gtg/simulator'
-require 'action_dispatch/journey/nfa/builder'
-require 'action_dispatch/journey/nfa/simulator'
+# frozen_string_literal: true
+
+require "action_dispatch/journey/router"
+require "action_dispatch/journey/gtg/builder"
+require "action_dispatch/journey/gtg/simulator"
+require "action_dispatch/journey/nfa/builder"
+require "action_dispatch/journey/nfa/simulator"
diff --git a/actionpack/lib/action_dispatch/journey/formatter.rb b/actionpack/lib/action_dispatch/journey/formatter.rb
index 0323360faa..52396ec901 100644
--- a/actionpack/lib/action_dispatch/journey/formatter.rb
+++ b/actionpack/lib/action_dispatch/journey/formatter.rb
@@ -1,10 +1,13 @@
-require 'action_controller/metal/exceptions'
+# frozen_string_literal: true
+
+require "action_controller/metal/exceptions"
module ActionDispatch
+ # :stopdoc:
module Journey
# The Formatter class is used for formatting URLs. For example, parameters
# passed to +url_for+ in Rails will eventually call Formatter#generate.
- class Formatter # :nodoc:
+ class Formatter
attr_reader :routes
def initialize(routes)
@@ -14,7 +17,7 @@ module ActionDispatch
def generate(name, options, path_parameters, parameterize = nil)
constraints = path_parameters.merge(options)
- missing_keys = nil # need for variable scope
+ missing_keys = nil
match_route(name, constraints) do |route|
parameterized_parts = extract_parameterized_parts(route, options, path_parameters, parameterize)
@@ -32,15 +35,24 @@ module ActionDispatch
defaults = route.defaults
required_parts = route.required_parts
- parameterized_parts.keep_if do |key, value|
- (defaults[key].nil? && value.present?) || value.to_s != defaults[key].to_s || required_parts.include?(key)
+
+ 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 [route.format(parameterized_parts), params]
end
- 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?
+ unmatched_keys = (missing_keys || []) & constraints.keys
+ missing_keys = (missing_keys || []) - unmatched_keys
+
+ 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?
raise ActionController::UrlGenerationError, message
end
@@ -82,7 +94,11 @@ module ActionDispatch
else
routes = non_recursive(cache, options)
- hash = routes.group_by { |_, r| r.score(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
@@ -169,4 +185,5 @@ module ActionDispatch
end
end
end
+ # :startdoc:
end
diff --git a/actionpack/lib/action_dispatch/journey/gtg/builder.rb b/actionpack/lib/action_dispatch/journey/gtg/builder.rb
index 450588cda6..44c31053cb 100644
--- a/actionpack/lib/action_dispatch/journey/gtg/builder.rb
+++ b/actionpack/lib/action_dispatch/journey/gtg/builder.rb
@@ -1,4 +1,6 @@
-require 'action_dispatch/journey/gtg/transition_table'
+# frozen_string_literal: true
+
+require "action_dispatch/journey/gtg/transition_table"
module ActionDispatch
module Journey # :nodoc:
@@ -17,7 +19,7 @@ module ActionDispatch
def transition_table
dtrans = TransitionTable.new
marked = {}
- state_id = Hash.new { |h,k| h[k] = h.length }
+ state_id = Hash.new { |h, k| h[k] = h.length }
start = firstpos(root)
dstates = [start]
@@ -75,7 +77,7 @@ module ActionDispatch
when Nodes::Unary
nullable?(node.left)
else
- raise ArgumentError, 'unknown nullable: %s' % node.class.name
+ raise ArgumentError, "unknown nullable: %s" % node.class.name
end
end
@@ -96,7 +98,7 @@ module ActionDispatch
when Nodes::Terminal
nullable?(node) ? [] : [node]
else
- raise ArgumentError, 'unknown firstpos: %s' % node.class.name
+ raise ArgumentError, "unknown firstpos: %s" % node.class.name
end
end
@@ -117,7 +119,7 @@ module ActionDispatch
when Nodes::Unary
lastpos(node.left)
else
- raise ArgumentError, 'unknown lastpos: %s' % node.class.name
+ raise ArgumentError, "unknown lastpos: %s" % node.class.name
end
end
diff --git a/actionpack/lib/action_dispatch/journey/gtg/simulator.rb b/actionpack/lib/action_dispatch/journey/gtg/simulator.rb
index 94b0a24344..2ee4f5c30c 100644
--- a/actionpack/lib/action_dispatch/journey/gtg/simulator.rb
+++ b/actionpack/lib/action_dispatch/journey/gtg/simulator.rb
@@ -1,4 +1,6 @@
-require 'strscan'
+# frozen_string_literal: true
+
+require "strscan"
module ActionDispatch
module Journey # :nodoc:
@@ -18,14 +20,6 @@ module ActionDispatch
@tt = transition_table
end
- def simulate(string)
- ms = memos(string) { return }
- MatchData.new(ms)
- end
-
- alias :=~ :simulate
- alias :match :simulate
-
def memos(string)
input = StringScanner.new(string)
state = [0]
diff --git a/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb b/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb
index d7ce6042c2..ea647e051a 100644
--- a/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb
+++ b/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb
@@ -1,4 +1,6 @@
-require 'action_dispatch/journey/nfa/dot'
+# frozen_string_literal: true
+
+require "action_dispatch/journey/nfa/dot"
module ActionDispatch
module Journey # :nodoc:
@@ -12,7 +14,7 @@ module ActionDispatch
@regexp_states = {}
@string_states = {}
@accepting = {}
- @memos = Hash.new { |h,k| h[k] = [] }
+ @memos = Hash.new { |h, k| h[k] = [] }
end
def add_accepting(state)
@@ -56,7 +58,7 @@ module ActionDispatch
end
def as_json(options = nil)
- simple_regexp = Hash.new { |h,k| h[k] = {} }
+ simple_regexp = Hash.new { |h, k| h[k] = {} }
@regexp_states.each do |from, hash|
hash.each do |re, to|
@@ -72,20 +74,20 @@ module ActionDispatch
end
def to_svg
- svg = IO.popen('dot -Tsvg', 'w+') { |f|
+ 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="[^"]*"/, '')
+ svg.join.sub(/width="[^"]*"/, "").sub(/height="[^"]*"/, "")
end
- def visualizer(paths, title = 'FSM')
- viz_dir = File.join File.dirname(__FILE__), '..', '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')
+ 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|
@@ -93,10 +95,10 @@ module ActionDispatch
case n
when Nodes::Symbol
case n.left
- when ':id' then rand(100).to_s
- when ':format' then %w{ xml json }.sample
+ when ":id" then rand(100).to_s
+ when ":format" then %w{ xml json }.sample
else
- 'omg'
+ "omg"
end
when Nodes::Terminal then n.symbol
else
@@ -109,13 +111,12 @@ module ActionDispatch
svg = to_svg
javascripts = [states, fsm_js]
- # Annoying hack warnings
fun_routes = fun_routes
stylesheets = stylesheets
svg = svg
javascripts = javascripts
- require 'erb'
+ require "erb"
template = ERB.new erb
template.result(binding)
end
@@ -148,7 +149,7 @@ module ActionDispatch
when Regexp
@regexp_states
else
- raise ArgumentError, 'unknown symbol: %s' % sym.class
+ raise ArgumentError, "unknown symbol: %s" % sym.class
end
end
end
diff --git a/actionpack/lib/action_dispatch/journey/nfa/builder.rb b/actionpack/lib/action_dispatch/journey/nfa/builder.rb
index ee6494c3e4..d22302e101 100644
--- a/actionpack/lib/action_dispatch/journey/nfa/builder.rb
+++ b/actionpack/lib/action_dispatch/journey/nfa/builder.rb
@@ -1,5 +1,7 @@
-require 'action_dispatch/journey/nfa/transition_table'
-require 'action_dispatch/journey/gtg/transition_table'
+# frozen_string_literal: true
+
+require "action_dispatch/journey/nfa/transition_table"
+require "action_dispatch/journey/gtg/transition_table"
module ActionDispatch
module Journey # :nodoc:
@@ -36,7 +38,7 @@ module ActionDispatch
def visit_OR(node)
from = @i += 1
children = node.children.map { |c| visit(c) }
- to = @i += 1
+ to = @i += 1
children.each do |child|
@tt[from, child.first] = nil
diff --git a/actionpack/lib/action_dispatch/journey/nfa/dot.rb b/actionpack/lib/action_dispatch/journey/nfa/dot.rb
index 7063b44bb5..56e9e3c83d 100644
--- a/actionpack/lib/action_dispatch/journey/nfa/dot.rb
+++ b/actionpack/lib/action_dispatch/journey/nfa/dot.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionDispatch
module Journey # :nodoc:
module NFA # :nodoc:
@@ -7,18 +9,18 @@ module ActionDispatch
" #{from} -> #{to} [label=\"#{sym || 'ε'}\"];"
}
- #memo_nodes = memos.values.flatten.map { |n|
- # label = n
- # if Journey::Route === n
- # label = "#{n.verb.source} #{n.path.spec}"
- # end
- # " #{n.object_id} [label=\"#{label}\", shape=box];"
- #}
- #memo_edges = memos.flat_map { |k, memos|
- # (memos || []).map { |v| " #{k} -> #{v.object_id};" }
- #}.uniq
+ # memo_nodes = memos.values.flatten.map { |n|
+ # label = n
+ # if Journey::Route === n
+ # label = "#{n.verb.source} #{n.path.spec}"
+ # end
+ # " #{n.object_id} [label=\"#{label}\", shape=box];"
+ # }
+ # memo_edges = memos.flat_map { |k, memos|
+ # (memos || []).map { |v| " #{k} -> #{v.object_id};" }
+ # }.uniq
- <<-eodot
+ <<-eodot
digraph nfa {
rankdir=LR;
node [shape = doublecircle];
@@ -26,7 +28,7 @@ digraph nfa {
node [shape = circle];
#{edges.join "\n"}
}
- eodot
+ eodot
end
end
end
diff --git a/actionpack/lib/action_dispatch/journey/nfa/simulator.rb b/actionpack/lib/action_dispatch/journey/nfa/simulator.rb
index b23270db3c..002f6feb97 100644
--- a/actionpack/lib/action_dispatch/journey/nfa/simulator.rb
+++ b/actionpack/lib/action_dispatch/journey/nfa/simulator.rb
@@ -1,4 +1,6 @@
-require 'strscan'
+# frozen_string_literal: true
+
+require "strscan"
module ActionDispatch
module Journey # :nodoc:
@@ -23,8 +25,6 @@ module ActionDispatch
state = tt.eclosure(0)
until input.eos?
sym = input.scan(%r([/.?]|[^/.?]+))
-
- # FIXME: tt.eclosure is not needed for the GTG
state = tt.eclosure(tt.move(state, sym))
end
diff --git a/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb b/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb
index 0ccab21801..fe55861507 100644
--- a/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb
+++ b/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb
@@ -1,4 +1,6 @@
-require 'action_dispatch/journey/nfa/dot'
+# frozen_string_literal: true
+
+require "action_dispatch/journey/nfa/dot"
module ActionDispatch
module Journey # :nodoc:
@@ -10,7 +12,7 @@ module ActionDispatch
attr_reader :memos
def initialize
- @table = Hash.new { |h,f| h[f] = {} }
+ @table = Hash.new { |h, f| h[f] = {} }
@memos = {}
@accepting = nil
@inverted = nil
diff --git a/actionpack/lib/action_dispatch/journey/nodes/node.rb b/actionpack/lib/action_dispatch/journey/nodes/node.rb
index 2793c5668d..086d6a3e07 100644
--- a/actionpack/lib/action_dispatch/journey/nodes/node.rb
+++ b/actionpack/lib/action_dispatch/journey/nodes/node.rb
@@ -1,4 +1,6 @@
-require 'action_dispatch/journey/visitors'
+# frozen_string_literal: true
+
+require "action_dispatch/journey/visitors"
module ActionDispatch
module Journey # :nodoc:
@@ -18,7 +20,7 @@ module ActionDispatch
end
def to_s
- Visitors::String::INSTANCE.accept(self, '')
+ Visitors::String::INSTANCE.accept(self, "")
end
def to_dot
@@ -30,7 +32,7 @@ module ActionDispatch
end
def name
- left.tr '*:'.freeze, ''.freeze
+ -left.tr("*:", "")
end
def type
@@ -63,12 +65,12 @@ module ActionDispatch
def literal?; false; end
end
- %w{ Symbol Slash Dot }.each do |t|
- class_eval <<-eoruby, __FILE__, __LINE__ + 1
- class #{t} < Terminal;
- def type; :#{t.upcase}; end
- end
- eoruby
+ class Slash < Terminal # :nodoc:
+ def type; :SLASH; end
+ end
+
+ class Dot < Terminal # :nodoc:
+ def type; :DOT; end
end
class Symbol < Terminal # :nodoc:
@@ -80,13 +82,14 @@ module ActionDispatch
def initialize(left)
super
@regexp = DEFAULT_EXP
- @name = left.tr '*:'.freeze, ''.freeze
+ @name = -left.tr("*:", "")
end
def default_regexp?
regexp == DEFAULT_EXP
end
+ def type; :SYMBOL; end
def symbol?; true; end
end
@@ -104,7 +107,7 @@ module ActionDispatch
def type; :STAR; end
def name
- left.name.tr '*:', ''
+ left.name.tr "*:", ""
end
end
diff --git a/actionpack/lib/action_dispatch/journey/parser.rb b/actionpack/lib/action_dispatch/journey/parser.rb
index 9012297400..e002755bcf 100644
--- a/actionpack/lib/action_dispatch/journey/parser.rb
+++ b/actionpack/lib/action_dispatch/journey/parser.rb
@@ -1,32 +1,33 @@
#
# DO NOT MODIFY!!!!
-# This file is automatically generated by Racc 1.4.11
-# from Racc grammer file "".
+# This file is automatically generated by Racc 1.4.14
+# from Racc grammar file "".
#
require 'racc/parser.rb'
+# :stopdoc:
-require 'action_dispatch/journey/parser_extras'
+require "action_dispatch/journey/parser_extras"
module ActionDispatch
module Journey
class Parser < Racc::Parser
##### State transition tables begin ###
racc_action_table = [
- 13, 15, 14, 7, 21, 16, 8, 19, 13, 15,
- 14, 7, 17, 16, 8, 13, 15, 14, 7, 24,
- 16, 8, 13, 15, 14, 7, 19, 16, 8 ]
+ 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 ]
racc_action_check = [
- 2, 2, 2, 2, 17, 2, 2, 2, 0, 0,
- 0, 0, 1, 0, 0, 19, 19, 19, 19, 20,
- 19, 19, 7, 7, 7, 7, 22, 7, 7 ]
+ 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 ]
racc_action_pointer = [
- 6, 12, -2, nil, nil, nil, nil, 20, nil, nil,
- nil, nil, nil, nil, nil, nil, nil, 4, nil, 13,
- 13, nil, 17, nil, nil ]
+ 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 ]
racc_action_default = [
-19, -19, -2, -3, -4, -5, -6, -19, -10, -11,
@@ -134,11 +135,11 @@ Racc_debug_parser = false
# reduce 0 omitted
def _reduce_1(val, _values)
- Cat.new(val.first, val.last)
+ Cat.new(val.first, val.last)
end
def _reduce_2(val, _values)
- val.first
+ val.first
end
# reduce 3 omitted
@@ -150,19 +151,19 @@ end
# reduce 6 omitted
def _reduce_7(val, _values)
- Group.new(val[1])
+ Group.new(val[1])
end
def _reduce_8(val, _values)
- Or.new([val.first, val.last])
+ Or.new([val.first, val.last])
end
def _reduce_9(val, _values)
- Or.new([val.first, val.last])
+ Or.new([val.first, val.last])
end
def _reduce_10(val, _values)
- Star.new(Symbol.new(val.last))
+ Star.new(Symbol.new(val.last))
end
# reduce 11 omitted
@@ -174,19 +175,19 @@ end
# reduce 14 omitted
def _reduce_15(val, _values)
- Slash.new('/')
+ Slash.new(val.first)
end
def _reduce_16(val, _values)
- Symbol.new(val.first)
+ Symbol.new(val.first)
end
def _reduce_17(val, _values)
- Literal.new(val.first)
+ Literal.new(val.first)
end
def _reduce_18(val, _values)
- Dot.new(val.first)
+ Dot.new(val.first)
end
def _reduce_none(val, _values)
diff --git a/actionpack/lib/action_dispatch/journey/parser.y b/actionpack/lib/action_dispatch/journey/parser.y
index d3f7c4d765..f9b1a7a958 100644
--- a/actionpack/lib/action_dispatch/journey/parser.y
+++ b/actionpack/lib/action_dispatch/journey/parser.y
@@ -30,7 +30,7 @@ rule
| dot
;
slash
- : SLASH { Slash.new('/') }
+ : SLASH { Slash.new(val.first) }
;
symbol
: SYMBOL { Symbol.new(val.first) }
@@ -45,5 +45,6 @@ rule
end
---- header
+# :stopdoc:
-require 'action_dispatch/journey/parser_extras'
+require "action_dispatch/journey/parser_extras"
diff --git a/actionpack/lib/action_dispatch/journey/parser_extras.rb b/actionpack/lib/action_dispatch/journey/parser_extras.rb
index fff0299812..18ec6c9b9b 100644
--- a/actionpack/lib/action_dispatch/journey/parser_extras.rb
+++ b/actionpack/lib/action_dispatch/journey/parser_extras.rb
@@ -1,9 +1,12 @@
-require 'action_dispatch/journey/scanner'
-require 'action_dispatch/journey/nodes/node'
+# frozen_string_literal: true
+
+require "action_dispatch/journey/scanner"
+require "action_dispatch/journey/nodes/node"
module ActionDispatch
- module Journey # :nodoc:
- class Parser < Racc::Parser # :nodoc:
+ # :stopdoc:
+ module Journey
+ class Parser < Racc::Parser
include Journey::Nodes
def self.parse(string)
@@ -24,4 +27,5 @@ module ActionDispatch
end
end
end
+ # :startdoc:
end
diff --git a/actionpack/lib/action_dispatch/journey/path/pattern.rb b/actionpack/lib/action_dispatch/journey/path/pattern.rb
index 018b89a2b7..697f5b9d8b 100644
--- a/actionpack/lib/action_dispatch/journey/path/pattern.rb
+++ b/actionpack/lib/action_dispatch/journey/path/pattern.rb
@@ -1,10 +1,12 @@
+# frozen_string_literal: true
+
module ActionDispatch
module Journey # :nodoc:
module Path # :nodoc:
class Pattern # :nodoc:
attr_reader :spec, :requirements, :anchored
- def self.from_string string
+ def self.from_string(string)
build(string, {}, "/.?", true)
end
@@ -31,6 +33,13 @@ module ActionDispatch
Visitors::FormatBuilder.new.accept(spec)
end
+ def eager_load!
+ required_names
+ offsets
+ to_regexp
+ nil
+ end
+
def ast
@spec.find_all(&:symbol?).each do |node|
re = @requirements[node.to_sym]
@@ -81,7 +90,7 @@ module ActionDispatch
return @separator_re unless @matchers.key?(node)
re = @matchers[node]
- "(#{re})"
+ "(#{Regexp.union(re)})"
end
def visit_GROUP(node)
@@ -98,7 +107,7 @@ module ActionDispatch
end
def visit_STAR(node)
- re = @matchers[node.left.to_sym] || '.+'
+ re = @matchers[node.left.to_sym] || ".+"
"(#{re})"
end
@@ -110,7 +119,7 @@ module ActionDispatch
class UnanchoredRegexp < AnchoredRegexp # :nodoc:
def accept(node)
- %r{\A#{visit node}}
+ %r{\A#{visit node}(?:\b|\Z)}
end
end
@@ -174,8 +183,8 @@ module ActionDispatch
node = node.to_sym
if @requirements.key?(node)
- re = /#{@requirements[node]}|/
- @offsets.push((re.match('').length - 1) + @offsets.last)
+ re = /#{Regexp.union(@requirements[node])}|/
+ @offsets.push((re.match("").length - 1) + @offsets.last)
else
@offsets << @offsets.last
end
diff --git a/actionpack/lib/action_dispatch/journey/route.rb b/actionpack/lib/action_dispatch/journey/route.rb
index cfd6681dd1..8165709a3d 100644
--- a/actionpack/lib/action_dispatch/journey/route.rb
+++ b/actionpack/lib/action_dispatch/journey/route.rb
@@ -1,6 +1,9 @@
+# frozen_string_literal: true
+
module ActionDispatch
- module Journey # :nodoc:
- class Route # :nodoc:
+ # :stopdoc:
+ module Journey
+ class Route
attr_reader :app, :path, :defaults, :name, :precedence
attr_reader :constraints, :internal
@@ -9,11 +12,11 @@ module ActionDispatch
module VerbMatchers
VERBS = %w{ DELETE GET HEAD OPTIONS LINK PATCH POST PUT TRACE UNLINK }
VERBS.each do |v|
- class_eval <<-eoc
- class #{v}
- def self.verb; name.split("::").last; end
- def self.call(req); req.#{v.downcase}?; end
- end
+ class_eval <<-eoc, __FILE__, __LINE__ + 1
+ class #{v}
+ def self.verb; name.split("::").last; end
+ def self.call(req); req.#{v.downcase}?; end
+ end
eoc
end
@@ -29,16 +32,15 @@ module ActionDispatch
class All
def self.call(_); true; end
- def self.verb; ''; end
+ def self.verb; ""; end
end
- VERB_TO_CLASS = VERBS.each_with_object({ :all => All }) do |verb, hash|
+ VERB_TO_CLASS = VERBS.each_with_object(all: All) do |verb, hash|
klass = const_get verb
hash[verb] = klass
hash[verb.downcase] = klass
hash[verb.downcase.to_sym] = klass
end
-
end
def self.verb_matcher(verb)
@@ -73,6 +75,14 @@ module ActionDispatch
@internal = internal
end
+ def eager_load!
+ path.eager_load!
+ ast
+ parts
+ required_defaults
+ nil
+ end
+
def ast
@decorated_ast ||= begin
decorated_ast = path.ast
@@ -81,9 +91,16 @@ module ActionDispatch
end
end
- def requirements # :nodoc:
- # needed for rails `rails routes`
- @defaults.merge(path.requirements).delete_if { |_,v|
+ # Needed for `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.
+ def requirements
+ @defaults.merge(path.requirements).delete_if { |_, v|
/.+?/ == v
}
end
@@ -96,13 +113,18 @@ module ActionDispatch
required_parts + required_defaults.keys
end
- def score(constraints)
+ def score(supplied_keys)
required_keys = path.required_names
- supplied_keys = constraints.map { |k,v| v && k.to_s }.compact
- return -1 unless (required_keys - supplied_keys).empty?
+ required_keys.each do |k|
+ return -1 unless supplied_keys.include?(k)
+ end
+
+ score = 0
+ path.names.each do |k|
+ score += 1 if supplied_keys.include?(k)
+ end
- score = (supplied_keys & path.names).length
score + (required_defaults.length * 2)
end
@@ -124,7 +146,7 @@ module ActionDispatch
end
def required_defaults
- @required_defaults ||= @defaults.dup.delete_if do |k,_|
+ @required_defaults ||= @defaults.dup.delete_if do |k, _|
parts.include?(k) || !required_default?(k)
end
end
@@ -164,17 +186,18 @@ module ActionDispatch
end
def verb
- verbs.join('|')
+ verbs.join("|")
end
private
- def verbs
- @request_method_match.map(&:verb)
- end
+ def verbs
+ @request_method_match.map(&:verb)
+ end
- def match_verb(request)
- @request_method_match.any? { |m| m.call request }
- end
+ def match_verb(request)
+ @request_method_match.any? { |m| m.call request }
+ end
end
end
+ # :startdoc:
end
diff --git a/actionpack/lib/action_dispatch/journey/router.rb b/actionpack/lib/action_dispatch/journey/router.rb
index 06cdce1724..89a164f968 100644
--- a/actionpack/lib/action_dispatch/journey/router.rb
+++ b/actionpack/lib/action_dispatch/journey/router.rb
@@ -1,27 +1,33 @@
-require 'action_dispatch/journey/router/utils'
-require 'action_dispatch/journey/routes'
-require 'action_dispatch/journey/formatter'
+# frozen_string_literal: true
+
+require "action_dispatch/journey/router/utils"
+require "action_dispatch/journey/routes"
+require "action_dispatch/journey/formatter"
before = $-w
$-w = false
-require 'action_dispatch/journey/parser'
+require "action_dispatch/journey/parser"
$-w = before
-require 'action_dispatch/journey/route'
-require 'action_dispatch/journey/path/pattern'
+require "action_dispatch/journey/route"
+require "action_dispatch/journey/path/pattern"
module ActionDispatch
module Journey # :nodoc:
class Router # :nodoc:
- class RoutingError < ::StandardError # :nodoc:
- end
-
attr_accessor :routes
def initialize(routes)
@routes = routes
end
+ def eager_load!
+ # Eagerly trigger the simulator's initialization so
+ # it doesn't happen during a request cycle.
+ simulator
+ nil
+ end
+
def serve(req)
find_routes(req).each do |match, parameters, route|
set_params = req.path_parameters
@@ -29,16 +35,20 @@ module ActionDispatch
script_name = req.script_name
unless route.path.anchored
- req.script_name = (script_name.to_s + match.to_s).chomp('/')
+ 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
+ parameters = route.defaults.merge parameters.transform_values { |val|
+ val.dup.force_encoding(::Encoding::UTF_8)
+ }
+
req.path_parameters = set_params.merge parameters
status, headers, body = route.app.serve(req)
- if 'pass' == headers['X-Cascade']
+ if "pass" == headers["X-Cascade"]
req.script_name = script_name
req.path_info = path_info
req.path_parameters = set_params
@@ -48,7 +58,7 @@ module ActionDispatch
return [status, headers, body]
end
- return [404, {'X-Cascade' => 'pass'}, ['Not Found']]
+ [404, { "X-Cascade" => "pass" }, ["Not Found"]]
end
def recognize(rails_req)
@@ -58,6 +68,7 @@ module ActionDispatch
rails_req.path_info = match.post_match.sub(/^([^\/])/, '/\1')
end
+ parameters = route.defaults.merge parameters
yield(route, parameters)
end
end
@@ -72,7 +83,9 @@ module ActionDispatch
private
def partitioned_routes
- routes.partitioned_routes
+ routes.partition { |r|
+ r.path.anchored && r.ast.grep(Nodes::Symbol).all? { |n| n.default_regexp? }
+ }
end
def ast
@@ -92,7 +105,7 @@ module ActionDispatch
simulator.memos(path) { [] }
end
- def find_routes req
+ def find_routes(req)
routes = filter_routes(req.path_info).concat custom_routes.find_all { |r|
r.path.match(req.path_info)
}
@@ -107,9 +120,9 @@ module ActionDispatch
routes.sort_by!(&:precedence)
routes.map! { |r|
- match_data = r.path.match(req.path_info)
- path_parameters = r.defaults.dup
- match_data.names.zip(match_data.captures) { |name,val|
+ match_data = r.path.match(req.path_info)
+ path_parameters = {}
+ match_data.names.zip(match_data.captures) { |name, val|
path_parameters[name.to_sym] = Utils.unescape_uri(val) if val
}
[match_data, path_parameters, r]
diff --git a/actionpack/lib/action_dispatch/journey/router/utils.rb b/actionpack/lib/action_dispatch/journey/router/utils.rb
index 9793ca1c7a..3c8b9a6eaa 100644
--- a/actionpack/lib/action_dispatch/journey/router/utils.rb
+++ b/actionpack/lib/action_dispatch/journey/router/utils.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionDispatch
module Journey # :nodoc:
class Router # :nodoc:
@@ -5,7 +7,7 @@ module ActionDispatch
# Normalizes URI path.
#
# Strips off trailing slash and ensures there is a leading slash.
- # Also converts downcase url encoded string to uppercase.
+ # Also converts downcase URL encoded string to uppercase.
#
# normalize_path("/foo") # => "/foo"
# normalize_path("/foo/") # => "/foo"
@@ -13,27 +15,30 @@ module ActionDispatch
# normalize_path("") # => "/"
# normalize_path("/%ab") # => "/%AB"
def self.normalize_path(path)
- path = "/#{path}"
- path.squeeze!('/'.freeze)
- path.sub!(%r{/+\Z}, ''.freeze)
+ path ||= ""
+ encoding = path.encoding
+ path = +"/#{path}"
+ path.squeeze!("/")
+ path.sub!(%r{/+\Z}, "")
path.gsub!(/(%[a-f0-9]{2})/) { $1.upcase }
- path = '/' if path == ''.freeze
+ path = +"/" if path == ""
+ path.force_encoding(encoding)
path
end
# URI path and fragment escaping
- # http://tools.ietf.org/html/rfc3986
+ # https://tools.ietf.org/html/rfc3986
class UriEncoder # :nodoc:
- ENCODE = "%%%02X".freeze
+ ENCODE = "%%%02X"
US_ASCII = Encoding::US_ASCII
UTF_8 = Encoding::UTF_8
- EMPTY = "".force_encoding(US_ASCII).freeze
- DEC2HEX = (0..255).to_a.map{ |i| ENCODE % i }.map{ |s| s.force_encoding(US_ASCII) }
+ EMPTY = (+"").force_encoding(US_ASCII).freeze
+ DEC2HEX = (0..255).to_a.map { |i| ENCODE % i }.map { |s| s.force_encoding(US_ASCII) }
- ALPHA = "a-zA-Z".freeze
- DIGIT = "0-9".freeze
- UNRESERVED = "#{ALPHA}#{DIGIT}\\-\\._~".freeze
- SUB_DELIMS = "!\\$&'\\(\\)\\*\\+,;=".freeze
+ ALPHA = "a-zA-Z"
+ DIGIT = "0-9"
+ UNRESERVED = "#{ALPHA}#{DIGIT}\\-\\._~"
+ SUB_DELIMS = "!\\$&'\\(\\)\\*\\+,;="
ESCAPED = /%[a-zA-Z0-9]{2}/.freeze
@@ -55,12 +60,12 @@ module ActionDispatch
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)
+ uri.gsub(ESCAPED) { |match| [match[1, 2].hex].pack("C") }.force_encoding(encoding)
end
- protected
+ private
def escape(component, pattern)
- component.gsub(pattern){ |unsafe| percent_encode(unsafe) }.force_encoding(US_ASCII)
+ component.gsub(pattern) { |unsafe| percent_encode(unsafe) }.force_encoding(US_ASCII)
end
def percent_encode(unsafe)
@@ -84,6 +89,10 @@ module ActionDispatch
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"
def self.unescape_uri(uri)
ENCODER.unescape_uri(uri)
end
diff --git a/actionpack/lib/action_dispatch/journey/routes.rb b/actionpack/lib/action_dispatch/journey/routes.rb
index f7b009109e..c0377459d5 100644
--- a/actionpack/lib/action_dispatch/journey/routes.rb
+++ b/actionpack/lib/action_dispatch/journey/routes.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionDispatch
module Journey # :nodoc:
# The Routing table. Contains all routes for a system. Routes can be
@@ -49,11 +51,12 @@ module ActionDispatch
def ast
@ast ||= begin
asts = anchored_routes.map(&:ast)
- Nodes::Or.new(asts) unless asts.empty?
+ Nodes::Or.new(asts)
end
end
def simulator
+ return if ast.nil?
@simulator ||= begin
gtg = GTG::Builder.new(ast).transition_table
GTG::Simulator.new(gtg)
diff --git a/actionpack/lib/action_dispatch/journey/scanner.rb b/actionpack/lib/action_dispatch/journey/scanner.rb
index 19e0bc03d6..2a075862e9 100644
--- a/actionpack/lib/action_dispatch/journey/scanner.rb
+++ b/actionpack/lib/action_dispatch/journey/scanner.rb
@@ -1,4 +1,6 @@
-require 'strscan'
+# frozen_string_literal: true
+
+require "strscan"
module ActionDispatch
module Journey # :nodoc:
@@ -32,27 +34,35 @@ module ActionDispatch
private
+ # takes advantage of String @- deduping capabilities in Ruby 2.5 upwards
+ # see: https://bugs.ruby-lang.org/issues/13077
+ def dedup_scan(regex)
+ r = @ss.scan(regex)
+ r ? -r : nil
+ end
+
def scan
case
# /
- when text = @ss.scan(/\//)
- [:SLASH, text]
- when text = @ss.scan(/\*\w+/)
- [:STAR, text]
- when text = @ss.scan(/(?<!\\)\(/)
- [:LPAREN, text]
- when text = @ss.scan(/(?<!\\)\)/)
- [:RPAREN, text]
- when text = @ss.scan(/\|/)
- [:OR, text]
- when text = @ss.scan(/\./)
- [:DOT, text]
- when text = @ss.scan(/(?<!\\):\w+/)
+ when @ss.skip(/\//)
+ [:SLASH, "/"]
+ when @ss.skip(/\(/)
+ [:LPAREN, "("]
+ when @ss.skip(/\)/)
+ [:RPAREN, ")"]
+ when @ss.skip(/\|/)
+ [:OR, "|"]
+ when @ss.skip(/\./)
+ [:DOT, "."]
+ when text = dedup_scan(/:\w+/)
[:SYMBOL, text]
- when text = @ss.scan(/(?:[\w%\-~!$&'*+,;=@]|\\:|\\\(|\\\))+/)
- [:LITERAL, text.tr('\\', '')]
+ when text = dedup_scan(/\*\w+/)
+ [:STAR, text]
+ when text = @ss.scan(/(?:[\w%\-~!$&'*+,;=@]|\\[:()])+/)
+ text.tr! "\\", ""
+ [:LITERAL, -text]
# any char
- when text = @ss.scan(/./)
+ when text = dedup_scan(/./)
[:LITERAL, text]
end
end
diff --git a/actionpack/lib/action_dispatch/journey/visitors.rb b/actionpack/lib/action_dispatch/journey/visitors.rb
index 306d2e674a..d2619cbf3a 100644
--- a/actionpack/lib/action_dispatch/journey/visitors.rb
+++ b/actionpack/lib/action_dispatch/journey/visitors.rb
@@ -1,10 +1,13 @@
+# frozen_string_literal: true
+
module ActionDispatch
- module Journey # :nodoc:
+ # :stopdoc:
+ module Journey
class Format
ESCAPE_PATH = ->(value) { Router::Utils.escape_path(value) }
ESCAPE_SEGMENT = ->(value) { Router::Utils.escape_segment(value) }
- class Parameter < Struct.new(:name, :escaper)
+ Parameter = Struct.new(:name, :escaper) do
def escape(value); escaper.call value; end
end
@@ -21,7 +24,7 @@ module ActionDispatch
@children = []
@parameters = []
- parts.each_with_index do |object,i|
+ parts.each_with_index do |object, i|
case object
when Journey::Format
@children << i
@@ -37,7 +40,7 @@ module ActionDispatch
@parameters.each do |index|
param = parts[index]
value = hash[param.name]
- return ''.freeze unless value
+ return "" unless value
parts[index] = param.escape value
end
@@ -57,7 +60,7 @@ module ActionDispatch
private
- def visit node
+ def visit(node)
send(DISPATCH_CACHE[node.type], node)
end
@@ -97,7 +100,7 @@ module ActionDispatch
visit(node, seed)
end
- def visit node, seed
+ def visit(node, seed)
send(DISPATCH_CACHE[node.type], node, seed)
end
@@ -153,7 +156,7 @@ module ActionDispatch
end
end
- # Loop through the requirements AST
+ # Loop through the requirements AST.
class Each < FunctionalVisitor # :nodoc:
def visit(node, block)
block.call(node)
@@ -166,28 +169,28 @@ module ActionDispatch
class String < FunctionalVisitor # :nodoc:
private
- def binary(node, seed)
- visit(node.right, visit(node.left, seed))
- end
+ def binary(node, seed)
+ visit(node.right, visit(node.left, seed))
+ end
- def nary(node, seed)
- last_child = node.children.last
- node.children.inject(seed) { |s, c|
- string = visit(c, s)
- string << "|".freeze unless last_child == c
- string
- }
- end
+ 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
- def terminal(node, seed)
- seed + node.left
- end
+ def terminal(node, seed)
+ seed + node.left
+ end
- def visit_GROUP(node, seed)
- visit(node.left, seed << "(".freeze) << ")".freeze
- end
+ def visit_GROUP(node, seed)
+ visit(node.left, seed.dup << "(") << ")"
+ end
- INSTANCE = new
+ INSTANCE = new
end
class Dot < FunctionalVisitor # :nodoc:
@@ -261,4 +264,5 @@ module ActionDispatch
end
end
end
+ # :startdoc:
end
diff --git a/actionpack/lib/action_dispatch/middleware/callbacks.rb b/actionpack/lib/action_dispatch/middleware/callbacks.rb
index c782779b34..87fe19225b 100644
--- a/actionpack/lib/action_dispatch/middleware/callbacks.rb
+++ b/actionpack/lib/action_dispatch/middleware/callbacks.rb
@@ -1,3 +1,4 @@
+# frozen_string_literal: true
module ActionDispatch
# Provides callbacks to be executed before and after dispatching the request.
@@ -7,17 +8,6 @@ module ActionDispatch
define_callbacks :call
class << self
- def to_prepare(*args, &block)
- ActiveSupport::Reloader.to_prepare(*args, &block)
- end
-
- def to_cleanup(*args, &block)
- ActiveSupport::Reloader.to_complete(*args, &block)
- end
-
- deprecate to_prepare: 'use ActiveSupport::Reloader.to_prepare instead',
- to_cleanup: 'use ActiveSupport::Reloader.to_complete instead'
-
def before(*args, &block)
set_callback(:call, :before, *args, &block)
end
@@ -34,10 +24,8 @@ module ActionDispatch
def call(env)
error = nil
result = run_callbacks :call do
- begin
- @app.call(env)
- rescue => error
- end
+ @app.call(env)
+ rescue => error
end
raise error if error
result
diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb
index f2f3150b56..b69bcab05c 100644
--- a/actionpack/lib/action_dispatch/middleware/cookies.rb
+++ b/actionpack/lib/action_dispatch/middleware/cookies.rb
@@ -1,13 +1,15 @@
-require 'active_support/core_ext/hash/keys'
-require 'active_support/key_generator'
-require 'active_support/message_verifier'
-require 'active_support/json'
-require 'rack/utils'
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/keys"
+require "active_support/key_generator"
+require "active_support/message_verifier"
+require "active_support/json"
+require "rack/utils"
module ActionDispatch
class Request
def cookie_jar
- fetch_header('action_dispatch.cookies'.freeze) do
+ fetch_header("action_dispatch.cookies") do
self.cookie_jar = Cookies::CookieJar.build(self, cookies)
end
end
@@ -20,11 +22,11 @@ module ActionDispatch
}
def have_cookie_jar?
- has_header? 'action_dispatch.cookies'.freeze
+ has_header? "action_dispatch.cookies"
end
def cookie_jar=(jar)
- set_header 'action_dispatch.cookies'.freeze, jar
+ set_header "action_dispatch.cookies", jar
end
def key_generator
@@ -43,8 +45,20 @@ module ActionDispatch
get_header Cookies::ENCRYPTED_SIGNED_COOKIE_SALT
end
- def secret_token
- get_header Cookies::SECRET_TOKEN
+ def authenticated_encrypted_cookie_salt
+ get_header Cookies::AUTHENTICATED_ENCRYPTED_COOKIE_SALT
+ end
+
+ def use_authenticated_cookie_encryption
+ get_header Cookies::USE_AUTHENTICATED_COOKIE_ENCRYPTION
+ end
+
+ def encrypted_cookie_cipher
+ get_header Cookies::ENCRYPTED_COOKIE_CIPHER
+ end
+
+ def signed_cookie_digest
+ get_header Cookies::SIGNED_COOKIE_DIGEST
end
def secret_key_base
@@ -58,6 +72,15 @@ module ActionDispatch
def cookies_digest
get_header Cookies::COOKIES_DIGEST
end
+
+ def cookies_rotations
+ get_header Cookies::COOKIES_ROTATIONS
+ end
+
+ def use_cookies_with_metadata
+ get_header Cookies::USE_COOKIES_WITH_METADATA
+ end
+
# :startdoc:
end
@@ -77,16 +100,17 @@ module ActionDispatch
# 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.from_now }
+ # 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.
- # # The cookie is signed by your app's `secrets.secret_key_base` 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.
- # # The cookie is signed by your app's `secrets.secret_key_base` value.
# # It can be read using the encrypted method `cookies.encrypted[:name]`
# cookies.encrypted[:discount] = 45
#
@@ -94,7 +118,7 @@ module ActionDispatch
# cookies.permanent[:login] = "XJ-122"
#
# # You can also chain these methods:
- # cookies.permanent.signed[:login] = "XJ-122"
+ # cookies.signed.permanent[:login] = "XJ-122"
#
# Examples of reading:
#
@@ -112,7 +136,7 @@ module ActionDispatch
#
# cookies[:name] = {
# value: 'a yummy cookie',
- # expires: 1.year.from_now,
+ # expires: 1.year,
# domain: 'domain.com'
# }
#
@@ -137,22 +161,27 @@ module ActionDispatch
#
# * <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 1.
- # * <tt>:expires</tt> - The time at which this cookie expires, as a \Time object.
+ # 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+.
class Cookies
- HTTP_HEADER = "Set-Cookie".freeze
- GENERATOR_KEY = "action_dispatch.key_generator".freeze
- SIGNED_COOKIE_SALT = "action_dispatch.signed_cookie_salt".freeze
- ENCRYPTED_COOKIE_SALT = "action_dispatch.encrypted_cookie_salt".freeze
- ENCRYPTED_SIGNED_COOKIE_SALT = "action_dispatch.encrypted_signed_cookie_salt".freeze
- SECRET_TOKEN = "action_dispatch.secret_token".freeze
- SECRET_KEY_BASE = "action_dispatch.secret_key_base".freeze
- COOKIES_SERIALIZER = "action_dispatch.cookies_serializer".freeze
- COOKIES_DIGEST = "action_dispatch.cookies_digest".freeze
+ HTTP_HEADER = "Set-Cookie"
+ GENERATOR_KEY = "action_dispatch.key_generator"
+ SIGNED_COOKIE_SALT = "action_dispatch.signed_cookie_salt"
+ ENCRYPTED_COOKIE_SALT = "action_dispatch.encrypted_cookie_salt"
+ ENCRYPTED_SIGNED_COOKIE_SALT = "action_dispatch.encrypted_signed_cookie_salt"
+ AUTHENTICATED_ENCRYPTED_COOKIE_SALT = "action_dispatch.authenticated_encrypted_cookie_salt"
+ USE_AUTHENTICATED_COOKIE_ENCRYPTION = "action_dispatch.use_authenticated_cookie_encryption"
+ ENCRYPTED_COOKIE_CIPHER = "action_dispatch.encrypted_cookie_cipher"
+ SIGNED_COOKIE_DIGEST = "action_dispatch.signed_cookie_digest"
+ SECRET_KEY_BASE = "action_dispatch.secret_key_base"
+ COOKIES_SERIALIZER = "action_dispatch.cookies_serializer"
+ COOKIES_DIGEST = "action_dispatch.cookies_digest"
+ COOKIES_ROTATIONS = "action_dispatch.cookies_rotations"
+ USE_COOKIES_WITH_METADATA = "action_dispatch.use_cookies_with_metadata"
# Cookies can typically store 4096 bytes.
MAX_COOKIE_SIZE = 4096
@@ -160,7 +189,7 @@ module ActionDispatch
# Raised when storing more than 4K of session data.
CookieOverflow = Class.new StandardError
- # Include in a cookie jar to allow chaining, e.g. cookies.permanent.signed
+ # Include in a cookie jar to allow chaining, e.g. cookies.permanent.signed.
module ChainedCookieJars
# Returns a jar that'll automatically set the assigned cookies to have an expiration date 20 years from now. Example:
#
@@ -179,12 +208,9 @@ module ActionDispatch
# 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.
- #
- # If +secrets.secret_key_base+ and +secrets.secret_token+ (deprecated) are both set,
- # legacy cookies signed with the old key generator will be transparently upgraded.
+ # 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 +secrets.secret_key_base+.
+ # This jar requires that you set a suitable secret for the verification on your app's +secret_key_base+.
#
# Example:
#
@@ -193,35 +219,25 @@ module ActionDispatch
#
# cookies.signed[:discount] # => 45
def signed
- @signed ||=
- if upgrade_legacy_signed_cookies?
- UpgradeLegacySignedCookieJar.new(self)
- else
- SignedCookieJar.new(self)
- end
+ @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 the cookie was tampered with by the user (or a 3rd party), +nil+ will be returned.
#
- # If +secrets.secret_key_base+ and +secrets.secret_token+ (deprecated) are both set,
- # legacy cookies signed with the old key generator will be transparently upgraded.
+ # 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 +secrets.secret_key_base+.
+ # 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=ZS9ZZ1R4cG1pcUJ1bm80anhQang3dz09LS1mbDZDSU5scGdOT3ltQ2dTdlhSdWpRPT0%3D--ab54663c9f4e3bc340c790d6d2b71e92f5b60315; path=/
+ # # => Set-Cookie: discount=DIQ7fw==--K3n//8vvnSbGq9dA--7Xh91HfLpwzbj1czhBiwOg==; path=/
#
# cookies.encrypted[:discount] # => 45
def encrypted
- @encrypted ||=
- if upgrade_legacy_signed_cookies?
- UpgradeLegacyEncryptedCookieJar.new(self)
- else
- EncryptedCookieJar.new(self)
- end
+ @encrypted ||= EncryptedKeyRotatingCookieJar.new(self)
end
# Returns the +signed+ or +encrypted+ jar, preferring +encrypted+ if +secret_key_base+ is set.
@@ -237,32 +253,19 @@ module ActionDispatch
private
- def upgrade_legacy_signed_cookies?
- request.secret_token.present? && request.secret_key_base.present?
- end
- end
-
- # Passing the ActiveSupport::MessageEncryptor::NullSerializer downstream
- # to the Message{Encryptor,Verifier} allows us to handle the
- # (de)serialization step within the cookie jar, which gives us the
- # opportunity to detect and migrate legacy cookies.
- module VerifyAndUpgradeLegacySignedMessage # :nodoc:
- def initialize(*args)
- super
- @legacy_verifier = ActiveSupport::MessageVerifier.new(request.secret_token, serializer: ActiveSupport::MessageEncryptor::NullSerializer)
- end
+ 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
- def verify_and_upgrade_legacy_signed_message(name, signed_message)
- deserialize(name, @legacy_verifier.verify(signed_message)).tap do |value|
- self[name] = { value: value }
+ def encrypted_cookie_cipher
+ request.encrypted_cookie_cipher || "aes-256-gcm"
end
- rescue ActiveSupport::MessageVerifier::InvalidSignature
- nil
- end
- private
- def parse(name, signed_message)
- super || verify_and_upgrade_legacy_signed_message(name, signed_message)
+ def signed_cookie_digest
+ request.signed_cookie_digest || "SHA1"
end
end
@@ -325,6 +328,9 @@ module ActionDispatch
end
alias :has_key? :key?
+ # Returns the cookies as Hash.
+ alias :to_hash :to_h
+
def update(other_hash)
@cookies.update other_hash.stringify_keys
self
@@ -332,30 +338,34 @@ module ActionDispatch
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 = request_jar.reject { |k, _| @delete_cookies.key?(k) || @set_cookies.key?(k) }
@cookies.update set_cookies if set_cookies
end
def to_header
- @cookies.map { |k,v| "#{escape(k)}=#{escape(v)}" }.join '; '
+ @cookies.map { |k, v| "#{escape(k)}=#{escape(v)}" }.join "; "
end
- def handle_options(options) #:nodoc:
+ def handle_options(options) # :nodoc:
+ if options[:expires].respond_to?(:from_now)
+ options[:expires] = options[:expires].from_now
+ end
+
options[:path] ||= "/"
- if options[:domain] == :all || options[:domain] == 'all'
- # if there is a provided tld length then we use it otherwise default domain regexp
+ 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
+ # 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 !~ /^[\d.]+$/) && (request.host =~ domain_regexp)
".#{$&}"
end
elsif options[:domain].is_a? Array
- # if host matches one of the supplied domains without a dot in front of it
- options[:domain] = options[:domain].find {|domain| request.host.include? domain.sub(/^\./, '') }
+ # If host matches one of the supplied domains without a dot in front of it.
+ options[:domain] = options[:domain].find { |domain| request.host.include? domain.sub(/^\./, "") }
end
end
@@ -367,12 +377,12 @@ module ActionDispatch
value = options[:value]
else
value = options
- options = { :value => value }
+ options = { value: value }
end
handle_options(options)
- if @cookies[name.to_s] != value or options[:expires]
+ 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)
@@ -404,9 +414,9 @@ module ActionDispatch
@delete_cookies[name.to_s] == options
end
- # Removes all cookies on the client machine by calling <tt>delete</tt> for each cookie
+ # Removes all cookies on the client machine by calling <tt>delete</tt> for each cookie.
def clear(options = {})
- @cookies.each_key{ |k| delete(k, options) }
+ @cookies.each_key { |k| delete(k, options) }
end
def write(headers)
@@ -415,31 +425,30 @@ module ActionDispatch
end
end
- mattr_accessor :always_write_cookie
- self.always_write_cookie = false
+ mattr_accessor :always_write_cookie, default: false
private
- def escape(string)
- ::Rack::Utils.escape(string)
- end
+ def escape(string)
+ ::Rack::Utils.escape(string)
+ end
- 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
+ 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
- def write_cookie?(cookie)
- request.ssl? || !cookie[:secure] || always_write_cookie
- end
+ def write_cookie?(cookie)
+ request.ssl? || !cookie[:secure] || always_write_cookie
+ end
end
class AbstractCookieJar # :nodoc:
@@ -451,7 +460,7 @@ module ActionDispatch
def [](name)
if data = @parent_jar[name.to_s]
- parse name, data
+ parse(name, data, purpose: "cookie.#{name}") || parse(name, data)
end
end
@@ -462,7 +471,7 @@ module ActionDispatch
options = { value: options }
end
- commit(options)
+ commit(name, options)
@parent_jar[name] = options
end
@@ -470,13 +479,27 @@ module ActionDispatch
def request; @parent_jar.request; end
private
- def parse(name, data); data; end
- def commit(options); end
+ def expiry_options(options)
+ if options[:expires].respond_to?(:from_now)
+ { expires_in: options[:expires] }
+ else
+ { expires_at: options[:expires] }
+ end
+ end
+
+ def cookie_metadata(name, options)
+ expiry_options(options).tap do |metadata|
+ metadata[:purpose] = "cookie.#{name}" if request.use_cookies_with_metadata
+ end
+ end
+
+ def parse(name, data, purpose: nil); data; end
+ def commit(name, options); end
end
class PermanentCookieJar < AbstractCookieJar # :nodoc:
private
- def commit(options)
+ def commit(name, options)
options[:expires] = 20.years.from_now
end
end
@@ -492,7 +515,8 @@ module ActionDispatch
end
module SerializedCookieJars # :nodoc:
- MARSHAL_SIGNATURE = "\x04\x08".freeze
+ MARSHAL_SIGNATURE = "\x04\x08"
+ SERIALIZER = ActiveSupport::MessageEncryptor::NullSerializer
protected
def needs_migration?(value)
@@ -503,12 +527,16 @@ module ActionDispatch
serializer.dump(value)
end
- def deserialize(name, value)
+ def deserialize(name)
+ rotate = false
+ value = yield -> { rotate = true }
+
if value
- if needs_migration?(value)
- Marshal.load(value).tap do |v|
- self[name] = { value: v }
- end
+ case
+ when needs_migration?(value)
+ self[name] = Marshal.load(value)
+ when rotate
+ self[name] = serializer.load(value)
else
serializer.load(value)
end
@@ -528,81 +556,84 @@ module ActionDispatch
end
def digest
- request.cookies_digest || 'SHA1'
- end
-
- def key_generator
- request.key_generator
+ request.cookies_digest || "SHA1"
end
end
- class SignedCookieJar < AbstractCookieJar # :nodoc:
+ class SignedKeyRotatingCookieJar < AbstractCookieJar # :nodoc:
include SerializedCookieJars
def initialize(parent_jar)
super
- secret = key_generator.generate_key(request.signed_cookie_salt)
- @verifier = ActiveSupport::MessageVerifier.new(secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer)
+
+ 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|
+ @verifier.rotate(*secrets, serializer: SERIALIZER, **options)
+ end
end
private
- def parse(name, signed_message)
- deserialize name, @verifier.verified(signed_message)
+ def parse(name, signed_message, purpose: nil)
+ deserialize(name) do |rotate|
+ @verifier.verified(signed_message, on_rotation: rotate, purpose: purpose)
+ end
end
- def commit(options)
- options[:value] = @verifier.generate(serialize(options[:value]))
+ 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
- # UpgradeLegacySignedCookieJar is used instead of SignedCookieJar if
- # secrets.secret_token and secrets.secret_key_base are both set. It reads
- # legacy cookies signed with the old dummy key generator and signs and
- # re-saves them using the new key generator to provide a smooth upgrade path.
- class UpgradeLegacySignedCookieJar < SignedCookieJar #:nodoc:
- include VerifyAndUpgradeLegacySignedMessage
- end
-
- class EncryptedCookieJar < AbstractCookieJar # :nodoc:
+ class EncryptedKeyRotatingCookieJar < AbstractCookieJar # :nodoc:
include SerializedCookieJars
def initialize(parent_jar)
super
- if ActiveSupport::LegacyKeyGenerator === key_generator
- raise "You didn't set secrets.secret_key_base, which is required for this cookie jar. " +
- "Read the upgrade documentation to learn more about this new config option."
+ 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
- secret = key_generator.generate_key(request.encrypted_cookie_salt || '')
- sign_secret = key_generator.generate_key(request.encrypted_signed_cookie_salt || '')
- @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer)
+ request.cookies_rotations.encrypted.each do |*secrets, **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)
+ end
end
private
- def parse(name, encrypted_message)
- deserialize name, @encryptor.decrypt_and_verify(encrypted_message)
- rescue ActiveSupport::MessageVerifier::InvalidSignature, ActiveSupport::MessageEncryptor::InvalidMessage
+ 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
- def commit(options)
- options[:value] = @encryptor.encrypt_and_sign(serialize(options[:value]))
+ 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
- # UpgradeLegacyEncryptedCookieJar is used by ActionDispatch::Session::CookieStore
- # instead of EncryptedCookieJar if secrets.secret_token and secrets.secret_key_base
- # are both set. It reads legacy cookies signed with the old dummy key generator and
- # encrypts and re-saves them using the new key generator to provide a smooth upgrade path.
- class UpgradeLegacyEncryptedCookieJar < EncryptedCookieJar #:nodoc:
- include VerifyAndUpgradeLegacySignedMessage
- end
-
def initialize(app)
@app = app
end
diff --git a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
index 51a471fb23..59113e13f4 100644
--- a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
+++ b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
@@ -1,185 +1,183 @@
-require 'action_dispatch/http/request'
-require 'action_dispatch/middleware/exception_wrapper'
-require 'action_dispatch/routing/inspector'
-require 'action_view'
-require 'action_view/base'
+# frozen_string_literal: true
-require 'pp'
+require "action_dispatch/http/request"
+require "action_dispatch/middleware/exception_wrapper"
+require "action_dispatch/routing/inspector"
+
+require "action_view"
+require "action_view/base"
module ActionDispatch
# This middleware is responsible for logging exceptions and
# showing a debugging page in case the request is local.
class DebugExceptions
- RESCUES_TEMPLATE_PATH = File.expand_path('../templates', __FILE__)
-
- class DebugView < ActionView::Base
- 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
+ cattr_reader :interceptors, instance_accessor: false, default: []
- 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 self.register_interceptor(object = nil, &block)
+ interceptor = object || block
+ interceptors << interceptor
end
- def initialize(app, routes_app = nil, response_format = :default)
+ def initialize(app, routes_app = nil, response_format = :default, interceptors = self.class.interceptors)
@app = app
@routes_app = routes_app
@response_format = response_format
+ @interceptors = interceptors
end
def call(env)
request = ActionDispatch::Request.new env
_, headers, body = response = @app.call(env)
- if headers['X-Cascade'] == 'pass'
+ 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
private
- 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')
- case @response_format
- when :api
- render_for_api_application(request, wrapper)
- when :default
- render_for_default_application(request, wrapper)
+ 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
- else
- raise exception
end
- end
- def render_for_default_application(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"
+ 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
+ render_for_api_request(Mime[:text], wrapper)
+ 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
- render(wrapper.status_code, body, format)
- end
- def render_for_api_application(request, 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
- }
-
- content_type = request.formats.first
- 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]
+ 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
- render(wrapper.status_code, formatted_body, format)
- end
+ 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
- def create_template(request, wrapper)
- traces = wrapper.traces
+ render(wrapper.status_code, formatted_body, format)
+ end
- trace_to_show = 'Application Trace'
- if traces[trace_to_show].empty? && wrapper.rescue_template != 'routing_error'
- trace_to_show = 'Full Trace'
+ 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
- if source_to_show = traces[trace_to_show].first
- source_to_show_id = source_to_show[:id]
+ def render(status, body, format)
+ [status, { "Content-Type" => "#{format}; charset=#{Response.default_charset}", "Content-Length" => body.bytesize.to_s }, [body]]
end
- DebugView.new([RESCUES_TEMPLATE_PATH],
- request: request,
- exception: wrapper.exception,
- traces: traces,
- show_source_idx: source_to_show_id,
- trace_to_show: trace_to_show,
- routes_inspector: routes_inspector(wrapper.exception),
- source_extracts: wrapper.source_extracts,
- line_number: wrapper.line_number,
- file: wrapper.file
- )
- end
+ def log_error(request, wrapper)
+ logger = logger(request)
+ return unless logger
- def render(status, body, format)
- [status, {'Content-Type' => "#{format}; charset=#{Response.default_charset}", 'Content-Length' => body.bytesize.to_s}, [body]]
- end
+ exception = wrapper.exception
- def log_error(request, wrapper)
- logger = logger(request)
- return unless logger
+ trace = wrapper.application_trace
+ trace = wrapper.framework_trace if trace.empty?
- exception = wrapper.exception
+ ActiveSupport::Deprecation.silence do
+ message = []
+ message << " "
+ message << "#{exception.class} (#{exception.message}):"
+ message.concat(exception.annotated_source_code) if exception.respond_to?(:annotated_source_code)
+ message << " "
+ message.concat(trace)
- trace = wrapper.application_trace
- trace = wrapper.framework_trace if trace.empty?
+ log_array(logger, message)
+ end
+ end
- ActiveSupport::Deprecation.silence do
- logger.fatal " "
- logger.fatal "#{exception.class} (#{exception.message}):"
- log_array logger, exception.annoted_source_code if exception.respond_to?(:annoted_source_code)
- logger.fatal " "
- log_array logger, trace
+ def log_array(logger, array)
+ if logger.formatter && logger.formatter.respond_to?(:tags_text)
+ logger.fatal array.join("\n#{logger.formatter.tags_text}")
+ else
+ logger.fatal array.join("\n")
+ end
end
- end
- def log_array(logger, array)
- array.map { |line| logger.fatal line }
- end
+ def logger(request)
+ request.logger || ActionView::Base.logger || stderr_logger
+ end
- def logger(request)
- request.logger || ActionView::Base.logger || stderr_logger
- end
+ def stderr_logger
+ @stderr_logger ||= ActiveSupport::Logger.new($stderr)
+ end
- def stderr_logger
- @stderr_logger ||= ActiveSupport::Logger.new($stderr)
- end
+ 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
- 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)
+ def api_request?(content_type)
+ @response_format == :api && !content_type.html?
end
- end
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/debug_locks.rb b/actionpack/lib/action_dispatch/middleware/debug_locks.rb
new file mode 100644
index 0000000000..93c6c85a71
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/debug_locks.rb
@@ -0,0 +1,124 @@
+# 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
diff --git a/actionpack/lib/action_dispatch/middleware/debug_view.rb b/actionpack/lib/action_dispatch/middleware/debug_view.rb
new file mode 100644
index 0000000000..43c0a84504
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/debug_view.rb
@@ -0,0 +1,56 @@
+# 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
+ end
+end
diff --git a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
index 59edc66086..0cc56f5013 100644
--- a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
+++ b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
@@ -1,50 +1,65 @@
-require 'active_support/core_ext/module/attribute_accessors'
-require 'rack/utils'
+# frozen_string_literal: true
+
+require "active_support/core_ext/module/attribute_accessors"
+require "rack/utils"
module ActionDispatch
class ExceptionWrapper
- cattr_accessor :rescue_responses
- @@rescue_responses = Hash.new(:internal_server_error)
- @@rescue_responses.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,
- 'ActionController::InvalidAuthenticityToken' => :unprocessable_entity,
- 'ActionController::InvalidCrossOriginRequest' => :unprocessable_entity,
- 'ActionDispatch::ParamsParser::ParseError' => :bad_request,
- 'ActionController::BadRequest' => :bad_request,
- 'ActionController::ParameterMissing' => :bad_request,
- 'Rack::Utils::ParameterTypeError' => :bad_request,
- 'Rack::Utils::InvalidParameterError' => :bad_request
+ 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
)
- cattr_accessor :rescue_templates
- @@rescue_templates = Hash.new('diagnostics')
- @@rescue_templates.merge!(
- 'ActionView::MissingTemplate' => 'missing_template',
- 'ActionController::RoutingError' => 'routing_error',
- 'AbstractController::ActionNotFound' => 'unknown_action',
- 'ActionView::Template::Error' => 'template_error'
+ 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",
)
- attr_reader :backtrace_cleaner, :exception, :line_number, :file
+ cattr_accessor :wrapper_exceptions, default: [
+ "ActionView::Template::Error"
+ ]
+
+ attr_reader :backtrace_cleaner, :exception, :wrapped_causes, :line_number, :file
def initialize(backtrace_cleaner, exception)
@backtrace_cleaner = backtrace_cleaner
- @exception = original_exception(exception)
+ @exception = exception
+ @wrapped_causes = wrapped_causes_for(exception, backtrace_cleaner)
expand_backtrace if exception.is_a?(SyntaxError) || exception.cause.is_a?(SyntaxError)
end
+ def unwrapped_exception
+ if wrapper_exceptions.include?(exception.class.to_s)
+ exception.cause
+ else
+ exception
+ end
+ end
+
def rescue_template
@@rescue_templates[@exception.class.name]
end
def status_code
- self.class.status_code_for_exception(@exception.class.name)
+ self.class.status_code_for_exception(unwrapped_exception.class.name)
end
def application_trace
@@ -65,7 +80,11 @@ module ActionDispatch
full_trace_with_ids = []
full_trace.each_with_index do |trace, idx|
- trace_with_id = { id: idx, trace: trace }
+ 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
@@ -98,51 +117,65 @@ module ActionDispatch
end
end
- private
+ def trace_to_show
+ if traces["Application Trace"].empty? && rescue_template != "routing_error"
+ "Full Trace"
+ else
+ "Application Trace"
+ end
+ end
- def backtrace
- Array(@exception.backtrace)
+ def source_to_show_id
+ (traces[trace_to_show].first || {})[:id]
end
- def original_exception(exception)
- if @@rescue_responses.has_key?(exception.cause.class.name)
- exception.cause
- else
- exception
+ private
+
+ def backtrace
+ Array(@exception.backtrace)
end
- end
- def clean_backtrace(*args)
- if backtrace_cleaner
- backtrace_cleaner.clean(backtrace, *args)
- else
- backtrace
+ def causes_for(exception)
+ return enum_for(__method__, exception) unless block_given?
+
+ yield exception while exception = exception.cause
+ end
+
+ def wrapped_causes_for(exception, backtrace_cleaner)
+ causes_for(exception).map { |cause| self.class.new(backtrace_cleaner, cause) }
+ end
+
+ def clean_backtrace(*args)
+ if backtrace_cleaner
+ backtrace_cleaner.clean(backtrace, *args)
+ else
+ backtrace
+ end
end
- end
- 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]
+ 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
- end
- 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
+ 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
- def expand_backtrace
- @exception.backtrace.unshift(
- @exception.to_s.split("\n")
- ).flatten!
- end
+ def expand_backtrace
+ @exception.backtrace.unshift(
+ @exception.to_s.split("\n")
+ ).flatten!
+ end
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/executor.rb b/actionpack/lib/action_dispatch/middleware/executor.rb
index 06245b403b..129b18d3d9 100644
--- a/actionpack/lib/action_dispatch/middleware/executor.rb
+++ b/actionpack/lib/action_dispatch/middleware/executor.rb
@@ -1,4 +1,6 @@
-require 'rack/body_proxy'
+# frozen_string_literal: true
+
+require "rack/body_proxy"
module ActionDispatch
class Executor
diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb
index 06038af571..cf9165d008 100644
--- a/actionpack/lib/action_dispatch/middleware/flash.rb
+++ b/actionpack/lib/action_dispatch/middleware/flash.rb
@@ -1,4 +1,6 @@
-require 'active_support/core_ext/hash/keys'
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/keys"
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
@@ -36,7 +38,7 @@ module ActionDispatch
#
# See docs on the FlashHash class for more details about the flash.
class Flash
- KEY = 'action_dispatch.request.flash_hash'.freeze
+ KEY = "action_dispatch.request.flash_hash"
module RequestMethods
# Access the contents of the flash. Use <tt>flash["notice"]</tt> to
@@ -60,18 +62,18 @@ module ActionDispatch
session = self.session || {}
flash_hash = self.flash_hash
- if flash_hash && (flash_hash.present? || session.key?('flash'))
+ 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')
+ 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
- def reset_session # :nodoc
+ def reset_session # :nodoc:
super
self.flash = nil
end
@@ -118,8 +120,8 @@ module ActionDispatch
end
new(flashes, flashes.keys)
when Hash # Rails 4.0
- flashes = value['flashes']
- if discard = value['discard']
+ flashes = value["flashes"]
+ if discard = value["discard"]
flashes.except!(*discard)
end
new(flashes, flashes.keys)
@@ -129,11 +131,11 @@ module ActionDispatch
end
# Builds a hash containing the flashes to keep for the next request.
- # If there are none to keep, returns nil.
+ # If there are none to keep, returns +nil+.
def to_session_value #:nodoc:
flashes_to_keep = @flashes.except(*@discard)
return nil if flashes_to_keep.empty?
- {'flashes' => flashes_to_keep}
+ { "discard" => [], "flashes" => flashes_to_keep }
end
def initialize(flashes = {}, discard = []) #:nodoc:
@@ -277,15 +279,16 @@ module ActionDispatch
end
protected
- def now_is_loaded?
- @now
- end
+ def now_is_loaded?
+ @now
+ end
- def stringify_array(array)
- array.map do |item|
- item.kind_of?(Symbol) ? item.to_s : item
+ private
+ def stringify_array(array) # :doc:
+ array.map do |item|
+ item.kind_of?(Symbol) ? item.to_s : item
+ end
end
- end
end
def self.new(app) app; end
diff --git a/actionpack/lib/action_dispatch/middleware/host_authorization.rb b/actionpack/lib/action_dispatch/middleware/host_authorization.rb
new file mode 100644
index 0000000000..b7dff1df41
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/host_authorization.rb
@@ -0,0 +1,103 @@
+# 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
diff --git a/actionpack/lib/action_dispatch/middleware/params_parser.rb b/actionpack/lib/action_dispatch/middleware/params_parser.rb
deleted file mode 100644
index faf3262b8f..0000000000
--- a/actionpack/lib/action_dispatch/middleware/params_parser.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-require 'action_dispatch/http/request'
-
-module ActionDispatch
- # ActionDispatch::ParamsParser works for all the requests having any Content-Length
- # (like POST). It takes raw data from the request and puts it through the parser
- # that is picked based on Content-Type header.
- #
- # In case of any error while parsing data ParamsParser::ParseError is raised.
- class ParamsParser
- # Raised when raw data from the request cannot be parsed by the parser
- # defined for request's content mime type.
- class ParseError < StandardError
-
- def initialize(message = nil, original_exception = nil)
- if message
- ActiveSupport::Deprecation.warn("Passing #message is deprecated and has no effect. " \
- "#{self.class} will automatically capture the message " \
- "of the original exception.", caller)
- end
-
- if original_exception
- ActiveSupport::Deprecation.warn("Passing #original_exception is deprecated and has no effect. " \
- "Exceptions will automatically capture the original exception.", caller)
- end
-
- super($!.message)
- end
-
- def original_exception
- ActiveSupport::Deprecation.warn("#original_exception is deprecated. Use #cause instead.", caller)
- cause
- end
- end
-
- # Create a new +ParamsParser+ middleware instance.
- #
- # The +parsers+ argument can take Hash of parsers where key is identifying
- # content mime type, and value is a lambda that is going to process data.
- def self.new(app, parsers = {})
- ActiveSupport::Deprecation.warn('ActionDispatch::ParamsParser is deprecated and will be removed in Rails 5.1. Configure the parameter parsing in ActionDispatch::Request.parameter_parsers.')
- parsers = parsers.transform_keys { |key| key.respond_to?(:symbol) ? key.symbol : key }
- ActionDispatch::Request.parameter_parsers = ActionDispatch::Request::DEFAULT_PARSERS.merge(parsers)
- app
- end
- end
-end
diff --git a/actionpack/lib/action_dispatch/middleware/public_exceptions.rb b/actionpack/lib/action_dispatch/middleware/public_exceptions.rb
index 0f27984550..a88ad40f21 100644
--- a/actionpack/lib/action_dispatch/middleware/public_exceptions.rb
+++ b/actionpack/lib/action_dispatch/middleware/public_exceptions.rb
@@ -1,11 +1,13 @@
+# frozen_string_literal: true
+
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 `/public`
+ # 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 `/public/500.html`.
+ # 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 `/public/500.<locale>.html`. If an internationalized template
- # is not found it will fall back on `/public/500.html`.
+ # 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.
@@ -19,37 +21,41 @@ module ActionDispatch
def call(env)
request = ActionDispatch::Request.new(env)
status = request.path_info[1..-1].to_i
- content_type = request.formats.first
- body = { :status => status, :error => Rack::Utils::HTTP_STATUS_CODES.fetch(status, Rack::Utils::HTTP_STATUS_CODES[500]) }
+ 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
private
- 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)
+ 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
- end
- 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
+ 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
- def render_html(status)
- path = "#{public_path}/#{status}.#{I18n.locale}.html"
- path = "#{public_path}/#{status}.html" unless (found = File.exist?(path))
+ 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" }, []]
+ if found || File.exist?(path)
+ render_format(status, "text/html", File.read(path))
+ else
+ [404, { "X-Cascade" => "pass" }, []]
+ end
end
- end
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/reloader.rb b/actionpack/lib/action_dispatch/middleware/reloader.rb
index 112bde6596..8bb3ba7504 100644
--- a/actionpack/lib/action_dispatch/middleware/reloader.rb
+++ b/actionpack/lib/action_dispatch/middleware/reloader.rb
@@ -1,54 +1,12 @@
+# frozen_string_literal: true
+
module ActionDispatch
- # ActionDispatch::Reloader provides prepare and cleanup callbacks,
- # intended to assist with code reloading during development.
- #
- # Prepare callbacks are run before each request, and cleanup callbacks
- # after each request. In this respect they are analogs of ActionDispatch::Callback's
- # before and after callbacks. However, cleanup callbacks are not called until the
- # request is fully complete -- that is, after #close has been called on
- # the response body. This is important for streaming responses such as the
- # following:
- #
- # self.response_body = -> (response, output) do
- # # code here which refers to application models
- # end
- #
- # Cleanup callbacks will not be called until after the response_body lambda
- # is evaluated, ensuring that it can refer to application models and other
- # classes before they are unloaded.
+ # 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. Callbacks may be registered even when it is not included in the
- # middleware stack, but are executed only when <tt>ActionDispatch::Reloader.prepare!</tt>
- # or <tt>ActionDispatch::Reloader.cleanup!</tt> are called manually.
- #
+ # is false.
class Reloader < Executor
- def self.to_prepare(*args, &block)
- ActiveSupport::Reloader.to_prepare(*args, &block)
- end
-
- def self.to_cleanup(*args, &block)
- ActiveSupport::Reloader.to_complete(*args, &block)
- end
-
- def self.prepare!
- default_reloader.prepare!
- end
-
- def self.cleanup!
- default_reloader.reload!
- end
-
- class << self
- attr_accessor :default_reloader # :nodoc:
-
- deprecate to_prepare: 'use ActiveSupport::Reloader.to_prepare instead',
- to_cleanup: 'use ActiveSupport::Reloader.to_complete instead',
- prepare!: 'use Rails.application.reloader.prepare! instead',
- cleanup!: 'use Rails.application.reloader.reload! instead of cleanup + prepare'
- end
-
- self.default_reloader = ActiveSupport::Reloader
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/remote_ip.rb b/actionpack/lib/action_dispatch/middleware/remote_ip.rb
index 31b75498b6..a5667573f4 100644
--- a/actionpack/lib/action_dispatch/middleware/remote_ip.rb
+++ b/actionpack/lib/action_dispatch/middleware/remote_ip.rb
@@ -1,4 +1,6 @@
-require 'ipaddr'
+# frozen_string_literal: true
+
+require "ipaddr"
module ActionDispatch
# This middleware calculates the IP address of the remote client that is
@@ -10,7 +12,7 @@ module ActionDispatch
# 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}[http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2]
+ # 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}[http://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)
@@ -29,7 +31,7 @@ module ActionDispatch
# 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
- # http://en.wikipedia.org/wiki/Private_network for details.
+ # https://en.wikipedia.org/wiki/Private_network for details.
TRUSTED_PROXIES = [
"127.0.0.1", # localhost IPv4
"::1", # localhost IPv6
@@ -131,8 +133,8 @@ module ActionDispatch
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} " +
+ raise IpSpoofAttackError, "IP spoofing attack?! " \
+ "HTTP_CLIENT_IP=#{@req.client_ip.inspect} " \
"HTTP_X_FORWARDED_FOR=#{@req.x_forwarded_for.inspect}"
end
@@ -153,31 +155,27 @@ module ActionDispatch
@ip ||= calculate_ip
end
- protected
+ private
- def ips_from(header)
+ def ips_from(header) # :doc:
return [] unless header
- # Split the comma-separated list into an array of strings
+ # Split the comma-separated list into an array of strings.
ips = header.strip.split(/[,\s]+/)
ips.select do |ip|
- begin
- # 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
+ # 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)
+ def filter_proxies(ips) # :doc:
ips.reject do |ip|
@proxies.any? { |proxy| proxy === ip }
end
end
-
end
-
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/request_id.rb b/actionpack/lib/action_dispatch/middleware/request_id.rb
index 1555ff72af..fcc0c72240 100644
--- a/actionpack/lib/action_dispatch/middleware/request_id.rb
+++ b/actionpack/lib/action_dispatch/middleware/request_id.rb
@@ -1,9 +1,12 @@
-require 'securerandom'
-require 'active_support/core_ext/string/access'
+# 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
- # ActionDispatch::Request#uuid or the alias ActionDispatch::Request#request_id) and sends the same id to the client via the X-Request-Id header.
+ # 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
@@ -12,7 +15,7 @@ module ActionDispatch
# 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".freeze # :nodoc:
+ X_REQUEST_ID = "X-Request-Id" #:nodoc:
def initialize(app)
@app = app
@@ -27,7 +30,7 @@ module ActionDispatch
private
def make_request_id(request_id)
if request_id.presence
- request_id.gsub(/[^\w\-]/, "".freeze).first(255)
+ request_id.gsub(/[^\w\-@]/, "").first(255)
else
internal_request_id
end
diff --git a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb
index 5fb5953811..5b0be96223 100644
--- a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb
+++ b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb
@@ -1,34 +1,25 @@
-require 'rack/utils'
-require 'rack/request'
-require 'rack/session/abstract/id'
-require 'action_dispatch/middleware/cookies'
-require 'action_dispatch/request/session'
+# frozen_string_literal: true
+
+require "rack/utils"
+require "rack/request"
+require "rack/session/abstract/id"
+require "action_dispatch/middleware/cookies"
+require "action_dispatch/request/session"
module ActionDispatch
module Session
class SessionRestoreError < StandardError #:nodoc:
-
- def initialize(const_error = nil)
- if const_error
- ActiveSupport::Deprecation.warn("Passing #original_exception is deprecated and has no effect. " \
- "Exceptions will automatically capture the original exception.", caller)
- end
-
- super("Session contains objects whose class definition isn't available.\n" +
- "Remember to require the classes for all objects kept in the session.\n" +
+ 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
-
- def original_exception
- ActiveSupport::Deprecation.warn("#original_exception is deprecated. Use #cause instead.", caller)
- cause
- end
end
module Compatibility
def initialize(app, options = {})
- options[:key] ||= '_session_id'
+ options[:key] ||= "_session_id"
super
end
@@ -38,14 +29,13 @@ module ActionDispatch
sid
end
- protected
+ private
- def initialize_sid
+ def initialize_sid # :doc:
@default_options.delete(:sidbits)
@default_options.delete(:secure_random)
end
- private
def make_request(env)
ActionDispatch::Request.new env
end
@@ -65,7 +55,7 @@ module ActionDispatch
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 ':'
+ # Note that the regexp does not allow $1 to end with a ':'.
$1.constantize
rescue LoadError, NameError
raise ActionDispatch::Session::SessionRestoreError
@@ -94,9 +84,9 @@ module ActionDispatch
private
- def set_cookie(request, session_id, cookie)
- request.cookie_jar[key] = cookie
- end
+ def set_cookie(request, session_id, cookie)
+ request.cookie_jar[key] = cookie
+ end
end
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/session/cache_store.rb b/actionpack/lib/action_dispatch/middleware/session/cache_store.rb
index 589ae46e38..a6d965a644 100644
--- a/actionpack/lib/action_dispatch/middleware/session/cache_store.rb
+++ b/actionpack/lib/action_dispatch/middleware/session/cache_store.rb
@@ -1,4 +1,6 @@
-require 'action_dispatch/middleware/session/abstract_store'
+# frozen_string_literal: true
+
+require "action_dispatch/middleware/session/abstract_store"
module ActionDispatch
module Session
@@ -19,7 +21,7 @@ module ActionDispatch
# Get a session from the cache.
def find_session(env, sid)
- unless sid and session = @cache.read(cache_key(sid))
+ unless sid && (session = @cache.read(cache_key(sid)))
sid, session = generate_sid, {}
end
[sid, session]
@@ -29,7 +31,7 @@ module ActionDispatch
def write_session(env, sid, session, options)
key = cache_key(sid)
if session
- @cache.write(key, session, :expires_in => options[:expire_after])
+ @cache.write(key, session, expires_in: options[:expire_after])
else
@cache.delete(key)
end
diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
index dec9c60ef2..7c43c781c7 100644
--- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
+++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
@@ -1,6 +1,8 @@
-require 'active_support/core_ext/hash/keys'
-require 'action_dispatch/middleware/session/abstract_store'
-require 'rack/session/cookie'
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/keys"
+require "action_dispatch/middleware/session/abstract_store"
+require "rack/session/cookie"
module ActionDispatch
module Session
@@ -14,44 +16,26 @@ module ActionDispatch
# The cookie jar used for storage is automatically configured to be the
# best possible option given your application's configuration.
#
- # If you only have secret_token set, your cookies will be signed, but
- # not encrypted. This means a user cannot alter their +user_id+ without
- # knowing your app's secret key, but can easily read their +user_id+. This
- # was the default for Rails 3 apps.
- #
- # If you have secret_key_base set, your cookies will be encrypted. This
+ # 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.
#
- # If you have both secret_token and secret_key_base set, your cookies will
- # be encrypted, and signed cookies generated by Rails 3 will be
- # transparently read and encrypted to provide a smooth upgrade path.
- #
- # Configure your session store in config/initializers/session_store.rb:
+ # Configure your session store in an initializer:
#
# Rails.application.config.session_store :cookie_store, key: '_your_app_session'
#
- # Configure your secret key in config/secrets.yml:
- #
- # development:
- # secret_key_base: 'secret key'
- #
- # To generate a secret key for an existing application, run `rails secret`.
+ # 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 you are upgrading an existing Rails 3 app, you should leave your
- # existing secret_token in place and simply add the new secret_key_base.
- # Note that you should wait to set secret_key_base until you have 100% of
- # your userbase on Rails 4 and are reasonably sure you will not need to
- # rollback to Rails 3. This is because cookies signed based on the new
- # secret_key_base in Rails 4 are not backwards compatible with Rails 3.
- # You are free to leave your existing secret_token in place, not set the
- # new secret_key_base, and ignore the deprecation warnings until you are
- # reasonably sure that your upgrade is otherwise complete. 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 upgrading.
+ # 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 the secret key will invalidate all existing sessions!
+ # 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
@@ -63,8 +47,8 @@ module ActionDispatch
# Other useful options include <tt>:key</tt>, <tt>:secure</tt> and
# <tt>:httponly</tt>.
class CookieStore < AbstractStore
- def initialize(app, options={})
- super(app, options.merge!(:cookie_only => true))
+ def initialize(app, options = {})
+ super(app, options.merge!(cookie_only: true))
end
def delete_session(req, session_id, options)
@@ -84,46 +68,46 @@ module ActionDispatch
private
- def extract_session_id(req)
- stale_session_check! do
- unpacked_cookie_data(req)["session_id"]
+ def extract_session_id(req)
+ stale_session_check! do
+ unpacked_cookie_data(req)["session_id"]
+ end
end
- end
- 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!
+ 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
- data || {}
+ req.set_header k, v
end
- req.set_header k, v
end
- end
- def persistent_session_id!(data, sid=nil)
- data ||= {}
- data["session_id"] ||= sid || generate_sid
- data
- end
+ def persistent_session_id!(data, sid = nil)
+ data ||= {}
+ data["session_id"] ||= sid || generate_sid
+ data
+ end
- def write_session(req, sid, session_data, options)
- session_data["session_id"] = sid
- session_data
- end
+ def write_session(req, sid, session_data, options)
+ session_data["session_id"] = sid
+ session_data
+ end
- def set_cookie(request, session_id, cookie)
- cookie_jar(request)[@key] = cookie
- end
+ def set_cookie(request, session_id, cookie)
+ cookie_jar(request)[@key] = cookie
+ end
- def get_cookie(req)
- cookie_jar(req)[@key]
- end
+ def get_cookie(req)
+ cookie_jar(req)[@key]
+ end
- def cookie_jar(request)
- request.cookie_jar.signed_or_encrypted
- end
+ def cookie_jar(request)
+ request.cookie_jar.signed_or_encrypted
+ end
end
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb b/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb
index cb19786f0b..914df3a2b1 100644
--- a/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb
+++ b/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb
@@ -1,6 +1,8 @@
-require 'action_dispatch/middleware/session/abstract_store'
+# frozen_string_literal: true
+
+require "action_dispatch/middleware/session/abstract_store"
begin
- require 'rack/session/dalli'
+ 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
diff --git a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb
index 64695f9738..767143a368 100644
--- a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb
+++ b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb
@@ -1,5 +1,7 @@
-require 'action_dispatch/http/request'
-require 'action_dispatch/middleware/exception_wrapper'
+# frozen_string_literal: true
+
+require "action_dispatch/http/request"
+require "action_dispatch/middleware/exception_wrapper"
module ActionDispatch
# This middleware rescues any exception returned by the application
@@ -8,14 +10,14 @@ module ActionDispatch
# 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.
+ # 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.
class ShowExceptions
- FAILSAFE_RESPONSE = [500, { 'Content-Type' => 'text/plain' },
+ 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 " \
@@ -39,22 +41,22 @@ module ActionDispatch
private
- 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.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
+ 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
- def pass_response(status)
- [status, {"Content-Type" => "text/html; charset=#{Response.default_charset}", "Content-Length" => "0"}, []]
- end
+ def pass_response(status)
+ [status, { "Content-Type" => "text/html; charset=#{Response.default_charset}", "Content-Length" => "0" }, []]
+ end
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/ssl.rb b/actionpack/lib/action_dispatch/middleware/ssl.rb
index ab3077b308..00902ede21 100644
--- a/actionpack/lib/action_dispatch/middleware/ssl.rb
+++ b/actionpack/lib/action_dispatch/middleware/ssl.rb
@@ -1,78 +1,69 @@
+# frozen_string_literal: true
+
module ActionDispatch
- # This middleware is added to the stack when `config.force_ssl = true`, and is passed
- # the options set in `config.ssl_options`. It does three jobs to enforce secure HTTP
+ # 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. TLS redirect: 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. `redirect: { host: "secure.widgets.com", port: 8080 }`), or set
- # `redirect: false` to disable this feature.
+ # 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 { request.path =~ /healthcheck/ } } }
+ #
+ # Cookies will not be flagged as secure for excluded requests.
#
- # 2. Secure cookies: Sets the `secure` flag on cookies to tell browsers they
- # mustn't be sent along with http:// requests. Enabled by default. Set
- # `config.ssl_options` with `secure_cookies: false` to disable this feature.
+ # 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. HTTP Strict Transport Security (HSTS): Tells the browser to remember
- # this site as TLS-only and automatically redirect non-TLS requests.
- # Enabled by default. Configure `config.ssl_options` with `hsts: false` to disable.
+ # 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 `hsts: { … }` to configure HSTS:
- # * `expires`: How long, in seconds, these settings will stick. Defaults to
- # `180.days` (recommended). The minimum required to qualify for browser
- # preload lists is `18.weeks`.
- # * `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 *except the
- # first visit* 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.appspot.com to submit your site for inclusion.
+ # Set +config.ssl_options+ with <tt>hsts: { ... }</tt> to configure HSTS:
#
- # 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 `hsts: false` is a shortcut for
- # `hsts: { expires: 0 }`.
+ # * +expires+: How long, in seconds, these settings will stick. The minimum
+ # required to qualify for browser preload lists is 1 year. Defaults to
+ # 1 year (recommended).
#
- # Requests can opt-out of redirection with `exclude`:
+ # * +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+.
#
- # config.ssl_options = { redirect: { exclude: -> request { request.path =~ /healthcheck/ } } }
+ # * +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
- # Default to 180 days, the low end for https://www.ssllabs.com/ssltest/
- # and greater than the 18-week requirement for browser preload lists.
- HSTS_EXPIRES_IN = 15552000
+ # :stopdoc:
+
+ # Default to 1 year, the minimum for browser preload lists.
+ HSTS_EXPIRES_IN = 31536000
def self.default_hsts_options
- { expires: HSTS_EXPIRES_IN, subdomains: false, preload: false }
+ { expires: HSTS_EXPIRES_IN, subdomains: true, preload: false }
end
- def initialize(app, redirect: {}, hsts: {}, secure_cookies: true, **options)
+ def initialize(app, redirect: {}, hsts: {}, secure_cookies: true)
@app = app
- if options[:host] || options[:port]
- ActiveSupport::Deprecation.warn <<-end_warning.strip_heredoc
- The `:host` and `:port` options are moving within `:redirect`:
- `config.ssl_options = { redirect: { host: …, port: … } }`.
- end_warning
- @redirect = options.slice(:host, :port)
- else
- @redirect = redirect
- end
+ @redirect = redirect
@exclude = @redirect && @redirect[:exclude] || proc { !@redirect }
@secure_cookies = secure_cookies
- if hsts != true && hsts != false && hsts[:subdomains].nil?
- hsts[:subdomains] = false
-
- ActiveSupport::Deprecation.warn <<-end_warning.strip_heredoc
- In Rails 5.1, The `:subdomains` option of HSTS config will be treated as true if
- unspecified. Set `config.ssl_options = { hsts: { subdomains: false } }` to opt out
- of this behavior.
- end_warning
- end
-
@hsts_header = build_hsts_header(normalize_hsts_options(hsts))
end
@@ -82,7 +73,7 @@ module ActionDispatch
if request.ssl?
@app.call(env).tap do |status, headers, body|
set_hsts_header! headers
- flag_cookies_as_secure! headers if @secure_cookies
+ flag_cookies_as_secure! headers if @secure_cookies && !@exclude.call(request)
end
else
return redirect_to_https request unless @exclude.call(request)
@@ -92,7 +83,7 @@ module ActionDispatch
private
def set_hsts_header!(headers)
- headers['Strict-Transport-Security'.freeze] ||= @hsts_header
+ headers["Strict-Transport-Security"] ||= @hsts_header
end
def normalize_hsts_options(options)
@@ -109,40 +100,48 @@ module ActionDispatch
end
end
- # http://tools.ietf.org/html/rfc6797#section-6.1
+ # https://tools.ietf.org/html/rfc6797#section-6.1
def build_hsts_header(hsts)
- value = "max-age=#{hsts[:expires].to_i}"
+ 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'.freeze]
- cookies = cookies.split("\n".freeze)
+ if cookies = headers["Set-Cookie"]
+ cookies = cookies.split("\n")
- headers['Set-Cookie'.freeze] = cookies.map { |cookie|
- if cookie !~ /;\s*secure\s*(;|$)/i
+ headers["Set-Cookie"] = cookies.map { |cookie|
+ if !/;\s*secure\s*(;|$)/i.match?(cookie)
"#{cookie}; secure"
else
cookie
end
- }.join("\n".freeze)
+ }.join("\n")
end
end
def redirect_to_https(request)
- [ @redirect.fetch(:status, 301),
- { 'Content-Type' => 'text/html',
- 'Location' => https_location_for(request) },
+ [ @redirect.fetch(:status, redirection_status(request)),
+ { "Content-Type" => "text/html",
+ "Location" => https_location_for(request) },
@redirect.fetch(:body, []) ]
end
+ def redirection_status(request)
+ if request.get? || request.head?
+ 301 # Issue a permanent redirect via a GET request.
+ 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 = +"https://#{host}"
location << ":#{port}" if port != 80 && port != 443
location << request.fullpath
location
diff --git a/actionpack/lib/action_dispatch/middleware/stack.rb b/actionpack/lib/action_dispatch/middleware/stack.rb
index 0b4bee5462..f0c869fba0 100644
--- a/actionpack/lib/action_dispatch/middleware/stack.rb
+++ b/actionpack/lib/action_dispatch/middleware/stack.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "active_support/inflector/methods"
require "active_support/dependencies"
@@ -32,7 +34,28 @@ module ActionDispatch
end
def build(app)
- klass.new(app, *args, &block)
+ InstrumentationProxy.new(klass.new(app, *args, &block), 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.
+ class InstrumentationProxy
+ EVENT_NAME = "process_middleware.action_dispatch"
+
+ def initialize(middleware, class_name)
+ @middleware = middleware
+
+ @payload = {
+ middleware: class_name,
+ }
+ end
+
+ def call(env)
+ ActiveSupport::Notifications.instrument(EVENT_NAME, @payload) do
+ @middleware.call(env)
+ end
end
end
@@ -88,7 +111,6 @@ module ActionDispatch
end
def delete(target)
- target = get_class target
middlewares.delete_if { |m| m.klass == target }
end
@@ -96,38 +118,20 @@ module ActionDispatch
middlewares.push(build_middleware(klass, args, block))
end
- def build(app = Proc.new)
- middlewares.freeze.reverse.inject(app) { |a, e| e.build(a) }
+ def build(app = nil, &block)
+ middlewares.freeze.reverse.inject(app || block) { |a, e| e.build(a) }
end
private
- def assert_index(index, where)
- index = get_class index
- i = index.is_a?(Integer) ? index : middlewares.index { |m| m.klass == index }
- raise "No such middleware to insert #{where}: #{index.inspect}" unless i
- i
- end
-
- def get_class(klass)
- if klass.is_a?(String) || klass.is_a?(Symbol)
- classcache = ActiveSupport::Dependencies::Reference
- converted_klass = classcache[klass.to_s]
- ActiveSupport::Deprecation.warn <<-eowarn
-Passing strings or symbols to the middleware builder is deprecated, please change
-them to actual class references. For example:
-
- "#{klass}" => #{converted_klass}
-
- eowarn
- converted_klass
- else
- klass
+ def assert_index(index, where)
+ i = index.is_a?(Integer) ? index : middlewares.index { |m| m.klass == index }
+ raise "No such middleware to insert #{where}: #{index.inspect}" unless i
+ i
end
- end
- def build_middleware(klass, args, block)
- Middleware.new(get_class(klass), args, block)
- end
+ def build_middleware(klass, args, block)
+ Middleware.new(klass, args, block)
+ end
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/static.rb b/actionpack/lib/action_dispatch/middleware/static.rb
index 41c220236a..1f2f7757a3 100644
--- a/actionpack/lib/action_dispatch/middleware/static.rb
+++ b/actionpack/lib/action_dispatch/middleware/static.rb
@@ -1,20 +1,22 @@
-require 'rack/utils'
-require 'active_support/core_ext/uri'
+# frozen_string_literal: true
+
+require "rack/utils"
+require "active_support/core_ext/uri"
module ActionDispatch
# This middleware returns a file's contents from disk in the body response.
# When initialized, it can accept optional HTTP headers, which will be set
# when a response containing a file's contents is delivered.
#
- # This middleware will render the file specified in `env["PATH_INFO"]`
+ # This middleware will render the file specified in <tt>env["PATH_INFO"]</tt>
# where the base path is in the +root+ directory. For example, if the +root+
- # is set to `public/`, then a request with `env["PATH_INFO"]` of
- # `assets/application.js` will return a response with the contents of a file
- # located at `public/assets/application.js` if the file exists. If the file
+ # is set to +public/+, then a request with <tt>env["PATH_INFO"]</tt> of
+ # +assets/application.js+ will return a response with the contents of a file
+ # located at +public/assets/application.js+ if the file exists. If the file
# does not exist, a 404 "File not Found" response will be returned.
class FileHandler
- def initialize(root, index: 'index', headers: {})
- @root = root.chomp('/')
+ def initialize(root, index: "index", headers: {})
+ @root = root.chomp("/").b
@file_server = ::Rack::File.new(@root, headers)
@index = index
end
@@ -23,30 +25,29 @@ module ActionDispatch
# correct read permissions, the return value is a URI-escaped string
# representing the filename. Otherwise, false is returned.
#
- # Used by the `Static` class to check the existence of a valid file
- # in the server's `public/` directory (see Static#call).
+ # Used by the +Static+ class to check the existence of a valid file
+ # in the server's +public/+ directory (see Static#call).
def match?(path)
path = ::Rack::Utils.unescape_path path
- return false unless valid_path?(path)
- path = Rack::Utils.clean_path_info path
+ return false unless ::Rack::Utils.valid_path? path
+ path = ::Rack::Utils.clean_path_info path
paths = [path, "#{path}#{ext}", "#{path}/#{@index}#{ext}"]
if match = paths.detect { |p|
- path = File.join(@root, p.force_encoding('UTF-8'.freeze))
+ path = File.join(@root, p.b)
begin
File.file?(path) && File.readable?(path)
rescue SystemCallError
false
end
-
}
- return ::Rack::Utils.escape_path(match)
+ return ::Rack::Utils.escape_path(match).b
end
end
def call(env)
- serve ActionDispatch::Request.new env
+ serve(Rack::Request.new(env))
end
def serve(request)
@@ -59,15 +60,15 @@ module ActionDispatch
if status == 304
return [status, headers, body]
end
- headers['Content-Encoding'] = 'gzip'
- headers['Content-Type'] = content_type(path)
+ headers["Content-Encoding"] = "gzip"
+ headers["Content-Type"] = content_type(path)
else
status, headers, body = @file_server.call(request.env)
end
- headers['Vary'] = 'Accept-Encoding' if gzip_path
+ headers["Vary"] = "Accept-Encoding" if gzip_path
- return [status, headers, body]
+ [status, headers, body]
ensure
request.path_info = path
end
@@ -78,11 +79,11 @@ module ActionDispatch
end
def content_type(path)
- ::Rack::Mime.mime_type(::File.extname(path), 'text/plain'.freeze)
+ ::Rack::Mime.mime_type(::File.extname(path), "text/plain")
end
def gzip_encoding_accepted?(request)
- request.accept_encoding =~ /\bgzip\b/i
+ request.accept_encoding.any? { |enc, quality| enc =~ /\bgzip\b/i }
end
def gzip_file_path(path)
@@ -94,39 +95,28 @@ module ActionDispatch
false
end
end
-
- def valid_path?(path)
- path.valid_encoding? && !path.include?("\0")
- end
end
# This middleware will attempt to return the contents of a file's body from
# disk in the response. If a file is not found on disk, the request will be
# delegated to the application stack. This middleware is commonly initialized
- # to serve assets from a server's `public/` directory.
+ # to serve assets from a server's +public/+ directory.
#
# This middleware verifies the path to ensure that only files
# living in the root directory can be rendered. A request cannot
# produce a directory traversal using this middleware. Only 'GET' and 'HEAD'
# requests will result in a file being returned.
class Static
- def initialize(app, path, deprecated_cache_control = :not_set, index: 'index', headers: {})
- if deprecated_cache_control != :not_set
- ActiveSupport::Deprecation.warn("The `cache_control` argument is deprecated," \
- "replaced by `headers: { 'Cache-Control' => #{deprecated_cache_control} }`, " \
- " and will be removed in Rails 5.1.")
- headers['Cache-Control'.freeze] = deprecated_cache_control
- end
-
+ def initialize(app, path, index: "index", headers: {})
@app = app
@file_handler = FileHandler.new(path, index: index, headers: headers)
end
def call(env)
- req = ActionDispatch::Request.new env
+ req = Rack::Request.new env
if req.get? || req.head?
- path = req.path_info.chomp('/'.freeze)
+ path = req.path_info.chomp("/")
if match = @file_handler.match?(path)
req.path_info = match
return @file_handler.serve(req)
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.html.erb
index e7b913bbe4..88a8e6ad83 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.html.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.html.erb
@@ -1,6 +1,8 @@
-<% @source_extracts.each_with_index do |source_extract, index| %>
+<% error_index = local_assigns[:error_index] || 0 %>
+
+<% source_extracts.each_with_index do |source_extract, index| %>
<% if source_extract[:code] %>
- <div class="source <%="hidden" if @show_source_idx != index%>" id="frame-source-<%=index%>">
+ <div class="source <%= "hidden" if show_source_idx != index %>" id="frame-source-<%= error_index %>-<%= index %>">
<div class="info">
Extracted source (around line <strong>#<%= source_extract[:line_number] %></strong>):
</div>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb
index ab57b11c7d..835ca8d260 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb
@@ -1,52 +1,62 @@
-<% names = @traces.keys %>
+<% names = traces.keys %>
+<% error_index = local_assigns[:error_index] || 0 %>
<p><code>Rails.root: <%= defined?(Rails) && Rails.respond_to?(:root) ? Rails.root : "unset" %></code></p>
-<div id="traces">
+<div id="traces-<%= error_index %>">
<% names.each do |name| %>
<%
- show = "show('#{name.gsub(/\s/, '-')}');"
- hide = (names - [name]).collect {|hide_name| "hide('#{hide_name.gsub(/\s/, '-')}');"}
+ show = "show('#{name.gsub(/\s/, '-')}-#{error_index}');"
+ hide = (names - [name]).collect {|hide_name| "hide('#{hide_name.gsub(/\s/, '-')}-#{error_index}');"}
%>
<a href="#" onclick="<%= hide.join %><%= show %>; return false;"><%= name %></a> <%= '|' unless names.last == name %>
<% end %>
- <% @traces.each do |name, trace| %>
- <div id="<%= name.gsub(/\s/, '-') %>" style="display: <%= (name == @trace_to_show) ? 'block' : 'none' %>;">
- <pre><code><% trace.each do |frame| %><a class="trace-frames" data-frame-id="<%= frame[:id] %>" href="#"><%= frame[:trace] %></a><br><% end %></code></pre>
+ <% traces.each do |name, trace| %>
+ <div id="<%= "#{name.gsub(/\s/, '-')}-#{error_index}" %>" style="display: <%= (name == trace_to_show) ? 'block' : 'none' %>;">
+ <code style="font-size: 11px;">
+ <% trace.each do |frame| %>
+ <a class="trace-frames trace-frames-<%= error_index %>" data-exception-object-id="<%= frame[:exception_object_id] %>" data-frame-id="<%= frame[:id] %>" href="#">
+ <%= frame[:trace] %>
+ </a>
+ <br>
+ <% end %>
+ </code>
</div>
<% end %>
<script type="text/javascript">
- var traceFrames = document.getElementsByClassName('trace-frames');
- var selectedFrame, currentSource = document.getElementById('frame-source-0');
-
- // Add click listeners for all stack frames
- for (var i = 0; i < traceFrames.length; i++) {
- traceFrames[i].addEventListener('click', function(e) {
- e.preventDefault();
- var target = e.target;
- var frame_id = target.dataset.frameId;
-
- if (selectedFrame) {
- selectedFrame.className = selectedFrame.className.replace("selected", "");
- }
-
- target.className += " selected";
- selectedFrame = target;
-
- // Change the extracted source code
- changeSourceExtract(frame_id);
- });
-
- function changeSourceExtract(frame_id) {
- var el = document.getElementById('frame-source-' + frame_id);
- if (currentSource && el) {
- currentSource.className += " hidden";
- el.className = el.className.replace(" hidden", "");
- currentSource = el;
+ (function() {
+ var traceFrames = document.getElementsByClassName('trace-frames-<%= error_index %>');
+ var selectedFrame, currentSource = document.getElementById('frame-source-<%= error_index %>-0');
+
+ // Add click listeners for all stack frames
+ for (var i = 0; i < traceFrames.length; i++) {
+ traceFrames[i].addEventListener('click', function(e) {
+ e.preventDefault();
+ var target = e.target;
+ var frame_id = target.dataset.frameId;
+
+ if (selectedFrame) {
+ selectedFrame.className = selectedFrame.className.replace("selected", "");
+ }
+
+ target.className += " selected";
+ selectedFrame = target;
+
+ // Change the extracted source code
+ changeSourceExtract(frame_id);
+ });
+
+ function changeSourceExtract(frame_id) {
+ var el = document.getElementById('frame-source-<%= error_index %>-' + frame_id);
+ if (currentSource && el) {
+ currentSource.className += " hidden";
+ el.className = el.className.replace(" hidden", "");
+ currentSource = el;
+ }
}
}
- }
+ })();
</script>
</div>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb
new file mode 100644
index 0000000000..1fbc107e28
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb
@@ -0,0 +1,7 @@
+<header>
+ <h1>Blocked host: <%= @host %></h1>
+</header>
+<div id="container">
+ <h2>To allow requests to <%= @host %>, add the following to your environment configuration:</h2>
+ <pre>config.hosts &lt;&lt; "<%= @host %>"</pre>
+</div>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb
new file mode 100644
index 0000000000..a94dd982a7
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb
@@ -0,0 +1,5 @@
+Blocked host: <%= @host %>
+
+To allow requests to <%= @host %>, add the following to your environment configuration:
+
+ config.hosts << "<%= @host %>"
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb
index f154021ae6..bde26f46c2 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb
@@ -10,7 +10,25 @@
<div id="container">
<h2><%= h @exception.message %></h2>
- <%= render template: "rescues/_source" %>
- <%= render template: "rescues/_trace" %>
+ <%= render "rescues/source", source_extracts: @source_extracts, show_source_idx: @show_source_idx, error_index: 0 %>
+ <%= render "rescues/trace", traces: @traces, trace_to_show: @trace_to_show, error_index: 0 %>
+
+ <% if @exception.cause %>
+ <h2>Exception Causes</h2>
+ <% end %>
+
+ <% @exception_wrapper.wrapped_causes.each.with_index(1) do |wrapper, index| %>
+ <div class="details">
+ <a class="summary" href="#" style="color: #F0F0F0; text-decoration: none; background: #C52F24; border-bottom: none;" onclick="return toggle(<%= wrapper.exception.object_id %>)">
+ <%= wrapper.exception.class.name %>: <%= h wrapper.exception.message %>
+ </a>
+ </div>
+
+ <div id="<%= wrapper.exception.object_id %>" style="display: none;">
+ <%= render "rescues/source", source_extracts: wrapper.source_extracts, show_source_idx: wrapper.source_to_show_id, error_index: index %>
+ <%= render "rescues/trace", traces: wrapper.traces, trace_to_show: wrapper.trace_to_show, error_index: index %>
+ </div>
+ <% end %>
+
<%= render template: "rescues/_request_and_response" %>
</div>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb
new file mode 100644
index 0000000000..77cfdd20c8
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb
@@ -0,0 +1,24 @@
+<header>
+ <h1>
+ <%= @exception.class.to_s %>
+ <% if @request.parameters['controller'] %>
+ in <%= @request.parameters['controller'].camelize %>Controller<% if @request.parameters['action'] %>#<%= @request.parameters['action'] %><% end %>
+ <% end %>
+ </h1>
+</header>
+
+<div id="container">
+ <h2>
+ <%= h @exception.message %>
+ <% if defined?(ActiveStorage) && @exception.message.match?(%r{#{ActiveStorage::Blob.table_name}|#{ActiveStorage::Attachment.table_name}}) %>
+ <br />To resolve this issue run: rails active_storage:install
+ <% end %>
+ <% if defined?(ActionMailbox) && @exception.message.match?(%r{#{ActionMailbox::InboundEmail.table_name}}) %>
+ <br />To resolve this issue run: rails action_mailbox:install
+ <% end %>
+ </h2>
+
+ <%= render "rescues/source", source_extracts: @source_extracts, show_source_idx: @show_source_idx %>
+ <%= render "rescues/trace", traces: @traces, trace_to_show: @trace_to_show %>
+ <%= render template: "rescues/_request_and_response" %>
+</div>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb
new file mode 100644
index 0000000000..16c3ecc331
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb
@@ -0,0 +1,15 @@
+<%= @exception.class.to_s %><%
+ if @request.parameters['controller']
+%> in <%= @request.parameters['controller'].camelize %>Controller<% if @request.parameters['action'] %>#<%= @request.parameters['action'] %><% end %>
+<% end %>
+
+<%= @exception.message %>
+<% if defined?(ActiveStorage) && @exception.message.match?(%r{#{ActiveStorage::Blob.table_name}|#{ActiveStorage::Attachment.table_name}}) %>
+To resolve this issue run: rails active_storage:install
+<% if defined?(ActionMailbox) && @exception.message.match?(%r{#{ActionMailbox::InboundEmail.table_name}}) %>
+To resolve this issue run: rails action_mailbox:install
+<% end %>
+
+<%= render template: "rescues/_source" %>
+<%= render template: "rescues/_trace" %>
+<%= render template: "rescues/_request_and_response" %>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb
index e0509f56f4..39ea25bdfc 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb
@@ -106,6 +106,7 @@
.line {
padding-left: 10px;
+ white-space: pre;
}
.line:hover {
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb
new file mode 100644
index 0000000000..76ab1691b5
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb
@@ -0,0 +1,19 @@
+<header>
+ <h1>No template for interactive request</h1>
+</header>
+
+<div id="container">
+ <h2><%= h @exception.message %></h2>
+
+ <p class="summary">
+ <strong>NOTE!</strong><br>
+ Unless told otherwise, Rails expects an action to render a template with the same name,<br>
+ contained in a folder named after its controller.
+
+ If this controller is an API responding with 204 (No Content), <br>
+ which does not require a template,
+ then this error will occur when trying to access it via browser,<br>
+ since we expect an HTML template
+ to be rendered for such requests. If that's the case, carry on.
+ </p>
+</div>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb
new file mode 100644
index 0000000000..fcdbe6069d
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb
@@ -0,0 +1,3 @@
+Missing exact template
+
+<%= @exception.message %>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb
index 2a65fd06ad..22eb6e9b4e 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb
@@ -5,7 +5,7 @@
<div id="container">
<h2><%= h @exception.message %></h2>
- <%= render template: "rescues/_source" %>
- <%= render template: "rescues/_trace" %>
+ <%= render "rescues/source", source_extracts: @source_extracts, show_source_idx: @show_source_idx %>
+ <%= render "rescues/trace", traces: @traces, trace_to_show: @trace_to_show %>
<%= render template: "rescues/_request_and_response" %>
</div>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb
index 55dd5ddc7b..2b8f3f2a5e 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb
@@ -14,7 +14,7 @@
</p>
<% end %>
- <%= render template: "rescues/_trace" %>
+ <%= render "rescues/trace", traces: @traces, trace_to_show: @trace_to_show %>
<% if @routes_inspector %>
<h2>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb
index 5060da9369..324ef1567a 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb
@@ -11,10 +11,10 @@
</p>
<pre><code><%= h @exception.message %></code></pre>
- <%= render template: "rescues/_source" %>
+ <%= render "rescues/source", source_extracts: @source_extracts, show_source_idx: @show_source_idx %>
<p><%= @exception.sub_template_message %></p>
- <%= render template: "rescues/_trace" %>
+ <%= render "rescues/trace", traces: @traces, trace_to_show: @trace_to_show %>
<%= render template: "rescues/_request_and_response" %>
</div>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb b/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb
index 429ea7057c..0242b706b2 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb
@@ -17,6 +17,10 @@
line-height: 15px;
}
+ #route_table thead tr.bottom th input#search {
+ -webkit-appearance: textfield;
+ }
+
#route_table tbody tr {
border-bottom: 1px solid #ddd;
}
@@ -60,7 +64,7 @@
<%= link_to "Path", "#", 'data-route-helper' => '_path',
title: "Returns a relative path (without the http or domain)" %> /
<%= link_to "Url", "#", 'data-route-helper' => '_url',
- title: "Returns an absolute url (with the http and domain)" %>
+ title: "Returns an absolute URL (with the http and domain)" %>
</th>
<th><%# HTTP Verb %>
</th>
@@ -93,7 +97,7 @@
}
}
- // get JSON from url and invoke callback with result
+ // get JSON from URL and invoke callback with result
function getJSON(url, success) {
var xhr = new XMLHttpRequest();
xhr.open('GET', url);
@@ -193,4 +197,7 @@
setupMatchPaths();
setupRouteToggleHelperLinks();
+
+ // Focus the search input after page has loaded
+ document.getElementById('search').focus();
</script>
diff --git a/actionpack/lib/action_dispatch/railtie.rb b/actionpack/lib/action_dispatch/railtie.rb
index e9e6a2e597..efc3988bc3 100644
--- a/actionpack/lib/action_dispatch/railtie.rb
+++ b/actionpack/lib/action_dispatch/railtie.rb
@@ -1,4 +1,7 @@
+# frozen_string_literal: true
+
require "action_dispatch"
+require "active_support/messages/rotation_configuration"
module ActionDispatch
class Railtie < Rails::Railtie # :nodoc:
@@ -8,22 +11,30 @@ module ActionDispatch
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.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.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.default_headers = {
- 'X-Frame-Options' => 'SAMEORIGIN',
- 'X-XSS-Protection' => '1; mode=block',
- 'X-Content-Type-Options' => 'nosniff'
+ "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|
@@ -39,8 +50,6 @@ module ActionDispatch
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::Reloader.default_reloader = app.reloader
-
ActionDispatch.test_app = app
end
end
diff --git a/actionpack/lib/action_dispatch/request/session.rb b/actionpack/lib/action_dispatch/request/session.rb
index 42890225fa..bc5e0670e0 100644
--- a/actionpack/lib/action_dispatch/request/session.rb
+++ b/actionpack/lib/action_dispatch/request/session.rb
@@ -1,4 +1,6 @@
-require 'rack/session/abstract/id'
+# frozen_string_literal: true
+
+require "rack/session/abstract/id"
module ActionDispatch
class Request
@@ -7,10 +9,10 @@ module ActionDispatch
ENV_SESSION_KEY = Rack::RACK_SESSION # :nodoc:
ENV_SESSION_OPTIONS_KEY = Rack::RACK_SESSION_OPTIONS # :nodoc:
- # Singleton object used to determine if an optional param wasn't specified
+ # Singleton object used to determine if an optional param wasn't specified.
Unspecified = Object.new
-
- # Creates a session hash, merging the properties of the previous session if any
+
+ # Creates a session hash, merging the properties of the previous session if any.
def self.create(store, req, default_options)
session_was = find req
session = Request::Session.new(store, req)
@@ -53,7 +55,7 @@ module ActionDispatch
}
end
- def []=(k,v); @delegate[k] = v; end
+ def []=(k, v); @delegate[k] = v; end
def to_hash; @delegate.dup; end
def values_at(*args); @delegate.values_at(*args); end
end
@@ -63,7 +65,7 @@ module ActionDispatch
@req = req
@delegate = {}
@loaded = false
- @exists = nil # we haven't checked yet
+ @exists = nil # We haven't checked yet.
end
def id
@@ -79,18 +81,26 @@ module ActionDispatch
options = self.options || {}
@by.send(:delete_session, @req, options.id(@req), options)
- # Load the new sid to be written with the response
+ # 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.
+ # +nil+ if the given key is not found in the session.
def [](key)
load_for_read!
@delegate[key.to_s]
end
+ # Returns the nested value specified by the sequence of keys, returning
+ # +nil+ if any intermediate step is +nil+.
+ 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.
def has_key?(key)
load_for_read!
@@ -101,11 +111,13 @@ module ActionDispatch
# Returns keys of the session as Array.
def keys
+ load_for_read!
@delegate.keys
end
# Returns values of the session as Array.
def values
+ load_for_read!
@delegate.values
end
@@ -124,8 +136,9 @@ module ActionDispatch
# Returns the session as Hash.
def to_hash
load_for_read!
- @delegate.dup.delete_if { |_,v| v.nil? }
+ @delegate.dup.delete_if { |_, v| v.nil? }
end
+ alias :to_h :to_hash
# Updates the session with given Hash.
#
@@ -162,7 +175,7 @@ module ActionDispatch
# :bar
# end
# # => :bar
- def fetch(key, default=Unspecified, &block)
+ def fetch(key, default = Unspecified, &block)
load_for_read!
if default == Unspecified
@delegate.fetch(key.to_s, &block)
@@ -198,28 +211,32 @@ module ActionDispatch
@delegate.merge!(other)
end
+ def each(&block)
+ to_hash.each(&block)
+ end
+
private
- def load_for_read!
- load! if !loaded? && exists?
- end
+ def load_for_read!
+ load! if !loaded? && exists?
+ end
- def load_for_write!
- load! unless loaded?
- end
+ def load_for_write!
+ load! unless loaded?
+ end
- def load!
- id, session = @by.load_session @req
- options[:id] = id
- @delegate.replace(stringify_keys(session))
- @loaded = true
- end
+ def load!
+ id, session = @by.load_session @req
+ options[:id] = id
+ @delegate.replace(stringify_keys(session))
+ @loaded = true
+ end
- def stringify_keys(other)
- other.each_with_object({}) { |(key, value), hash|
- hash[key.to_s] = value
- }
- end
+ def stringify_keys(other)
+ other.each_with_object({}) { |(key, value), hash|
+ hash[key.to_s] = value
+ }
+ end
end
end
end
diff --git a/actionpack/lib/action_dispatch/request/utils.rb b/actionpack/lib/action_dispatch/request/utils.rb
index bb3df3c311..fb0efb9a58 100644
--- a/actionpack/lib/action_dispatch/request/utils.rb
+++ b/actionpack/lib/action_dispatch/request/utils.rb
@@ -1,9 +1,22 @@
+# 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
- mattr_accessor :perform_deep_munge
- self.perform_deep_munge = 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
@@ -23,14 +36,13 @@ module ActionDispatch
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, "Non UTF-8 value: #{params}"
+ 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
@@ -53,7 +65,7 @@ module ActionDispatch
end
end
- # Remove nils from the params hash
+ # Remove nils from the params hash.
class NoNilParamEncoder < ParamEncoder # :nodoc:
def self.handle_array(params)
list = super
@@ -64,4 +76,3 @@ module ActionDispatch
end
end
end
-
diff --git a/actionpack/lib/action_dispatch/routing.rb b/actionpack/lib/action_dispatch/routing.rb
index 79d2f1f13c..d78b1c4f71 100644
--- a/actionpack/lib/action_dispatch/routing.rb
+++ b/actionpack/lib/action_dispatch/routing.rb
@@ -1,3 +1,7 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/string/filters"
+
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
@@ -70,8 +74,8 @@ module ActionDispatch
# 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' => 'posts#show'
- # post 'post/:id' => 'posts#create_comment'
+ # 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.
@@ -79,7 +83,7 @@ module ActionDispatch
# 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' => 'posts#show', via: [:get, :post]
+ # match 'post/:id', to: 'posts#show', via: [:get, :post]
#
# == Named routes
#
@@ -89,8 +93,8 @@ module ActionDispatch
#
# Example:
#
- # # In routes.rb
- # get '/login' => 'accounts#login', as: 'login'
+ # # In config/routes.rb
+ # get '/login', to: 'accounts#login', as: 'login'
#
# # With render, redirect_to, tests, etc.
# redirect_to login_url
@@ -101,7 +105,7 @@ module ActionDispatch
#
# Use <tt>root</tt> as a shorthand to name a route for the root path "/".
#
- # # In routes.rb
+ # # In config/routes.rb
# root to: 'blogs#index'
#
# # would recognize http://www.example.com/ as
@@ -114,21 +118,21 @@ module ActionDispatch
# Note: when using +controller+, the route is simply named after the
# method you call on the block parameter rather than map.
#
- # # In routes.rb
+ # # In config/routes.rb
# controller :blog do
- # get 'blog/show' => :list
- # get 'blog/delete' => :delete
- # get 'blog/edit/:id' => :edit
+ # 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, show_path(id: @article.id)
+ # link_to @article.title, blog_show_path(id: @article.id)
#
# == Pretty URLs
#
# Routes can generate pretty URLs. For example:
#
- # get '/articles/:year/:month/:day' => 'articles#find_by_id', constraints: {
+ # get '/articles/:year/:month/:day', to: 'articles#find_by_id', constraints: {
# year: /\d{4}/,
# month: /\d{1,2}/,
# day: /\d{1,2}/
@@ -143,7 +147,7 @@ module ActionDispatch
# You can specify a regular expression to define a format for a parameter.
#
# controller 'geocode' do
- # get 'geocode/:postalcode' => :show, constraints: {
+ # get 'geocode/:postalcode', to: :show, constraints: {
# postalcode: /\d{5}(-\d{4})?/
# }
# end
@@ -152,13 +156,13 @@ module ActionDispatch
# expression modifiers:
#
# controller 'geocode' do
- # get 'geocode/:postalcode' => :show, constraints: {
+ # get 'geocode/:postalcode', to: :show, constraints: {
# postalcode: /hx\d\d\s\d[a-z]{2}/i
# }
# end
#
# controller 'geocode' do
- # get 'geocode/:postalcode' => :show, constraints: {
+ # get 'geocode/:postalcode', to: :show, constraints: {
# postalcode: /# Postalcode format
# \d{5} #Prefix
# (-\d{4})? #Suffix
@@ -174,13 +178,13 @@ module ActionDispatch
#
# You can redirect any path to another path using the redirect helper in your router:
#
- # get "/stories" => redirect("/posts")
+ # get "/stories", to: redirect("/posts")
#
# == Unicode character routes
#
# You can specify unicode character routes in your router:
#
- # get "こんにちは" => "welcome#index"
+ # get "こんにちは", to: "welcome#index"
#
# == Routing to Rack Applications
#
@@ -188,7 +192,7 @@ module ActionDispatch
# index action in the PostsController, you can specify any Rack application
# as the endpoint for a matcher:
#
- # get "/application.js" => Sprockets
+ # get "/application.js", to: Sprockets
#
# == Reloading routes
#
@@ -196,7 +200,7 @@ module ActionDispatch
#
# Rails.application.reload_routes!
#
- # This will clear all named routes and reload routes.rb if the file has been modified from
+ # 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
@@ -206,8 +210,8 @@ module ActionDispatch
# === +assert_routing+
#
# def test_movie_route_properly_splits
- # opts = {controller: "plugin", action: "checkout", id: "2"}
- # assert_routing "plugin/checkout/2", opts
+ # 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.
@@ -215,8 +219,8 @@ module ActionDispatch
# === +assert_recognizes+
#
# def test_route_has_options
- # opts = {controller: "plugin", action: "show", id: "12"}
- # assert_recognizes opts, "/plugins/show/12"
+ # 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
@@ -239,8 +243,9 @@ module ActionDispatch
#
# rails routes
#
- # Target specific controllers by prefixing the command with <tt>-c</tt> option.
- #
+ # 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.
module Routing
extend ActiveSupport::Autoload
diff --git a/actionpack/lib/action_dispatch/routing/endpoint.rb b/actionpack/lib/action_dispatch/routing/endpoint.rb
index 88aa13c3e8..28bb20d688 100644
--- a/actionpack/lib/action_dispatch/routing/endpoint.rb
+++ b/actionpack/lib/action_dispatch/routing/endpoint.rb
@@ -1,10 +1,17 @@
+# frozen_string_literal: true
+
module ActionDispatch
module Routing
class Endpoint # :nodoc:
def dispatcher?; false; end
def redirect?; false; end
- def matches?(req); true; end
- def app; self; end
+ def matches?(req); true; end
+ def app; self; end
+ def rack_app; app; end
+
+ def engine?
+ rack_app.is_a?(Class) && rack_app < Rails::Engine
+ end
end
end
end
diff --git a/actionpack/lib/action_dispatch/routing/inspector.rb b/actionpack/lib/action_dispatch/routing/inspector.rb
index 5d30a545a2..413e524ef6 100644
--- a/actionpack/lib/action_dispatch/routing/inspector.rb
+++ b/actionpack/lib/action_dispatch/routing/inspector.rb
@@ -1,5 +1,7 @@
-require 'delegate'
-require 'active_support/core_ext/string/strip'
+# frozen_string_literal: true
+
+require "delegate"
+require "io/console/size"
module ActionDispatch
module Routing
@@ -13,7 +15,7 @@ module ActionDispatch
end
def rack_app
- app.app
+ app.rack_app
end
def path
@@ -33,11 +35,11 @@ module ActionDispatch
end
def controller
- requirements[:controller] || ':controller'
+ parts.include?(:controller) ? ":controller" : requirements[:controller]
end
def action
- requirements[:action] || ':action'
+ parts.include?(:action) ? ":action" : requirements[:action]
end
def internal?
@@ -45,7 +47,7 @@ module ActionDispatch
end
def engine?
- rack_app.respond_to?(:routes)
+ app.engine?
end
end
@@ -59,11 +61,11 @@ module ActionDispatch
@routes = routes
end
- def format(formatter, filter = nil)
+ 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))
+ formatter.no_routes(collect_routes(@routes), filter)
return formatter.result
end
@@ -79,107 +81,156 @@ module ActionDispatch
end
private
-
- def normalize_filter(filter)
- if filter.is_a?(Hash) && filter[:controller]
- { controller: /#{filter[:controller].downcase.sub(/_?controller\z/, '').sub('::', '/')}/ }
- elsif filter
- { controller: /#{filter}/, action: /#{filter}/, verb: /#{filter}/, name: /#{filter}/, path: /#{filter}/ }
+ 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
- end
- def filter_routes(filter)
- if filter
- @routes.select do |route|
- route_wrapper = RouteWrapper.new(route)
- filter.any? { |default, value| route_wrapper.send(default) =~ value }
+ def filter_routes(filter)
+ if filter
+ @routes.select do |route|
+ route_wrapper = RouteWrapper.new(route)
+ filter.any? { |default, value| route_wrapper.send(default) =~ value }
+ end
+ else
+ @routes
end
- else
- @routes
end
- end
- def collect_routes(routes)
- routes.collect do |route|
- RouteWrapper.new(route)
- end.reject(&:internal?).collect do |route|
- collect_engine_routes(route)
+ 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 }
+ { name: route.name,
+ verb: route.verb,
+ path: route.path,
+ reqs: route.reqs }
+ end
end
- end
- def collect_engine_routes(route)
- name = route.endpoint
- return unless route.engine?
- return if @engines[name]
+ 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)
+ routes = route.rack_app.routes
+ if routes.is_a?(ActionDispatch::Routing::RouteSet)
+ @engines[name] = collect_routes(routes.routes)
+ end
end
- end
end
- class ConsoleFormatter
- def initialize
- @buffer = []
- end
+ module ConsoleFormatter
+ class Base
+ def initialize
+ @buffer = []
+ end
- def result
- @buffer.join("\n")
- end
+ def result
+ @buffer.join("\n")
+ end
- def section_title(title)
- @buffer << "\n#{title}:"
- end
+ def section_title(title)
+ end
- def section(routes)
- @buffer << draw_section(routes)
- end
+ def section(routes)
+ end
- def header(routes)
- @buffer << draw_header(routes)
+ def header(routes)
+ end
+
+ 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
- def no_routes(routes)
- @buffer <<
- if routes.none?
- <<-MESSAGE.strip_heredoc
- You don't have any routes defined!
+ class Sheet < Base
+ def section_title(title)
+ @buffer << "\n#{title}:"
+ end
- Please add some routes in config/routes.rb.
- MESSAGE
- else
- "No routes were found for this controller"
+ def section(routes)
+ @buffer << draw_section(routes)
end
- @buffer << "For more information about routes, see the Rails guide: http://guides.rubyonrails.org/routing.html."
- end
- private
- 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)
+ def header(routes)
+ @buffer << draw_header(routes)
+ end
+
+ private
- routes.map do |r|
- "#{r[:name].rjust(name_width)} #{r[:verb].ljust(verb_width)} #{r[:path].ljust(path_width)} #{r[:reqs]}"
+ 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
- end
- def draw_header(routes)
- name_width, verb_width, path_width = widths(routes)
+ 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"
+ "#{"Prefix".rjust(name_width)} #{"Verb".ljust(verb_width)} #{"URI Pattern".ljust(path_width)} Controller#Action"
+ end
+
+ 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
+
+ class Expanded < Base
+ def section_title(title)
+ @buffer << "\n#{"[ #{title} ]"}"
end
- 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]
+ def section(routes)
+ @buffer << draw_expanded_section(routes)
end
+
+ private
+
+ 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
+
+ def route_header(index:)
+ console_width = IO.console_size.second
+ header_prefix = "--[ Route #{index} ]"
+ dash_remainder = [console_width - header_prefix.size, 0].max
+
+ "#{header_prefix}#{'-' * dash_remainder}"
+ end
+ end
end
class HtmlTableFormatter
@@ -196,21 +247,21 @@ module ActionDispatch
@buffer << @view.render(partial: "routes/route", collection: routes)
end
- # the header is part of the HTML page, so we don't construct it here.
+ # The header is part of the HTML page, so we don't construct it here.
def header(routes)
end
def no_routes(*)
- @buffer << <<-MESSAGE.strip_heredoc
+ @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="http://guides.rubyonrails.org/routing.html">Rails Routing from the Outside In</a>.
+ <a href="https://guides.rubyonrails.org/routing.html">Rails Routing from the Outside In</a>.
</li>
</ul>
- MESSAGE
+ MESSAGE
end
def result
diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb
index 81ffea4d5d..08560ccf7d 100644
--- a/actionpack/lib/action_dispatch/routing/mapper.rb
+++ b/actionpack/lib/action_dispatch/routing/mapper.rb
@@ -1,10 +1,11 @@
-require 'active_support/core_ext/hash/reverse_merge'
-require 'active_support/core_ext/hash/slice'
-require 'active_support/core_ext/enumerable'
-require 'active_support/core_ext/array/extract_options'
-require 'active_support/core_ext/regexp'
-require 'action_dispatch/routing/redirection'
-require 'action_dispatch/routing/endpoint'
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/slice"
+require "active_support/core_ext/enumerable"
+require "active_support/core_ext/array/extract_options"
+require "active_support/core_ext/regexp"
+require "action_dispatch/routing/redirection"
+require "action_dispatch/routing/endpoint"
module ActionDispatch
module Routing
@@ -18,9 +19,9 @@ module ActionDispatch
CALL = ->(app, req) { app.call req.env }
def initialize(app, constraints, strategy)
- # Unwrap Constraints objects. I don't actually think it's possible
+ # 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
+ # multiple places that kept testing children of this object. I
# *think* they were just being defensive, but I have no idea.
if app.is_a?(self.class)
constraints += app.constraints
@@ -42,19 +43,32 @@ module ActionDispatch
end
def serve(req)
- return [ 404, {'X-Cascade' => 'pass'}, [] ] unless matches?(req)
+ return [ 404, { "X-Cascade" => "pass" }, [] ] unless matches?(req)
@strategy.call @app, req
end
private
def constraint_args(constraint, request)
- constraint.arity == 1 ? [request] : [request.path_parameters, 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
class Mapping #:nodoc:
ANCHOR_CHARACTERS_REGEX = %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z}
+ OPTIONAL_FORMAT_REGEX = %r{(?:\(\.:format\)+|\.:format|/)\Z}
attr_reader :requirements, :defaults
attr_reader :to, :default_controller, :default_action
@@ -94,7 +108,7 @@ module ActionDispatch
end
def self.optional_format?(path, format)
- format != false && !path.include?(':format') && !path.end_with?('/')
+ format != false && path !~ OPTIONAL_FORMAT_REGEX
end
def initialize(set, ast, defaults, controller, default_action, modyoule, to, formatted, scope_constraints, blocks, via, options_constraints, anchor, options)
@@ -107,7 +121,7 @@ module ActionDispatch
@ast = ast
@anchor = anchor
@via = via
- @internal = options[:internal]
+ @internal = options.delete(:internal)
path_params = ast.find_all(&:symbol?).map(&:to_sym)
@@ -121,7 +135,7 @@ module ActionDispatch
if options_constraints.is_a?(Hash)
@defaults = Hash[options_constraints.find_all { |key, default|
- URL_OPTIONS.include?(key) && (String === default || Fixnum === default)
+ URL_OPTIONS.include?(key) && (String === default || Integer === default)
}].merge @defaults
@blocks = blocks
constraints.merge! options_constraints
@@ -138,21 +152,16 @@ module ActionDispatch
@conditions = Hash[conditions]
@defaults = formats[:defaults].merge(@defaults).merge(normalize_defaults(options))
+ if path_params.include?(:action) && !@requirements.key?(:action)
+ @defaults[:action] ||= "index"
+ end
+
@required_defaults = (split_options[:required_defaults] || []).map(&:first)
end
def make_route(name, precedence)
- route = Journey::Route.new(name,
- application,
- path,
- conditions,
- required_defaults,
- defaults,
- request_method,
- precedence,
- @internal)
-
- route
+ Journey::Route.new(name, application, path, conditions, required_defaults,
+ defaults, request_method, precedence, @internal)
end
def application
@@ -215,7 +224,7 @@ module ActionDispatch
private
def add_wildcard_options(options, formatted, path_ast)
# Add a constraint for wildcard route to make it non-greedy and match the
- # optional format part of the route by default
+ # optional format part of the route by default.
if formatted != false
path_ast.grep(Journey::Nodes::Star).each_with_object({}) { |node, hash|
hash[node.name.to_sym] ||= /.+?/
@@ -236,7 +245,7 @@ module ActionDispatch
options[:controller] ||= /.+?/
end
- if to.respond_to? :call
+ if to.respond_to?(:action) || to.respond_to?(:call)
options
else
to_endpoint = split_to to
@@ -267,13 +276,13 @@ module ActionDispatch
{ requirements: { format: Regexp.compile(formatted) },
defaults: { format: formatted } }
else
- { requirements: { }, defaults: { } }
+ { requirements: {}, defaults: {} }
end
end
def verify_regexp_requirements(requirements)
requirements.each do |requirement|
- if requirement.source =~ ANCHOR_CHARACTERS_REGEX
+ if ANCHOR_CHARACTERS_REGEX.match?(requirement.source)
raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}"
end
@@ -288,24 +297,22 @@ module ActionDispatch
end
def app(blocks)
- if to.is_a?(Class) && to < ActionController::Metal
+ if to.respond_to?(:action)
Routing::RouteSet::StaticDispatcher.new to
+ elsif to.respond_to?(:call)
+ Constraints.new(to, blocks, Constraints::CALL)
+ elsif blocks.any?
+ Constraints.new(dispatcher(defaults.key?(:controller)), blocks, Constraints::SERVE)
else
- if to.respond_to?(:call)
- Constraints.new(to, blocks, Constraints::CALL)
- elsif blocks.any?
- Constraints.new(dispatcher(defaults.key?(:controller)), blocks, Constraints::SERVE)
- else
- dispatcher(defaults.key?(:controller))
- end
+ dispatcher(defaults.key?(:controller))
end
end
def check_controller_and_action(path_params, controller, action)
hash = check_part(:controller, controller, path_params, {}) do |part|
translate_controller(part) {
- message = "'#{part}' is not a supported controller name. This can lead to potential routing problems."
- message << " See http://guides.rubyonrails.org/routing.html#specifying-a-controller-to-use"
+ 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
}
@@ -329,8 +336,8 @@ module ActionDispatch
end
def split_to(to)
- if to =~ /#/
- to.split('#')
+ if /#/.match?(to)
+ to.split("#")
else
[]
end
@@ -338,7 +345,7 @@ module ActionDispatch
def add_controller_module(controller, modyoule)
if modyoule && !controller.is_a?(Regexp)
- if controller =~ %r{\A/}
+ if %r{\A/}.match?(controller)
controller[1..-1]
else
[modyoule, controller].compact.join("/")
@@ -386,7 +393,7 @@ module ActionDispatch
# for root cases, where the latter is the correct one.
def self.normalize_path(path)
path = Journey::Router::Utils.normalize_path(path)
- path.gsub!(%r{/(\(+)/?}, '\1/') unless path =~ %r{^/\(+[^)]+\)$}
+ path.gsub!(%r{/(\(+)/?}, '\1/') unless path =~ %r{^/(\(+[^)]+\)){1,}$}
path
end
@@ -395,7 +402,7 @@ module ActionDispatch
end
module Base
- # Matches a url pattern to one or more routes.
+ # Matches a URL pattern to one or more routes.
#
# You should not use the +match+ method in your router
# without specifying an HTTP method.
@@ -405,7 +412,7 @@ module ActionDispatch
# # sets :controller, :action and :id in params
# match ':controller/:action/:id', via: [:get, :post]
#
- # Note that +:controller+, +:action+ and +:id+ are interpreted as url
+ # 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:
@@ -454,7 +461,7 @@ module ActionDispatch
#
# === Options
#
- # Any options not seen here are passed on as params with the url.
+ # Any options not seen here are passed on as params with the URL.
#
# [:controller]
# The route's controller.
@@ -469,7 +476,17 @@ module ActionDispatch
# <tt>params[<:param>]</tt>.
# In your router:
#
- # resources :user, param: :name
+ # 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:
@@ -480,8 +497,8 @@ module ActionDispatch
# end
# end
#
- # user = User.find_by(name: 'Phusion')
- # user_path(user) # => "/users/Phusion"
+ # user = User.find_by(name: 'Phusion')
+ # user_path(user) # => "/users/Phusion"
#
# [:path]
# The path prefix for the routes.
@@ -539,10 +556,10 @@ module ActionDispatch
#
# match 'json_only', constraints: { format: 'json' }, via: :get
#
- # class Whitelist
+ # class PermitList
# def matches?(request) request.remote_ip == '1.2.3.4' end
# end
- # match 'path', to: 'c#a', constraints: Whitelist.new, via: :get
+ # match 'path', to: 'c#a', constraints: PermitList.new, via: :get
#
# See <tt>Scoping#constraints</tt> for more examples with its scope
# equivalent.
@@ -565,7 +582,7 @@ module ActionDispatch
# [:format]
# Allows you to specify the default value for optional +format+
# segment or disable it by supplying +false+.
- def match(path, options=nil)
+ def match(path, options = nil)
end
# Mount a Rack-based application to be used within the application.
@@ -597,7 +614,7 @@ module ActionDispatch
end
raise ArgumentError, "A rack application must be specified" unless app.respond_to?(:call)
- raise ArgumentError, <<-MSG.strip_heredoc unless path
+ raise ArgumentError, <<~MSG unless path
Must be called with mount point
mount SomeRackApp, at: "some_route"
@@ -611,7 +628,7 @@ module ActionDispatch
target_as = name_for_action(options[:as], path)
options[:via] ||= :all
- match(path, options.merge(:to => app, :anchor => false, :format => false))
+ match(path, options.merge(to: app, anchor: false, format: false))
define_generate_prefix(app, target_as) if rails_app
self
@@ -630,7 +647,7 @@ module ActionDispatch
# Query if the following named route was already defined.
def has_named_route?(name)
- @set.named_routes.key? name
+ @set.named_routes.key?(name)
end
private
@@ -650,18 +667,31 @@ module ActionDispatch
def define_generate_prefix(app, name)
_route = @set.named_routes.get name
_routes = @set
- app.routes.define_mounted_helper(name)
+ _url_helpers = @set.url_helpers
+
+ 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
+
+ app.routes.define_mounted_helper(name, script_namer)
+
app.routes.extend Module.new {
def optimize_routes_generation?; false; end
+
define_method :find_script_name do |options|
if options.key? :script_name
super(options)
else
- prefix_options = options.slice(*_route.segment_keys)
- prefix_options[:relative_url_root] = ''.freeze
- # we must actually delete prefix segment keys to avoid passing them to next url_for
- _route.segment_keys.each { |k| options.delete(k) }
- _routes.url_helpers.send("#{name}_path", prefix_options)
+ script_namer.call(options)
end
end
}
@@ -713,11 +743,7 @@ module ActionDispatch
def map_method(method, args, &block)
options = args.extract_options!
options[:via] = method
- if options.key?(:defaults)
- defaults(options.delete(:defaults)) { match(*args, options, &block) }
- else
- match(*args, options, &block)
- end
+ match(*args, options, &block)
self
end
end
@@ -811,7 +837,7 @@ module ActionDispatch
options = args.extract_options!.dup
scope = {}
- options[:path] = args.flatten.join('/') if args.any?
+ options[:path] = args.flatten.join("/") if args.any?
options[:constraints] ||= {}
unless nested_scope?
@@ -821,10 +847,10 @@ module ActionDispatch
if options[:constraints].is_a?(Hash)
defaults = options[:constraints].select do |k, v|
- URL_OPTIONS.include?(k) && (v.is_a?(String) || v.is_a?(Fixnum))
+ URL_OPTIONS.include?(k) && (v.is_a?(String) || v.is_a?(Integer))
end
- (options[:defaults] ||= {}).reverse_merge!(defaults)
+ options[:defaults] = defaults.merge(options[:defaults] || {})
else
block, options[:constraints] = options[:constraints], {}
end
@@ -835,7 +861,7 @@ module ActionDispatch
end
if options.key? :anchor
- raise ArgumentError, 'anchor is ignored unless passed to `match`'
+ raise ArgumentError, "anchor is ignored unless passed to `match`"
end
@scope.options.each do |option|
@@ -982,7 +1008,7 @@ module ActionDispatch
# resources :iphones
# end
def constraints(constraints = {})
- scope(:constraints => constraints) { yield }
+ scope(constraints: constraints) { yield }
end
# Allows you to set default parameters for a route, such as this:
@@ -998,67 +1024,71 @@ module ActionDispatch
end
private
- def merge_path_scope(parent, child) #:nodoc:
+ def merge_path_scope(parent, child)
Mapper.normalize_path("#{parent}/#{child}")
end
- def merge_shallow_path_scope(parent, child) #:nodoc:
+ def merge_shallow_path_scope(parent, child)
Mapper.normalize_path("#{parent}/#{child}")
end
- def merge_as_scope(parent, child) #:nodoc:
+ def merge_as_scope(parent, child)
parent ? "#{parent}_#{child}" : child
end
- def merge_shallow_prefix_scope(parent, child) #:nodoc:
+ def merge_shallow_prefix_scope(parent, child)
parent ? "#{parent}_#{child}" : child
end
- def merge_module_scope(parent, child) #:nodoc:
+ def merge_module_scope(parent, child)
parent ? "#{parent}/#{child}" : child
end
- def merge_controller_scope(parent, child) #:nodoc:
+ def merge_controller_scope(parent, child)
child
end
- def merge_action_scope(parent, child) #:nodoc:
+ def merge_action_scope(parent, child)
child
end
- def merge_via_scope(parent, child) #:nodoc:
+ def merge_via_scope(parent, child)
child
end
- def merge_format_scope(parent, child) #:nodoc:
+ def merge_format_scope(parent, child)
child
end
- def merge_path_names_scope(parent, child) #:nodoc:
+ def merge_path_names_scope(parent, child)
merge_options_scope(parent, child)
end
- def merge_constraints_scope(parent, child) #:nodoc:
+ def merge_constraints_scope(parent, child)
merge_options_scope(parent, child)
end
- def merge_defaults_scope(parent, child) #:nodoc:
+ def merge_defaults_scope(parent, child)
merge_options_scope(parent, child)
end
- def merge_blocks_scope(parent, child) #:nodoc:
+ def merge_blocks_scope(parent, child)
merged = parent ? parent.dup : []
merged << child if child
merged
end
- def merge_options_scope(parent, child) #:nodoc:
+ def merge_options_scope(parent, child)
(parent || {}).merge(child)
end
- def merge_shallow_scope(parent, child) #:nodoc:
+ def merge_shallow_scope(parent, child)
child ? true : false
end
+
+ def merge_to_scope(parent, child)
+ child
+ end
end
# Resource routing allows you to quickly declare all of the common routes
@@ -1111,6 +1141,10 @@ module ActionDispatch
attr_reader :controller, :path, :param
def initialize(entities, api_only, shallow, options = {})
+ if options[:param].to_s.include?(":")
+ raise ArgumentError, ":param option can't contain colons"
+ end
+
@name = entities.to_s
@path = (options[:path] || @name).to_s
@controller = (options[:controller] || @name).to_s
@@ -1132,10 +1166,16 @@ module ActionDispatch
end
def actions
+ if @except
+ available_actions - Array(@except).map(&:to_sym)
+ else
+ available_actions
+ end
+ end
+
+ def available_actions
if @only
Array(@only).map(&:to_sym)
- elsif @except
- default_actions - Array(@except).map(&:to_sym)
else
default_actions
end
@@ -1237,19 +1277,19 @@ module ActionDispatch
#
# resource :profile
#
- # creates six different routes in your application, all mapping to
+ # 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
- # POST /profile
# GET /profile
# GET /profile/edit
# PATCH/PUT /profile
# DELETE /profile
+ # POST /profile
#
# === Options
- # Takes same options as +resources+.
+ # Takes same options as resources[rdoc-ref:#resources]
def resource(*resources, &block)
options = resources.extract_options!.dup
@@ -1264,15 +1304,15 @@ module ActionDispatch
concerns(options[:concerns]) if options[:concerns]
- collection do
- post :create
- end if parent_resource.actions.include?(:create)
-
new do
get :new
end if parent_resource.actions.include?(:new)
set_member_mappings_for_resource
+
+ collection do
+ post :create
+ end if parent_resource.actions.include?(:create)
end
end
@@ -1314,7 +1354,7 @@ module ActionDispatch
# DELETE /photos/:photo_id/comments/:id
#
# === Options
- # Takes same options as <tt>Base#match</tt> as well as:
+ # 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.
@@ -1322,14 +1362,14 @@ module ActionDispatch
#
# resources :posts, path_names: { new: "brand_new" }
#
- # The above example will now change /posts/new to /posts/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
+ # The resource and all segments will now route to /postings instead of /posts.
#
# [:only]
# Only generate routes for the given actions.
@@ -1406,6 +1446,9 @@ module ActionDispatch
# 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>
@@ -1526,7 +1569,7 @@ module ActionDispatch
end
end
- # See ActionDispatch::Routing::Mapper::Scoping#namespace
+ # See ActionDispatch::Routing::Mapper::Scoping#namespace.
def namespace(path, options = {})
if resource_scope?
nested { super }
@@ -1546,22 +1589,24 @@ module ActionDispatch
!parent_resource.singleton? && @scope[:shallow]
end
- # Matches a url pattern to one or more routes.
+ # 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' => 'controller#action', via: :patch
# match 'path', to: 'controller#action', via: :post
# match 'path', 'otherpath', on: :member, via: :get
- def match(path, *rest)
+ def match(path, *rest, &block)
if rest.empty? && Hash === path
options = path
path, to = options.find { |name, _value| name.is_a?(String) }
+ raise ArgumentError, "Route path not specified" if path.nil?
+
case to
when Symbol
options[:action] = to
when String
- if to =~ /#/
+ if /#/.match?(to)
options[:to] = to
else
options[:controller] = to
@@ -1577,110 +1622,13 @@ module ActionDispatch
paths = [path] + rest
end
- if options[:on] && !VALID_ON_OPTIONS.include?(options[:on])
- raise ArgumentError, "Unknown scope #{on.inspect} given to :on"
- end
-
- if @scope[:controller] && @scope[:action]
- options[:to] ||= "#{@scope[:controller]}##{@scope[:action]}"
- end
-
- controller = options.delete(:controller) || @scope[:controller]
- option_path = options.delete :path
- to = options.delete :to
- via = Mapping.check_via Array(options.delete(:via) {
- @scope[:via]
- })
- formatted = options.delete(:format) { @scope[:format] }
- anchor = options.delete(:anchor) { true }
- options_constraints = options.delete(:constraints) || {}
-
- path_types = paths.group_by(&:class)
- path_types.fetch(String, []).each do |_path|
- route_options = options.dup
- if _path && option_path
- ActiveSupport::Deprecation.warn <<-eowarn
-Specifying strings for both :path and the route path is deprecated. Change things like this:
-
- match #{_path.inspect}, :path => #{option_path.inspect}
-
-to this:
-
- match #{option_path.inspect}, :as => #{_path.inspect}, :action => #{path.inspect}
- eowarn
- route_options[:action] = _path
- route_options[:as] = _path
- _path = option_path
- end
- to = get_to_from_path(_path, to, route_options[:action])
- decomposed_match(_path, controller, route_options, _path, to, via, formatted, anchor, options_constraints)
- end
-
- path_types.fetch(Symbol, []).each do |action|
- route_options = options.dup
- decomposed_match(action, controller, route_options, option_path, to, via, formatted, anchor, options_constraints)
- end
-
- self
- end
-
- def get_to_from_path(path, to, action)
- return to if to || action
-
- path_without_format = path.sub(/\(\.:format\)$/, '')
- if using_match_shorthand?(path_without_format)
- path_without_format.gsub(%r{^/}, "").sub(%r{/([^/]*)$}, '#\1').tr("-", "_")
+ if options.key?(:defaults)
+ defaults(options.delete(:defaults)) { map_match(paths, options, &block) }
else
- nil
+ map_match(paths, options, &block)
end
end
- def using_match_shorthand?(path)
- path =~ %r{^/?[-\w]+/[-\w/]+$}
- end
-
- def decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) # :nodoc:
- if on = options.delete(:on)
- send(on) { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) }
- else
- case @scope.scope_level
- when :resources
- 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
- add_route(path, controller, options, _path, to, via, formatted, anchor, options_constraints)
- end
- end
- end
-
- def add_route(action, controller, options, _path, to, via, formatted, anchor, options_constraints) # :nodoc:
- path = path_for_action(action, _path)
- raise ArgumentError, "path is required" if path.blank?
-
- action = action.to_s
-
- default_action = options.delete(:action) || @scope[:action]
-
- if action =~ /^[\w\-\/]+$/
- default_action ||= action.tr('-', '_') unless action.include?("/")
- else
- action = nil
- end
-
- as = if !options.fetch(:as, true) # if it's set to nil or false
- options.delete(:as)
- else
- name_for_action(options.delete(:as), action)
- end
-
- path = Mapping.normalize_path URI.parser.escape(path), formatted
- ast = Journey::Parser.parse path
-
- mapping = Mapping.build(@scope, @set, ast, controller, default_action, to, via, formatted, options_constraints, anchor, options)
- @set.add_route(mapping, ast, as, anchor)
- end
-
# You can specify what Rails should route "/" to with the root method:
#
# root to: 'pages#main'
@@ -1697,7 +1645,7 @@ to this:
def root(path, options = {})
if path.is_a?(String)
options[:to] = path
- elsif path.is_a?(Hash) and options.empty?
+ elsif path.is_a?(Hash) && options.empty?
options = path
else
raise ArgumentError, "must be called with a path and/or options"
@@ -1714,13 +1662,13 @@ to this:
end
end
- protected
+ private
- def parent_resource #:nodoc:
+ def parent_resource
@scope[:scope_level_resource]
end
- def apply_common_behavior_for(method, resources, options, &block) #:nodoc:
+ def apply_common_behavior_for(method, resources, options, &block)
if resources.length > 1
resources.each { |r| send(method, r, options, &block) }
return true
@@ -1754,48 +1702,48 @@ to this:
false
end
- def apply_action_options(options) # :nodoc:
+ def apply_action_options(options)
return options if action_options? options
options.merge scope_action_options
end
- def action_options?(options) #:nodoc:
+ def action_options?(options)
options[:only] || options[:except]
end
- def scope_action_options #:nodoc:
+ def scope_action_options
@scope[:action_options] || {}
end
- def resource_scope? #:nodoc:
+ def resource_scope?
@scope.resource_scope?
end
- def resource_method_scope? #:nodoc:
+ def resource_method_scope?
@scope.resource_method_scope?
end
- def nested_scope? #:nodoc:
+ def nested_scope?
@scope.nested?
end
- def with_scope_level(kind)
+ def with_scope_level(kind) # :doc:
@scope = @scope.new_level(kind)
yield
ensure
@scope = @scope.parent
end
- def resource_scope(resource) #:nodoc:
- @scope = @scope.new(:scope_level_resource => resource)
+ def resource_scope(resource)
+ @scope = @scope.new(scope_level_resource: resource)
controller(resource.resource_scope) { yield }
ensure
@scope = @scope.parent
end
- def nested_options #:nodoc:
- options = { :as => parent_resource.member_name }
+ def nested_options
+ options = { as: parent_resource.member_name }
options[:constraints] = {
parent_resource.nested_param => param_constraint
} if param_constraint?
@@ -1803,27 +1751,27 @@ to this:
options
end
- def shallow_nesting_depth #:nodoc:
+ def shallow_nesting_depth
@scope.find_all { |node|
node.frame[:scope_level_resource]
}.count { |node| node.frame[:scope_level_resource].shallow? }
end
- def param_constraint? #:nodoc:
+ def param_constraint?
@scope[:constraints] && @scope[:constraints][parent_resource.param].is_a?(Regexp)
end
- def param_constraint #:nodoc:
+ def param_constraint
@scope[:constraints][parent_resource.param]
end
- def canonical_action?(action) #:nodoc:
+ def canonical_action?(action)
resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s)
end
- def shallow_scope #:nodoc:
- scope = { :as => @scope[:shallow_prefix],
- :path => @scope[:shallow_path] }
+ def shallow_scope
+ scope = { as: @scope[:shallow_prefix],
+ path: @scope[:shallow_path] }
@scope = @scope.new scope
yield
@@ -1831,7 +1779,7 @@ to this:
@scope = @scope.parent
end
- def path_for_action(action, path) #:nodoc:
+ def path_for_action(action, path)
return "#{@scope[:path]}/#{path}" if path
if canonical_action?(action)
@@ -1841,23 +1789,23 @@ to this:
end
end
- def action_path(name) #:nodoc:
+ def action_path(name)
@scope[:path_names][name.to_sym] || name
end
- def prefix_name_for_action(as, action) #:nodoc:
+ def prefix_name_for_action(as, action)
if as
prefix = as
elsif !canonical_action?(action)
prefix = action
end
- if prefix && prefix != '/' && !prefix.empty?
- Mapper.normalize_name prefix.to_s.tr('-', '_')
+ if prefix && prefix != "/" && !prefix.empty?
+ Mapper.normalize_name prefix.to_s.tr("-", "_")
end
end
- def name_for_action(as, action) #:nodoc:
+ def name_for_action(as, action)
prefix = prefix_name_for_action(as, action)
name_prefix = @scope[:as]
@@ -1869,7 +1817,7 @@ to this:
end
action_name = @scope.action_name(name_prefix, prefix, collection_name, member_name)
- candidate = action_name.select(&:present?).join('_')
+ candidate = action_name.select(&:present?).join("_")
unless candidate.empty?
# If a name was not explicitly given, we check if it is valid
@@ -1883,7 +1831,7 @@ to this:
end
end
- def set_member_mappings_for_resource
+ def set_member_mappings_for_resource # :doc:
member do
get :edit if parent_resource.actions.include?(:edit)
get :show if parent_resource.actions.include?(:show)
@@ -1895,22 +1843,119 @@ to this:
end
end
- def api_only?
+ def api_only? # :doc:
@set.api_only?
end
- private
- def path_scope(path)
- @scope = @scope.new(path: merge_path_scope(@scope[:path], path))
- yield
- ensure
- @scope = @scope.parent
- end
+ def path_scope(path)
+ @scope = @scope.new(path: merge_path_scope(@scope[:path], path))
+ yield
+ ensure
+ @scope = @scope.parent
+ end
- def match_root_route(options)
- name = has_named_route?(:root) ? nil : :root
- match '/', { :as => name, :via => :get }.merge!(options)
- end
+ def map_match(paths, options)
+ if options[:on] && !VALID_ON_OPTIONS.include?(options[:on])
+ raise ArgumentError, "Unknown scope #{on.inspect} given to :on"
+ end
+
+ if @scope[:to]
+ options[:to] ||= @scope[:to]
+ end
+
+ if @scope[:controller] && @scope[:action]
+ options[:to] ||= "#{@scope[:controller]}##{@scope[:action]}"
+ end
+
+ controller = options.delete(:controller) || @scope[:controller]
+ option_path = options.delete :path
+ to = options.delete :to
+ via = Mapping.check_via Array(options.delete(:via) {
+ @scope[:via]
+ })
+ formatted = options.delete(:format) { @scope[:format] }
+ anchor = options.delete(:anchor) { true }
+ options_constraints = options.delete(:constraints) || {}
+
+ path_types = paths.group_by(&:class)
+ path_types.fetch(String, []).each do |_path|
+ route_options = options.dup
+ if _path && option_path
+ raise ArgumentError, "Ambiguous route definition. Both :path and the route path were specified as strings."
+ end
+ to = get_to_from_path(_path, to, route_options[:action])
+ decomposed_match(_path, controller, route_options, _path, to, via, formatted, anchor, options_constraints)
+ end
+
+ path_types.fetch(Symbol, []).each do |action|
+ route_options = options.dup
+ decomposed_match(action, controller, route_options, option_path, to, via, formatted, anchor, options_constraints)
+ end
+
+ self
+ end
+
+ def get_to_from_path(path, to, action)
+ return to if to || action
+
+ path_without_format = path.sub(/\(\.:format\)$/, "")
+ if using_match_shorthand?(path_without_format)
+ path_without_format.gsub(%r{^/}, "").sub(%r{/([^/]*)$}, '#\1').tr("-", "_")
+ else
+ nil
+ end
+ end
+
+ def using_match_shorthand?(path)
+ path =~ %r{^/?[-\w]+/[-\w/]+$}
+ end
+
+ def decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints)
+ if on = options.delete(:on)
+ send(on) { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) }
+ else
+ case @scope.scope_level
+ when :resources
+ 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
+ add_route(path, controller, options, _path, to, via, formatted, anchor, options_constraints)
+ end
+ end
+ end
+
+ def add_route(action, controller, options, _path, to, via, formatted, anchor, options_constraints)
+ path = path_for_action(action, _path)
+ raise ArgumentError, "path is required" if path.blank?
+
+ action = action.to_s
+
+ default_action = options.delete(:action) || @scope[:action]
+
+ if /^[\w\-\/]+$/.match?(action)
+ default_action ||= action.tr("-", "_") unless action.include?("/")
+ else
+ action = nil
+ end
+
+ as = if !options.fetch(:as, true) # if it's set to nil or false
+ options.delete(:as)
+ else
+ name_for_action(options.delete(:as), action)
+ end
+
+ path = Mapping.normalize_path URI.parser.escape(path), formatted
+ ast = Journey::Parser.parse path
+
+ mapping = Mapping.build(@scope, @set, ast, controller, default_action, to, via, formatted, options_constraints, anchor, options)
+ @set.add_route(mapping, as)
+ end
+
+ def match_root_route(options)
+ args = ["/", { as: :root, via: :get }.merge(options)]
+ match(*args)
+ end
end
# Routing Concerns allow you to declare common routes that can be reused
@@ -2001,7 +2046,7 @@ to this:
# concerns :commentable
# end
#
- # concerns also work in any routes helper that you want to use:
+ # Concerns also work in any routes helper that you want to use:
#
# namespace :posts do
# concerns :commentable
@@ -2018,10 +2063,124 @@ to this:
end
end
+ 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
+ # "http://www.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.
+ def direct(name, options = {}, &block)
+ unless @scope.root?
+ raise RuntimeError, "The direct method can't be used inside a routes scope block"
+ end
+
+ @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.
+ def resolve(*args, &block)
+ unless @scope.root?
+ raise RuntimeError, "The resolve method can't be used inside a routes scope block"
+ end
+
+ options = args.extract_options!
+ args = args.flatten(1)
+
+ args.each do |klass|
+ @set.add_polymorphic_mapping(klass, options, &block)
+ end
+ end
+ end
+
class Scope # :nodoc:
OPTIONS = [:path, :shallow_path, :as, :shallow_prefix, :module,
:controller, :action, :path_names, :constraints,
- :shallow, :blocks, :defaults, :via, :format, :options]
+ :shallow, :blocks, :defaults, :via, :format, :options, :to]
RESOURCE_SCOPES = [:resource, :resources]
RESOURCE_METHOD_SCOPES = [:collection, :member, :new]
@@ -2038,6 +2197,14 @@ to this:
scope_level == :nested
end
+ def null?
+ @hash.nil? && @parent.nil?
+ end
+
+ def root?
+ @parent.null?
+ end
+
def resources?
scope_level == :resources
end
@@ -2088,8 +2255,7 @@ to this:
def each
node = self
- loop do
- break if node.equal? NULL
+ until node.equal? NULL
yield node
node = node.parent
end
@@ -2102,7 +2268,7 @@ to this:
def initialize(set) #:nodoc:
@set = set
- @scope = Scope.new({ :path_names => @set.resources_path_names })
+ @scope = Scope.new(path_names: @set.resources_path_names)
@concerns = {}
end
@@ -2112,6 +2278,7 @@ to this:
include Scoping
include Concerns
include Resources
+ include CustomUrls
end
end
end
diff --git a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb
index 9934f5547a..4de5f9e2f7 100644
--- a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb
+++ b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb
@@ -1,10 +1,12 @@
+# frozen_string_literal: true
+
module ActionDispatch
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 correct URL or path to a RESTful
+ # 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:
@@ -40,7 +42,7 @@ module ActionDispatch
#
# Example usage:
#
- # edit_polymorphic_path(@post) # => "/posts/1/edit"
+ # edit_polymorphic_path(@post) # => "/posts/1/edit"
# polymorphic_path(@post, format: :pdf) # => "/posts/1.pdf"
#
# == Usage with mounted engines
@@ -79,7 +81,7 @@ module ActionDispatch
# 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 <tt>url_for</tt>.
+ # For all of these options, see the documentation for {url_for}[rdoc-ref:ActionDispatch::Routing::UrlFor].
#
# ==== Functionality
#
@@ -103,6 +105,10 @@ module ActionDispatch
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
@@ -114,8 +120,7 @@ module ActionDispatch
opts
end
- # Returns the path component of a URL for the given record. It uses
- # <tt>polymorphic_url</tt> with <tt>routing_type: :path</tt>.
+ # Returns the path component of a URL for the given record.
def polymorphic_path(record_or_hash_or_array, options = {})
if Hash === record_or_hash_or_array
options = record_or_hash_or_array.merge(options)
@@ -123,6 +128,10 @@ module ActionDispatch
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
@@ -134,7 +143,6 @@ module ActionDispatch
opts
end
-
%w(edit new).each do |action|
module_eval <<-EOT, __FILE__, __LINE__ + 1
def #{action}_polymorphic_url(record_or_hash, options = {})
@@ -149,176 +157,195 @@ module ActionDispatch
private
- def polymorphic_url_for_action(action, record_or_hash, options)
- polymorphic_url(record_or_hash, options.merge(:action => action))
- end
-
- def polymorphic_path_for_action(action, record_or_hash, options)
- polymorphic_path(record_or_hash, options.merge(:action => action))
- end
-
- class HelperMethodBuilder # :nodoc:
- CACHE = { 'path' => {}, 'url' => {} }
-
- def self.get(action, type)
- type = type.to_s
- CACHE[type].fetch(action) { build action, type }
+ def polymorphic_url_for_action(action, record_or_hash, options)
+ polymorphic_url(record_or_hash, options.merge(action: action))
end
- def self.url; CACHE['url'.freeze][nil]; end
- def self.path; CACHE['path'.freeze][nil]; end
+ def polymorphic_path_for_action(action, record_or_hash, options)
+ polymorphic_path(record_or_hash, options.merge(action: action))
+ end
- def self.build(action, type)
- prefix = action ? "#{action}_" : ""
- suffix = type
- if action.to_s == 'new'
- HelperMethodBuilder.singular prefix, suffix
+ def polymorphic_mapping(record)
+ if record.respond_to?(:to_model)
+ _routes.polymorphic_mappings[record.to_model.model_name.name]
else
- HelperMethodBuilder.plural prefix, suffix
+ _routes.polymorphic_mappings[record.class.name]
end
end
- def self.singular(prefix, suffix)
- new(->(name) { name.singular_route_key }, prefix, suffix)
- end
+ class HelperMethodBuilder # :nodoc:
+ CACHE = { "path" => {}, "url" => {} }
- def self.plural(prefix, suffix)
- new(->(name) { name.route_key }, prefix, suffix)
- end
+ def self.get(action, type)
+ type = type.to_s
+ CACHE[type].fetch(action) { build action, type }
+ end
- def self.polymorphic_method(recipient, record_or_hash_or_array, action, type, options)
- builder = get action, type
+ def self.url; CACHE["url"][nil]; end
+ def self.path; CACHE["path"][nil]; end
- 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
+ def self.build(action, type)
+ prefix = action ? "#{action}_" : ""
+ suffix = type
+ if action.to_s == "new"
+ HelperMethodBuilder.singular prefix, suffix
+ else
+ HelperMethodBuilder.plural prefix, suffix
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
+ def self.singular(prefix, suffix)
+ new(->(name) { name.singular_route_key }, prefix, suffix)
+ end
- if options.empty?
- recipient.send(method, *args)
- else
- recipient.send(method, *args, options)
+ def self.plural(prefix, suffix)
+ new(->(name) { name.route_key }, prefix, suffix)
end
- end
- attr_reader :suffix, :prefix
+ 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
- def initialize(key_strategy, prefix, suffix)
- @key_strategy = key_strategy
- @prefix = prefix
- @suffix = suffix
- end
+ when nil
+ raise ArgumentError, "Nil location provided. Can't build URI."
+ else
+ method, args = builder.handle_model record_or_hash_or_array
+ end
- def handle_string(record)
- [get_method_for_string(record), []]
- end
+ if options.empty?
+ recipient.send(method, *args)
+ else
+ recipient.send(method, *args, options)
+ end
+ end
- def handle_string_call(target, str)
- target.send get_method_for_string str
- end
+ attr_reader :suffix, :prefix
- def handle_class(klass)
- [get_method_for_class(klass), []]
- end
+ def initialize(key_strategy, prefix, suffix)
+ @key_strategy = key_strategy
+ @prefix = prefix
+ @suffix = suffix
+ end
- def handle_class_call(target, klass)
- target.send get_method_for_class klass
- end
+ def handle_string(record)
+ [get_method_for_string(record), []]
+ end
- def handle_model(record)
- args = []
+ def handle_string_call(target, str)
+ target.send get_method_for_string str
+ end
- 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
+ def handle_class(klass)
+ [get_method_for_class(klass), []]
+ end
- [named_route, args]
- end
+ def handle_class_call(target, klass)
+ target.send get_method_for_class klass
+ end
- def handle_model_call(target, model)
- method, args = handle_model model
- target.send(method, *args)
- end
+ def handle_model(record)
+ args = []
- def handle_list(list)
- record_list = list.dup
- record = record_list.pop
+ 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
- args = []
+ [named_route, args]
+ end
- route = record_list.map { |parent|
- case parent
- when Symbol, String
- parent.to_s
- when Class
- args << parent
- parent.model_name.singular_route_key
+ def handle_model_call(target, record)
+ if mapping = polymorphic_mapping(target, record)
+ mapping.call(target, [record], suffix == "path")
else
- args << parent.to_model
- parent.to_model.model_name.singular_route_key
+ method, args = handle_model(record)
+ target.send(method, *args)
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
+ end
+
+ 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
- @key_strategy.call model.model_name
+ model = record.to_model
+ if model.persisted?
+ args << model
+ model.model_name.singular_route_key
+ else
+ @key_strategy.call model.model_name
+ end
end
- end
- route << suffix
+ route << suffix
- named_route = prefix + route.join("_")
- [named_route, args]
- end
+ named_route = prefix + route.join("_")
+ [named_route, args]
+ end
- private
+ private
- def get_method_for_class(klass)
- name = @key_strategy.call klass.model_name
- get_method_for_string name
- end
+ 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
- def get_method_for_string(str)
- "#{prefix}#{str}_#{suffix}"
- end
+ def get_method_for_class(klass)
+ name = @key_strategy.call klass.model_name
+ get_method_for_string name
+ end
- [nil, 'new', 'edit'].each do |action|
- CACHE['url'][action] = build action, 'url'
- CACHE['path'][action] = build action, 'path'
+ def get_method_for_string(str)
+ "#{prefix}#{str}_#{suffix}"
+ end
+
+ [nil, "new", "edit"].each do |action|
+ CACHE["url"][action] = build action, "url"
+ CACHE["path"][action] = build action, "path"
+ end
end
- end
end
end
end
diff --git a/actionpack/lib/action_dispatch/routing/redirection.rb b/actionpack/lib/action_dispatch/routing/redirection.rb
index d6987f4d09..143a4b3d62 100644
--- a/actionpack/lib/action_dispatch/routing/redirection.rb
+++ b/actionpack/lib/action_dispatch/routing/redirection.rb
@@ -1,9 +1,11 @@
-require 'action_dispatch/http/request'
-require 'active_support/core_ext/uri'
-require 'active_support/core_ext/array/extract_options'
-require 'rack/utils'
-require 'action_controller/metal/exceptions'
-require 'action_dispatch/routing/endpoint'
+# frozen_string_literal: true
+
+require "action_dispatch/http/request"
+require "active_support/core_ext/uri"
+require "active_support/core_ext/array/extract_options"
+require "rack/utils"
+require "action_controller/metal/exceptions"
+require "action_dispatch/routing/endpoint"
module ActionDispatch
module Routing
@@ -22,7 +24,6 @@ module ActionDispatch
end
def serve(req)
- req.check_path_parameters!
uri = URI.parse(path(req.path_parameters, req))
unless uri.host
@@ -37,12 +38,14 @@ module ActionDispatch
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
+ "Location" => uri.to_s,
+ "Content-Type" => "text/html",
+ "Content-Length" => body.length.to_s
}
[ status, headers, [body] ]
@@ -58,19 +61,19 @@ module ActionDispatch
private
def relative_path?(path)
- path && !path.empty? && path[0] != '/'
+ path && !path.empty? && path[0] != "/"
end
def escape(params)
- Hash[params.map{ |k,v| [k, Rack::Utils.escape(v)] }]
+ Hash[params.map { |k, v| [k, Rack::Utils.escape(v)] }]
end
def escape_fragment(params)
- Hash[params.map{ |k,v| [k, Journey::Router::Utils.escape_fragment(v)] }]
+ Hash[params.map { |k, v| [k, Journey::Router::Utils.escape_fragment(v)] }]
end
def escape_path(params)
- Hash[params.map{ |k,v| [k, Journey::Router::Utils.escape_path(v)] }]
+ Hash[params.map { |k, v| [k, Journey::Router::Utils.escape_path(v)] }]
end
end
@@ -104,11 +107,11 @@ module ActionDispatch
def path(params, request)
url_options = {
- :protocol => request.protocol,
- :host => request.host,
- :port => request.optional_port,
- :path => request.path,
- :params => request.query_parameters
+ 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*\}/)
@@ -129,21 +132,23 @@ module ActionDispatch
end
def inspect
- "redirect(#{status}, #{options.map{ |k,v| "#{k}: #{v}" }.join(', ')})"
+ "redirect(#{status}, #{options.map { |k, v| "#{k}: #{v}" }.join(', ')})"
end
end
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
+ # 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.
#
@@ -162,11 +167,16 @@ module ActionDispatch
# 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
+ # 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
diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb
index 85f202b823..d0a7eadf45 100644
--- a/actionpack/lib/action_dispatch/routing/route_set.rb
+++ b/actionpack/lib/action_dispatch/routing/route_set.rb
@@ -1,12 +1,13 @@
-require 'action_dispatch/journey'
-require 'active_support/concern'
-require 'active_support/core_ext/object/to_query'
-require 'active_support/core_ext/hash/slice'
-require 'active_support/core_ext/module/remove_method'
-require 'active_support/core_ext/array/extract_options'
-require 'action_controller/metal/exceptions'
-require 'action_dispatch/http/request'
-require 'action_dispatch/routing/endpoint'
+# frozen_string_literal: true
+
+require "action_dispatch/journey"
+require "active_support/core_ext/object/to_query"
+require "active_support/core_ext/module/redefine_method"
+require "active_support/core_ext/module/remove_method"
+require "active_support/core_ext/array/extract_options"
+require "action_controller/metal/exceptions"
+require "action_dispatch/http/request"
+require "action_dispatch/routing/endpoint"
module ActionDispatch
module Routing
@@ -34,7 +35,7 @@ module ActionDispatch
if @raise_on_name_error
raise
else
- return [404, {'X-Cascade' => 'pass'}, []]
+ [404, { "X-Cascade" => "pass" }, []]
end
end
@@ -59,7 +60,7 @@ module ActionDispatch
private
- def controller(_); @controller_class; end
+ def controller(_); @controller_class; end
end
# A NamedRouteCollection instance is a collection of named routes, and also
@@ -71,7 +72,7 @@ module ActionDispatch
private :routes
def initialize
- @routes = {}
+ @routes = {}
@path_helpers = Set.new
@url_helpers = Set.new
@url_helpers_module = Module.new
@@ -89,11 +90,11 @@ module ActionDispatch
def clear!
@path_helpers.each do |helper|
- @path_helpers_module.send :undef_method, helper
+ @path_helpers_module.remove_method helper
end
@url_helpers.each do |helper|
- @url_helpers_module.send :undef_method, helper
+ @url_helpers_module.remove_method helper
end
@routes.clear
@@ -107,8 +108,8 @@ module ActionDispatch
url_name = :"#{name}_url"
if routes.key? key
- @path_helpers_module.send :undef_method, path_name
- @url_helpers_module.send :undef_method, url_name
+ @path_helpers_module.undef_method path_name
+ @url_helpers_module.undef_method url_name
end
routes[key] = route
define_url_helper @path_helpers_module, route, path_name, route.defaults, name, PATH
@@ -144,6 +145,31 @@ module ActionDispatch
routes.length
end
+ # Given a +name+, defines name_path and name_url helpers.
+ # Used by 'direct', 'resolve', and 'polymorphic' route helpers.
+ def add_url_helper(name, defaults, &block)
+ helper = CustomUrlHelper.new(name, defaults, &block)
+ path_name = :"#{name}_path"
+ url_name = :"#{name}_url"
+
+ @path_helpers_module.module_eval do
+ redefine_method(path_name) do |*args|
+ helper.call(self, args, true)
+ end
+ end
+
+ @url_helpers_module.module_eval do
+ redefine_method(url_name) do |*args|
+ helper.call(self, args, false)
+ end
+ end
+
+ @path_helpers << path_name
+ @url_helpers << url_name
+
+ self
+ end
+
class UrlHelper
def self.create(route, options, route_name, url_strategy)
if optimize_helper?(route)
@@ -172,6 +198,16 @@ module ActionDispatch
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
@@ -180,40 +216,40 @@ module ActionDispatch
private
- def optimized_helper(args)
- params = parameterize_args(args) do
- raise_generation_error(args)
- end
+ def optimized_helper(args)
+ params = parameterize_args(args) do
+ raise_generation_error(args)
+ end
- @route.format params
- end
+ @route.format params
+ end
- def optimize_routes_generation?(t)
- t.send(:optimize_routes_generation?)
- end
+ def optimize_routes_generation?(t)
+ t.send(:optimize_routes_generation?)
+ end
- 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
+ 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
- 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}"
+ 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
+ raise ActionController::UrlGenerationError, message
+ end
end
def initialize(route, options, route_name, url_strategy)
@@ -248,6 +284,8 @@ module ActionDispatch
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)
@@ -264,53 +302,50 @@ module ActionDispatch
end
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')
- #
- def define_url_helper(mod, route, name, opts, route_key, url_strategy)
- helper = UrlHelper.create(route, opts, route_key, url_strategy)
- mod.module_eval do
- define_method(name) do |*args|
- last = args.last
- options = case last
- when Hash
- args.pop
- when ActionController::Parameters
- if last.permitted?
- args.pop.to_h
- else
- raise ArgumentError, "Generating a URL from non sanitized request parameters is insecure!"
- end
- end
- helper.call self, args, options
+ # 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')
+ #
+ def define_url_helper(mod, route, name, opts, route_key, url_strategy)
+ helper = UrlHelper.create(route, opts, route_key, url_strategy)
+ mod.module_eval do
+ 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, args, options
+ end
end
end
- end
end
- # strategy for building urls to send to the client
+ # strategy for building URLs to send to the client
PATH = ->(options) { ActionDispatch::Http::URL.path_for(options) }
UNKNOWN = ->(options) { ActionDispatch::Http::URL.url_for(options) }
attr_accessor :formatter, :set, :named_routes, :default_scope, :router
attr_accessor :disable_clear_and_finalize, :resources_path_names
attr_accessor :default_url_options
- attr_reader :env_key
+ attr_reader :env_key, :polymorphic_mappings
alias :routes :set
def self.default_resources_path_names
- { :new => 'new', :edit => 'edit' }
+ { new: "new", edit: "edit" }
end
def self.new_with_config(config)
@@ -342,11 +377,18 @@ module ActionDispatch
@prepend = []
@disable_clear_and_finalize = false
@finalized = false
- @env_key = "ROUTES_#{object_id}_SCRIPT_NAME".freeze
+ @env_key = "ROUTES_#{object_id}_SCRIPT_NAME"
@set = Journey::Routes.new
@router = Journey::Router.new @set
@formatter = Journey::Formatter.new self
+ @polymorphic_mappings = {}
+ end
+
+ def eager_load!
+ router.eager_load!
+ routes.each(&:eager_load!)
+ nil
end
def relative_url_root
@@ -402,6 +444,7 @@ module ActionDispatch
named_routes.clear
set.clear
formatter.clear
+ @polymorphic_mappings.clear
@prepend.each { |blk| eval_block(blk) }
end
@@ -418,7 +461,7 @@ module ActionDispatch
MountedHelpers
end
- def define_mounted_helper(name)
+ def define_mounted_helper(name, script_namer = nil)
return if MountedHelpers.method_defined?(name)
routes = self
@@ -426,7 +469,7 @@ module ActionDispatch
MountedHelpers.class_eval do
define_method "_#{name}" do
- RoutesProxy.new(routes, _routes_context, helpers)
+ RoutesProxy.new(routes, _routes_context, helpers, script_namer)
end
end
@@ -446,17 +489,50 @@ module ActionDispatch
# Define url_for in the singleton level so one can do:
# Rails.application.routes.url_helpers.url_for(args)
- @_routes = routes
+ proxy_class = Class.new do
+ include UrlFor
+ include routes.named_routes.path_helpers_module
+ include routes.named_routes.url_helpers_module
+
+ attr_reader :_routes
+
+ def initialize(routes)
+ @_routes = routes
+ end
+
+ def optimize_routes_generation?
+ @_routes.optimize_routes_generation?
+ end
+ end
+
+ @_proxy = proxy_class.new(routes)
+
class << self
def url_for(options)
- @_routes.url_for(options)
+ @_proxy.url_for(options)
+ end
+
+ def full_url_for(options)
+ @_proxy.full_url_for(options)
+ end
+
+ def route_for(name, *args)
+ @_proxy.route_for(name, *args)
end
def optimize_routes_generation?
- @_routes.optimize_routes_generation?
+ @_proxy.optimize_routes_generation?
end
- attr_reader :_routes
+ def polymorphic_url(record_or_hash_or_array, options = {})
+ @_proxy.polymorphic_url(record_or_hash_or_array, options)
+ end
+
+ def polymorphic_path(record_or_hash_or_array, options = {})
+ @_proxy.polymorphic_path(record_or_hash_or_array, options)
+ end
+
+ def _routes; @_proxy._routes; end
def url_options; {}; end
end
@@ -480,7 +556,7 @@ module ActionDispatch
# plus a singleton class method called _routes ...
included do
- singleton_class.send(:redefine_method, :_routes) { routes }
+ redefine_singleton_method(:_routes) { routes }
end
# And an instance method _routes. Note that
@@ -500,7 +576,7 @@ module ActionDispatch
routes.empty?
end
- def add_route(mapping, path_ast, name, anchor)
+ def add_route(mapping, name)
raise ArgumentError, "Invalid route name: '#{name}'" unless name.blank? || name.to_s.match(/^[_a-z]\w*$/i)
if name && named_routes[name]
@@ -508,7 +584,7 @@ module ActionDispatch
"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" \
- "http://guides.rubyonrails.org/routing.html#restricting-the-routes-created"
+ "https://guides.rubyonrails.org/routing.html#restricting-the-routes-created"
end
route = @set.add_route(name, mapping)
@@ -517,20 +593,58 @@ module ActionDispatch
if route.segment_keys.include?(:controller)
ActiveSupport::Deprecation.warn(<<-MSG.squish)
Using a dynamic :controller segment in a route is deprecated and
- will be removed in Rails 5.1
+ will be removed in Rails 6.0.
MSG
end
if route.segment_keys.include?(:action)
ActiveSupport::Deprecation.warn(<<-MSG.squish)
Using a dynamic :action segment in a route is deprecated and
- will be removed in Rails 5.1
+ will be removed in Rails 6.0.
MSG
end
route
end
+ def add_polymorphic_mapping(klass, options, &block)
+ @polymorphic_mappings[klass] = CustomUrlHelper.new(klass, options, &block)
+ end
+
+ def add_url_helper(name, options, &block)
+ named_routes.add_url_helper(name, options, &block)
+ end
+
+ class CustomUrlHelper
+ attr_reader :name, :defaults, :block
+
+ def initialize(name, defaults, &block)
+ @name = name
+ @defaults = defaults
+ @block = block
+ end
+
+ 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
+
+ private
+ def eval_block(t, args, options)
+ t.instance_exec(*args, merge_defaults(options), &block)
+ end
+
+ def merge_defaults(options)
+ defaults ? defaults.merge(options) : options
+ end
+ end
+
class Generator
PARAMETERIZE = lambda do |name, value|
if name == :controller
@@ -548,12 +662,10 @@ module ActionDispatch
@recall = recall
@set = set
- normalize_recall!
normalize_options!
normalize_controller_action_id!
use_relative_controller!
normalize_controller!
- normalize_action!
end
def controller
@@ -572,11 +684,6 @@ module ActionDispatch
end
end
- # Set 'index' as default action for recall
- def normalize_recall!
- @recall[:action] ||= 'index'
- end
-
def normalize_options!
# If an explicit :controller was given, always make :action explicit
# too, so that action expiry works as expected for things like
@@ -588,12 +695,12 @@ module ActionDispatch
# be "index", not the recalled action of "show".
if options[:controller]
- options[:action] ||= 'index'
+ options[:action] ||= "index"
options[:controller] = options[:controller].to_s
end
if options.key?(:action)
- options[:action] = (options[:action] || 'index').to_s
+ options[:action] = (options[:action] || "index").to_s
end
end
@@ -603,8 +710,8 @@ module ActionDispatch
# :controller, :action or :id is not found, don't pull any
# more keys from the recall.
def normalize_controller_action_id!
- use_recall_for(:controller) or return
- use_recall_for(:action) or return
+ use_recall_for(:controller) || return
+ use_recall_for(:action) || return
use_recall_for(:id)
end
@@ -612,7 +719,7 @@ module ActionDispatch
# is specified, the controller becomes "foo/baz/bat"
def use_relative_controller!
if !named_route && different_controller? && !controller.start_with?("/")
- old_parts = current_controller.split('/')
+ old_parts = current_controller.split("/")
size = controller.count("/") + 1
parts = old_parts[0...-size] << controller
@options[:controller] = parts.join("/")
@@ -622,7 +729,7 @@ module ActionDispatch
# Remove leading slashes from controllers
def normalize_controller!
if controller
- if controller.start_with?("/".freeze)
+ if controller.start_with?("/")
@options[:controller] = controller[1..-1]
else
@options[:controller] = controller
@@ -630,13 +737,6 @@ module ActionDispatch
end
end
- # Move 'index' action from options to recall
- def normalize_action!
- if @options[:action] == 'index'.freeze
- @recall[:action] = @options.delete(:action)
- end
- end
-
# Generates a path from routes, returns [path, params].
# If no route is generated the formatter will raise ActionController::UrlGenerationError
def generate
@@ -660,11 +760,11 @@ module ActionDispatch
# Generate the path indicated by the arguments, and return an array of
# the keys that were not used to generate it.
- def extra_keys(options, recall={})
+ def extra_keys(options, recall = {})
generate_extras(options, recall).last
end
- def generate_extras(options, recall={})
+ def generate_extras(options, recall = {})
route_key = options.delete :use_route
path, params = generate(route_key, options, recall)
return path, params.keys
@@ -684,7 +784,7 @@ module ActionDispatch
end
def find_script_name(options)
- options.delete(:script_name) || find_relative_url_root(options) || ''
+ options.delete(:script_name) || find_relative_url_root(options) || ""
end
def find_relative_url_root(options)
@@ -706,7 +806,7 @@ module ActionDispatch
password = options.delete :password
end
- recall = options.delete(:_recall) { {} }
+ recall = options.delete(:_recall) { {} }
original_script_name = options.delete(:original_script_name)
script_name = find_script_name options
@@ -745,12 +845,16 @@ module ActionDispatch
extras = environment[:extras] || {}
begin
- env = Rack::MockRequest.env_for(path, {:method => method})
+ 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
+
+ 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|
@@ -759,8 +863,7 @@ module ActionDispatch
params[key] = URI.parser.unescape(value)
end
end
- old_params = req.path_parameters
- req.path_parameters = old_params.merge params
+ req.path_parameters = params
app = route.app
if app.matches?(req) && app.dispatcher?
begin
@@ -770,10 +873,15 @@ module ActionDispatch
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
- raise ActionController::RoutingError, "No route matches #{path.inspect}"
+ if raise_on_missing
+ raise ActionController::RoutingError, "No route matches #{path.inspect}"
+ end
end
end
# :startdoc:
diff --git a/actionpack/lib/action_dispatch/routing/routes_proxy.rb b/actionpack/lib/action_dispatch/routing/routes_proxy.rb
index 040ea04046..587a72729c 100644
--- a/actionpack/lib/action_dispatch/routing/routes_proxy.rb
+++ b/actionpack/lib/action_dispatch/routing/routes_proxy.rb
@@ -1,4 +1,6 @@
-require 'active_support/core_ext/array/extract_options'
+# frozen_string_literal: true
+
+require "active_support/core_ext/array/extract_options"
module ActionDispatch
module Routing
@@ -8,9 +10,10 @@ module ActionDispatch
attr_accessor :scope, :routes
alias :_routes :routes
- def initialize(routes, scope, helpers)
+ def initialize(routes, scope, helpers, script_namer = nil)
@routes, @scope = routes, scope
@helpers = helpers
+ @script_namer = script_namer
end
def url_options
@@ -19,7 +22,8 @@ module ActionDispatch
end
end
- def respond_to?(method, include_private = false)
+ private
+ def respond_to_missing?(method, _)
super || @helpers.respond_to?(method)
end
@@ -28,15 +32,38 @@ module ActionDispatch
self.class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{method}(*args)
options = args.extract_options!
- args << url_options.merge((options || {}).symbolize_keys)
+ 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
- send(method, *args)
+ 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
diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb
index 28be189f93..fcb8ae296b 100644
--- a/actionpack/lib/action_dispatch/routing/url_for.rb
+++ b/actionpack/lib/action_dispatch/routing/url_for.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionDispatch
module Routing
# In <tt>config/routes.rb</tt> you define URL-to-controller mappings, but the reverse
@@ -107,16 +109,16 @@ module ActionDispatch
end
# Hook overridden in controller to add request information
- # with `default_url_options`. Application logic should not
+ # with +default_url_options+. Application logic should not
# go into url_options.
def url_options
default_url_options
end
- # Generate a url based on the options provided, default_url_options and the
+ # 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>: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
@@ -131,6 +133,7 @@ module ActionDispatch
# <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.
#
@@ -153,7 +156,7 @@ module ActionDispatch
# 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 `GET /users/1`:
+ # through <tt>GET /users/1</tt>:
#
# url_for(only_path: true) # => '/users/1'
# url_for(only_path: true, action: 'edit') # => '/users/1/edit'
@@ -164,20 +167,17 @@ module ActionDispatch
# implicitly used by +url_for+ can always be overwritten like shown on the
# last +url_for+ calls.
def url_for(options = nil)
+ full_url_for(options)
+ end
+
+ def full_url_for(options = nil) # :nodoc:
case options
when nil
_routes.url_for(url_options.symbolize_keys)
- when Hash
- route_name = options.delete :use_route
- _routes.url_for(options.symbolize_keys.reverse_merge!(url_options),
- route_name)
- when ActionController::Parameters
- unless options.permitted?
- raise ArgumentError.new("Generating a URL from non sanitized request parameters is insecure!")
- end
+ when Hash, ActionController::Parameters
route_name = options.delete :use_route
- _routes.url_for(options.to_h.symbolize_keys.
- reverse_merge!(url_options), route_name)
+ 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
@@ -192,22 +192,46 @@ module ActionDispatch
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"
+ #
+ def route_for(name, *args)
+ public_send(:"#{name}_url", *args)
+ end
+
protected
- def optimize_routes_generation?
- _routes.optimize_routes_generation? && default_url_options.empty?
- end
+ def optimize_routes_generation?
+ _routes.optimize_routes_generation? && default_url_options.empty?
+ end
- def _with_routes(routes)
- old_routes, @_routes = @_routes, routes
- yield
- ensure
- @_routes = old_routes
- end
+ private
- def _routes_context
- self
- end
+ def _with_routes(routes) # :doc:
+ old_routes, @_routes = @_routes, routes
+ yield
+ ensure
+ @_routes = old_routes
+ end
+
+ def _routes_context # :doc:
+ self
+ end
end
end
end
diff --git a/actionpack/lib/action_dispatch/system_test_case.rb b/actionpack/lib/action_dispatch/system_test_case.rb
new file mode 100644
index 0000000000..066daa4a12
--- /dev/null
+++ b/actionpack/lib/action_dispatch/system_test_case.rb
@@ -0,0 +1,167 @@
+# frozen_string_literal: true
+
+gem "capybara", ">= 2.15"
+
+require "capybara/dsl"
+require "capybara/minitest"
+require "action_controller"
+require "action_dispatch/system_testing/driver"
+require "action_dispatch/system_testing/browser"
+require "action_dispatch/system_testing/server"
+require "action_dispatch/system_testing/test_helpers/screenshot_helper"
+require "action_dispatch/system_testing/test_helpers/setup_and_teardown"
+require "action_dispatch/system_testing/test_helpers/undef_methods"
+
+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.
+ class SystemTestCase < IntegrationTest
+ include Capybara::DSL
+ include Capybara::Minitest::Assertions
+ include SystemTesting::TestHelpers::SetupAndTeardown
+ include SystemTesting::TestHelpers::ScreenshotHelper
+ include SystemTesting::TestHelpers::UndefMethods
+
+ def initialize(*) # :nodoc:
+ super
+ self.class.driver.use
+ end
+
+ def self.start_application # :nodoc:
+ Capybara.app = Rack::Builder.new do
+ map "/" do
+ run Rails.application
+ end
+ end
+
+ SystemTesting::Server.new.run
+ end
+
+ 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
+ def self.driven_by(driver, using: :chrome, screen_size: [1400, 1400], options: {}, &capabilities)
+ driver_options = { using: using, screen_size: screen_size, options: options }
+
+ self.driver = SystemTesting::Driver.new(driver, driver_options, &capabilities)
+ end
+
+ driven_by :selenium
+
+ ActiveSupport.run_load_hooks(:action_dispatch_system_test_case, self)
+ end
+
+ SystemTestCase.start_application
+end
diff --git a/actionpack/lib/action_dispatch/system_testing/browser.rb b/actionpack/lib/action_dispatch/system_testing/browser.rb
new file mode 100644
index 0000000000..c34907b6cb
--- /dev/null
+++ b/actionpack/lib/action_dispatch/system_testing/browser.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+module ActionDispatch
+ module SystemTesting
+ class Browser # :nodoc:
+ attr_reader :name
+
+ def initialize(name)
+ @name = name
+ end
+
+ def type
+ case name
+ when :headless_chrome
+ :chrome
+ when :headless_firefox
+ :firefox
+ else
+ name
+ end
+ end
+
+ def options
+ case name
+ when :headless_chrome
+ headless_chrome_browser_options
+ when :headless_firefox
+ headless_firefox_browser_options
+ end
+ end
+
+ def capabilities
+ @option ||=
+ case type
+ when :chrome
+ ::Selenium::WebDriver::Chrome::Options.new
+ when :firefox
+ ::Selenium::WebDriver::Firefox::Options.new
+ end
+ end
+
+ private
+ def headless_chrome_browser_options
+ capabilities.args << "--headless"
+ capabilities.args << "--disable-gpu" if Gem.win_platform?
+
+ capabilities
+ end
+
+ def headless_firefox_browser_options
+ capabilities.args << "-headless"
+
+ capabilities
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/system_testing/driver.rb b/actionpack/lib/action_dispatch/system_testing/driver.rb
new file mode 100644
index 0000000000..25a09dd918
--- /dev/null
+++ b/actionpack/lib/action_dispatch/system_testing/driver.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+module ActionDispatch
+ module SystemTesting
+ class Driver # :nodoc:
+ def initialize(name, **options, &capabilities)
+ @name = name
+ @browser = Browser.new(options[:using])
+ @screen_size = options[:screen_size]
+ @options = options[:options]
+ @capabilities = capabilities
+ end
+
+ def use
+ register if registerable?
+
+ setup
+ end
+
+ private
+ def registerable?
+ [:selenium, :poltergeist, :webkit].include?(@name)
+ end
+
+ def register
+ define_browser_capabilities(@browser.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
+
+ def define_browser_capabilities(capabilities)
+ @capabilities.call(capabilities) if @capabilities
+ end
+
+ def browser_options
+ @options.merge(options: @browser.options).compact
+ end
+
+ 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
+
+ def register_poltergeist(app)
+ Capybara::Poltergeist::Driver.new(app, @options.merge(window_size: @screen_size))
+ end
+
+ 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
+
+ def setup
+ Capybara.current_driver = @name
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/system_testing/server.rb b/actionpack/lib/action_dispatch/system_testing/server.rb
new file mode 100644
index 0000000000..4fc1f33767
--- /dev/null
+++ b/actionpack/lib/action_dispatch/system_testing/server.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module ActionDispatch
+ module SystemTesting
+ class Server # :nodoc:
+ class << self
+ attr_accessor :silence_puma
+ end
+
+ self.silence_puma = false
+
+ def run
+ setup
+ end
+
+ private
+ def setup
+ set_server
+ set_port
+ end
+
+ def set_server
+ Capybara.server = :puma, { Silent: self.class.silence_puma } if Capybara.server == Capybara.servers[:default]
+ end
+
+ def set_port
+ Capybara.always_include_port = true
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb b/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb
new file mode 100644
index 0000000000..79359a0c8b
--- /dev/null
+++ b/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb
@@ -0,0 +1,96 @@
+# frozen_string_literal: true
+
+module ActionDispatch
+ module SystemTesting
+ module TestHelpers
+ # Screenshot helper for system testing.
+ 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.
+ #
+ # The screenshot will be displayed in your console, if supported.
+ #
+ # 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/).
+ def take_screenshot
+ 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.
+ def take_failed_screenshot
+ take_screenshot if failed? && supports_screenshot?
+ end
+
+ private
+ def image_name
+ failed? ? "failures_#{method_name}" : method_name
+ end
+
+ def image_path
+ @image_path ||= absolute_image_path.to_s
+ end
+
+ def absolute_image_path
+ Rails.root.join("tmp/screenshots/#{image_name}.png")
+ end
+
+ def save_image
+ page.save_screenshot(absolute_image_path)
+ end
+
+ 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
+
+ def display_image
+ message = +"[Screenshot]: #{image_path}\n"
+
+ 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
+
+ def inline_base64(path)
+ Base64.strict_encode64(path)
+ end
+
+ def failed?
+ !passed? && !skipped?
+ end
+
+ def supports_screenshot?
+ Capybara.current_driver != :rack_test
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb b/actionpack/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb
new file mode 100644
index 0000000000..600e9c733b
--- /dev/null
+++ b/actionpack/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module ActionDispatch
+ module SystemTesting
+ module TestHelpers
+ module SetupAndTeardown # :nodoc:
+ DEFAULT_HOST = "http://127.0.0.1"
+
+ def host!(host)
+ super
+ Capybara.app_host = host
+ end
+
+ def before_setup
+ host! DEFAULT_HOST
+ super
+ end
+
+ def after_teardown
+ begin
+ take_failed_screenshot
+ ensure
+ Capybara.reset_sessions!
+ end
+ ensure
+ super
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb b/actionpack/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb
new file mode 100644
index 0000000000..d64be3b3d9
--- /dev/null
+++ b/actionpack/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module ActionDispatch
+ module SystemTesting
+ module TestHelpers
+ module UndefMethods # :nodoc:
+ extend ActiveSupport::Concern
+ included do
+ METHODS = %i(get post put patch delete).freeze
+
+ METHODS.each do |verb|
+ undef_method verb
+ end
+
+ def method_missing(method, *args, &block)
+ if METHODS.include?(method)
+ raise NoMethodError, "System tests cannot make direct requests via ##{method}; use #visit and #click_on instead. See http://www.rubydoc.info/github/teamcapybara/capybara/master#The_DSL for more information."
+ else
+ super
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/testing/assertion_response.rb b/actionpack/lib/action_dispatch/testing/assertion_response.rb
index 3fb81ff083..dc019db6ac 100644
--- a/actionpack/lib/action_dispatch/testing/assertion_response.rb
+++ b/actionpack/lib/action_dispatch/testing/assertion_response.rb
@@ -1,14 +1,9 @@
+# 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.
- #
- # As an input to the initializer, we take a Fixnum, a String, or a Symbol.
- # If it's a Fixnum or String, we figure out what its symbolized name.
- # If it's a Symbol, we figure out what its corresponding code is.
- # The resulting code will be a Fixnum, for real HTTP codes, and it will
- # be a String for the pseudo-HTTP codes, such as:
- # :success, :missing, :redirect and :error
+ # 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
@@ -19,6 +14,9 @@ module ActionDispatch
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
@@ -38,12 +36,12 @@ module ActionDispatch
private
- def code_from_name(name)
- GENERIC_RESPONSE_CODES[name] || Rack::Utils::SYMBOL_TO_STATUS_CODE[name]
- end
+ 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
+ def name_from_code(code)
+ GENERIC_RESPONSE_CODES.invert[code] || Rack::Utils::HTTP_STATUS_CODES[code]
+ end
end
end
diff --git a/actionpack/lib/action_dispatch/testing/assertions.rb b/actionpack/lib/action_dispatch/testing/assertions.rb
index fae266273e..08c2969685 100644
--- a/actionpack/lib/action_dispatch/testing/assertions.rb
+++ b/actionpack/lib/action_dispatch/testing/assertions.rb
@@ -1,9 +1,11 @@
-require 'rails-dom-testing'
+# frozen_string_literal: true
+
+require "rails-dom-testing"
module ActionDispatch
module Assertions
- autoload :ResponseAssertions, 'action_dispatch/testing/assertions/response'
- autoload :RoutingAssertions, 'action_dispatch/testing/assertions/routing'
+ autoload :ResponseAssertions, "action_dispatch/testing/assertions/response"
+ autoload :RoutingAssertions, "action_dispatch/testing/assertions/routing"
extend ActiveSupport::Concern
@@ -12,7 +14,7 @@ module ActionDispatch
include Rails::Dom::Testing::Assertions
def html_document
- @html_document ||= if @response.content_type.to_s =~ /xml\z/
+ @html_document ||= if @response.content_type.to_s.end_with?("xml")
Nokogiri::XML::Document.parse(@response.body)
else
Nokogiri::HTML::Document.parse(@response.body)
diff --git a/actionpack/lib/action_dispatch/testing/assertions/response.rb b/actionpack/lib/action_dispatch/testing/assertions/response.rb
index cd55b7d975..8595ea03cf 100644
--- a/actionpack/lib/action_dispatch/testing/assertions/response.rb
+++ b/actionpack/lib/action_dispatch/testing/assertions/response.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionDispatch
module Assertions
# A small suite of assertions that test responses from \Rails applications.
@@ -45,12 +47,12 @@ module ActionDispatch
# # 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
+ # # 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)
- def assert_redirected_to(options = {}, message=nil)
+ def assert_redirected_to(options = {}, message = nil)
assert_response(:redirect, message)
return true if options === @response.location
@@ -77,13 +79,17 @@ module ActionDispatch
end
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
+ (+"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
+
+ def response_body_if_short
+ return "" if @response.body.size > 500
+ "\nResponse body: #{@response.body}"
end
def location_if_redirected
- return '' unless @response.redirection? && @response.location.present?
+ return "" unless @response.redirection? && @response.location.present?
location = normalize_argument_to_redirection(@response.location)
" redirect to <#{location}>"
end
diff --git a/actionpack/lib/action_dispatch/testing/assertions/routing.rb b/actionpack/lib/action_dispatch/testing/assertions/routing.rb
index 44ad2c10d8..28cde6704e 100644
--- a/actionpack/lib/action_dispatch/testing/assertions/routing.rb
+++ b/actionpack/lib/action_dispatch/testing/assertions/routing.rb
@@ -1,12 +1,19 @@
-require 'uri'
-require 'active_support/core_ext/hash/indifferent_access'
-require 'active_support/core_ext/string/access'
-require 'action_controller/metal/exceptions'
+# frozen_string_literal: true
+
+require "uri"
+require "active_support/core_ext/hash/indifferent_access"
+require "active_support/core_ext/string/access"
+require "action_controller/metal/exceptions"
module ActionDispatch
module Assertions
# Suite of assertions to test routes generated by \Rails and the handling of requests made to them.
module RoutingAssertions
+ 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+.
#
@@ -18,8 +25,8 @@ module ActionDispatch
# 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, appending the query string on the path directly will not work. For example:
+ # 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" })
@@ -37,7 +44,7 @@ module ActionDispatch
#
# # Test a custom route
# assert_recognizes({controller: 'items', action: 'show', id: '1'}, 'view/item1')
- def assert_recognizes(expected_options, path, extras={}, msg=nil)
+ 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)
@@ -75,14 +82,14 @@ module ActionDispatch
#
# # Asserts that the generated route gives us our custom route
# assert_generates "changesets/12", { controller: 'scm', action: 'show_diff', revision: "12" }
- def assert_generates(expected_path, options, defaults={}, extras={}, message=nil)
- if expected_path =~ %r{://}
+ 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.first == '/'
+ expected_path = "/#{expected_path}" unless expected_path.first == "/"
end
# Load routes.rb if it hasn't been loaded.
@@ -119,7 +126,7 @@ module ActionDispatch
#
# # Tests a route with an HTTP method
# assert_routing({ method: 'put', path: '/product/321' }, { controller: "product", action: "update", id: "321" })
- def assert_routing(path, options, defaults={}, extras={}, message=nil)
+ def assert_routing(path, options, defaults = {}, extras = {}, message = nil)
assert_recognizes(options, path, extras, message)
controller, default_controller = options[:controller], defaults[:controller]
@@ -127,13 +134,12 @@ module ActionDispatch
options[:controller] = "/#{controller}"
end
- generate_options = options.dup.delete_if{ |k, _| defaults.key?(k) }
+ 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.
+ # 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>:
@@ -152,8 +158,18 @@ module ActionDispatch
_routes = @routes
@controller.singleton_class.include(_routes.url_helpers)
- @controller.view_context_class = Class.new(@controller.view_context_class) do
- 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
@@ -183,10 +199,9 @@ module ActionDispatch
method = :get
end
- # Assume given controller
- request = ActionController::TestRequest.create
+ request = ActionController::TestRequest.create @controller.class
- if path =~ %r{://}
+ if %r{://}.match?(path)
fail_on(URI::InvalidURIError, msg) do
uri = URI.parse(path)
request.env["rack.url_scheme"] = uri.scheme || "http"
@@ -202,7 +217,7 @@ module ActionDispatch
request.request_method = method if method
params = fail_on(ActionController::RoutingError, msg) do
- @routes.recognize_path(path, { :method => method, :extras => extras })
+ @routes.recognize_path(path, method: method, extras: extras)
end
request.path_parameters = params.with_indifferent_access
diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb
index 60c562d7cd..bb8b43ad4d 100644
--- a/actionpack/lib/action_dispatch/testing/integration.rb
+++ b/actionpack/lib/action_dispatch/testing/integration.rb
@@ -1,167 +1,62 @@
-require 'stringio'
-require 'uri'
-require 'active_support/core_ext/kernel/singleton_class'
-require 'active_support/core_ext/object/try'
-require 'active_support/core_ext/string/strip'
-require 'rack/test'
-require 'minitest'
+# frozen_string_literal: true
+
+require "stringio"
+require "uri"
+require "active_support/core_ext/kernel/singleton_class"
+require "active_support/core_ext/object/try"
+require "rack/test"
+require "minitest"
+
+require "action_dispatch/testing/request_encoder"
module ActionDispatch
module Integration #:nodoc:
module RequestHelpers
- # Performs a GET request with the given parameters.
- #
- # - +path+: The URI (as a String) on which you want to perform a GET
- # 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.
- #
- # This method returns a Response object, which one can use to
- # inspect the details of the response. Furthermore, if this method was
- # called from an ActionDispatch::IntegrationTest object, then that
- # object's <tt>@response</tt> instance variable will point to the same
- # response object.
- #
- # You can also perform POST, PATCH, PUT, DELETE, and HEAD requests with
- # +#post+, +#patch+, +#put+, +#delete+, and +#head+.
- #
- # Example:
- #
- # get '/feed', params: { since: 201501011400 }
- # post '/profile', headers: { "X-Test-Header" => "testvalue" }
- def get(path, *args)
- process_with_kwargs(:get, path, *args)
+ # Performs a GET request with the given parameters. See ActionDispatch::Integration::Session#process
+ # for more details.
+ def get(path, **args)
+ process(:get, path, **args)
end
- # Performs a POST request with the given parameters. See +#get+ for more
- # details.
- def post(path, *args)
- process_with_kwargs(:post, path, *args)
+ # Performs a POST request with the given parameters. See ActionDispatch::Integration::Session#process
+ # for more details.
+ def post(path, **args)
+ process(:post, path, **args)
end
- # Performs a PATCH request with the given parameters. See +#get+ for more
- # details.
- def patch(path, *args)
- process_with_kwargs(:patch, path, *args)
+ # Performs a PATCH request with the given parameters. See ActionDispatch::Integration::Session#process
+ # for more details.
+ def patch(path, **args)
+ process(:patch, path, **args)
end
- # Performs a PUT request with the given parameters. See +#get+ for more
- # details.
- def put(path, *args)
- process_with_kwargs(:put, path, *args)
+ # Performs a PUT request with the given parameters. See ActionDispatch::Integration::Session#process
+ # for more details.
+ def put(path, **args)
+ process(:put, path, **args)
end
- # Performs a DELETE request with the given parameters. See +#get+ for
- # more details.
- def delete(path, *args)
- process_with_kwargs(:delete, path, *args)
+ # Performs a DELETE request with the given parameters. See ActionDispatch::Integration::Session#process
+ # for more details.
+ def delete(path, **args)
+ process(:delete, path, **args)
end
- # Performs a HEAD request with the given parameters. See +#get+ for more
- # details.
+ # Performs a HEAD request with the given parameters. See ActionDispatch::Integration::Session#process
+ # for more details.
def head(path, *args)
- process_with_kwargs(:head, path, *args)
- end
-
- # Performs an XMLHttpRequest request with the given parameters, mirroring
- # an AJAX request made from JavaScript.
- #
- # The request_method is +:get+, +:post+, +:patch+, +:put+, +:delete+ or
- # +:head+; the parameters are +nil+, a hash, or a url-encoded or multipart
- # string; the headers are a hash.
- #
- # Example:
- #
- # xhr :get, '/feed', params: { since: 201501011400 }
- def xml_http_request(request_method, path, *args)
- if kwarg_request?(args)
- params, headers, env = args.first.values_at(:params, :headers, :env)
- else
- params = args[0]
- headers = args[1]
- env = {}
-
- if params.present? || headers.present?
- non_kwarg_request_warning
- end
- end
-
- ActiveSupport::Deprecation.warn(<<-MSG.strip_heredoc)
- xhr and xml_http_request methods are deprecated in favor of
- `get "/posts", xhr: true` and `post "/posts/1", xhr: true`
- MSG
-
- process(request_method, path, params: params, headers: headers, xhr: true)
+ process(:head, path, *args)
end
- alias xhr :xml_http_request
# 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.
- def follow_redirect!
+ # performed on the location header. Any arguments are passed to the
+ # underlying call to `get`.
+ def follow_redirect!(**args)
raise "not a redirect! #{status} #{status_message}" unless redirect?
- get(response.location)
+ get(response.location, **args)
status
end
-
- # Performs a request using the specified method, following any subsequent
- # redirect. Note that the redirects are followed until the response is
- # not a redirect--this means you may run into an infinite loop if your
- # redirect loops back to itself.
- #
- # Example:
- #
- # request_via_redirect :post, '/welcome',
- # params: { ref_id: 14 },
- # headers: { "X-Test-Header" => "testvalue" }
- def request_via_redirect(http_method, path, *args)
- process_with_kwargs(http_method, path, *args)
-
- follow_redirect! while redirect?
- status
- end
-
- # Performs a GET request, following any subsequent redirect.
- # See +request_via_redirect+ for more information.
- def get_via_redirect(path, *args)
- ActiveSupport::Deprecation.warn('`get_via_redirect` is deprecated and will be removed in Rails 5.1. Please use follow_redirect! manually after the request call for the same behavior.')
- request_via_redirect(:get, path, *args)
- end
-
- # Performs a POST request, following any subsequent redirect.
- # See +request_via_redirect+ for more information.
- def post_via_redirect(path, *args)
- ActiveSupport::Deprecation.warn('`post_via_redirect` is deprecated and will be removed in Rails 5.1. Please use follow_redirect! manually after the request call for the same behavior.')
- request_via_redirect(:post, path, *args)
- end
-
- # Performs a PATCH request, following any subsequent redirect.
- # See +request_via_redirect+ for more information.
- def patch_via_redirect(path, *args)
- ActiveSupport::Deprecation.warn('`patch_via_redirect` is deprecated and will be removed in Rails 5.1. Please use follow_redirect! manually after the request call for the same behavior.')
- request_via_redirect(:patch, path, *args)
- end
-
- # Performs a PUT request, following any subsequent redirect.
- # See +request_via_redirect+ for more information.
- def put_via_redirect(path, *args)
- ActiveSupport::Deprecation.warn('`put_via_redirect` is deprecated and will be removed in Rails 5.1. Please use follow_redirect! manually after the request call for the same behavior.')
- request_via_redirect(:put, path, *args)
- end
-
- # Performs a DELETE request, following any subsequent redirect.
- # See +request_via_redirect+ for more information.
- def delete_via_redirect(path, *args)
- ActiveSupport::Deprecation.warn('`delete_via_redirect` is deprecated and will be removed in Rails 5.1. Please use follow_redirect! manually after the request call for the same behavior.')
- request_via_redirect(:delete, path, *args)
- end
end
# An instance of this class represents a set of requests and responses
@@ -179,11 +74,11 @@ module ActionDispatch
include TestProcess, RequestHelpers, Assertions
%w( status status_message headers body redirect? ).each do |method|
- delegate method, :to => :response, :allow_nil => true
+ delegate method, to: :response, allow_nil: true
end
%w( path ).each do |method|
- delegate method, :to => :request, :allow_nil => true
+ delegate method, to: :request, allow_nil: true
end
# The hostname used in the last request.
@@ -234,7 +129,7 @@ module ActionDispatch
url_options.reverse_merge!(@app.routes.default_url_options)
end
- url_options.reverse_merge!(:host => host, :protocol => https? ? "https" : "http")
+ url_options.reverse_merge!(host: host, protocol: https? ? "https" : "http")
end
end
@@ -252,8 +147,8 @@ module ActionDispatch
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," +
+ 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
@@ -280,168 +175,132 @@ module ActionDispatch
@https
end
- # Set the host name to use in the next request.
+ # Performs the actual request.
#
- # session.host! "www.example.com"
- alias :host! :host=
-
- private
- def _mock_session
- @_mock_session ||= Rack::MockSession.new(@app, host)
- end
-
- def process_with_kwargs(http_method, path, *args)
- if kwarg_request?(args)
- process(http_method, path, *args)
- else
- non_kwarg_request_warning if args.any?
- process(http_method, path, { params: args[0], headers: args[1] })
- end
- end
-
- REQUEST_KWARGS = %i(params headers env xhr)
- def kwarg_request?(args)
- args[0].respond_to?(:keys) && args[0].keys.any? { |k| REQUEST_KWARGS.include?(k) }
- end
-
- def non_kwarg_request_warning
- ActiveSupport::Deprecation.warn(<<-MSG.strip_heredoc)
- ActionDispatch::IntegrationTest HTTP request methods will accept only
- the following keyword arguments in future Rails versions:
- #{REQUEST_KWARGS.join(', ')}
-
- Examples:
-
- get '/profile',
- params: { id: 1 },
- headers: { 'X-Extra-Header' => '123' },
- env: { 'action_dispatch.custom' => 'custom' },
- xhr: true
- MSG
+ # - +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 }
+ 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
- # Performs the actual request.
- def process(method, path, params: nil, headers: nil, env: nil, xhr: false, as: nil)
- request_encoder = RequestEncoder.encoder(as)
-
- if path =~ %r{://}
- location = URI.parse(path)
+ 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
- path = request_encoder.append_format_to location.path
- path = location.query ? "#{path}?#{location.query}" : path
- else
- path = request_encoder.append_format_to path
- 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" => accept
- }
-
- if xhr
- headers ||= {}
- headers['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest'
- headers['HTTP_ACCEPT'] ||= [Mime[:js], Mime[:html], Mime[:xml], 'text/xml', '*/*'].join(', ')
end
+ end
- # this modifies the passed request_env directly
- if headers.present?
- Http::Headers.from_hash(request_env).merge!(headers)
- end
- if env.present?
- Http::Headers.from_hash(request_env).merge!(env)
- end
+ hostname, port = host.split(":")
- session = Rack::Test::Session.new(_mock_session)
+ request_env = {
+ :method => method,
+ :params => request_encoder.encode_params(params),
- # 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)
+ "SERVER_NAME" => hostname,
+ "SERVER_PORT" => port || (https? ? "443" : "80"),
+ "HTTPS" => https? ? "on" : "off",
+ "rack.url_scheme" => https? ? "https" : "http",
- @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
- @response.response_parser = RequestEncoder.parser(@response.content_type)
- @html_document = nil
- @url_options = nil
+ "REQUEST_URI" => path,
+ "HTTP_HOST" => host,
+ "REMOTE_ADDR" => remote_addr,
+ "CONTENT_TYPE" => request_encoder.content_type,
+ "HTTP_ACCEPT" => request_encoder.accept_header || accept
+ }
- @controller = @request.controller_instance
+ wrapped_headers = Http::Headers.from_hash({})
+ wrapped_headers.merge!(headers) if headers
- response.status
+ if xhr
+ wrapped_headers["HTTP_X_REQUESTED_WITH"] = "XMLHttpRequest"
+ wrapped_headers["HTTP_ACCEPT"] ||= [Mime[:js], Mime[:html], Mime[:xml], "text/xml", "*/*"].join(", ")
end
- def build_full_uri(path, env)
- "#{env['rack.url_scheme']}://#{env['SERVER_NAME']}:#{env['SERVER_PORT']}#{path}"
+ # 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
- class RequestEncoder # :nodoc:
- @encoders = {}
-
- attr_reader :response_parser
-
- def initialize(mime_name, param_encoder, response_parser, url_encoded_form = false)
- @mime = Mime[mime_name]
-
- unless @mime
- raise ArgumentError, "Can't register a request encoder for " \
- "unregistered MIME Type: #{mime_name}. See `Mime::Type.register`."
- end
-
- @url_encoded_form = url_encoded_form
- @path_format = ".#{@mime.symbol}" unless @url_encoded_form
- @response_parser = response_parser || -> body { body }
- @param_encoder = param_encoder || :"to_#{@mime.symbol}".to_proc
- end
+ session = Rack::Test::Session.new(_mock_session)
- def append_format_to(path)
- path << @path_format unless @url_encoded_form
- path
- end
+ # 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)
- def content_type
- @mime.to_s
- end
+ @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
- def encode_params(params)
- @param_encoder.call(params)
- end
+ @controller = @request.controller_instance
- def self.parser(content_type)
- mime = Mime::Type.lookup(content_type)
- encoder(mime ? mime.ref : nil).response_parser
- end
+ response.status
+ end
- def self.encoder(name)
- @encoders[name] || WWWFormEncoder
- end
+ # Set the host name to use in the next request.
+ #
+ # session.host! "www.example.com"
+ alias :host! :host=
- def self.register_encoder(mime_name, param_encoder: nil, response_parser: nil)
- @encoders[mime_name] = new(mime_name, param_encoder, response_parser)
- end
+ private
+ def _mock_session
+ @_mock_session ||= Rack::MockSession.new(@app, host)
+ end
- register_encoder :json, response_parser: -> body { JSON.parse(body) }
+ def build_full_uri(path, env)
+ "#{env['rack.url_scheme']}://#{env['SERVER_NAME']}:#{env['SERVER_PORT']}#{path}"
+ end
- WWWFormEncoder = new(:url_encoded_form, -> params { params }, nil, true)
+ 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
@@ -452,9 +311,13 @@ module ActionDispatch
attr_reader :app
+ def initialize(*args, &blk)
+ super(*args, &blk)
+ @integration_session = nil
+ end
+
def before_setup # :nodoc:
@app = nil
- @integration_session = nil
super
end
@@ -470,9 +333,9 @@ module ActionDispatch
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)
+ # 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
@@ -484,11 +347,10 @@ module ActionDispatch
@integration_session = nil
end
- %w(get post patch put head delete cookies assigns
- xml_http_request xhr get_via_redirect post_via_redirect).each do |method|
+ %w(get post patch put head delete cookies assigns follow_redirect!).each do |method|
define_method(method) do |*args|
# reset the html_document variable, except for cookies/assigns calls
- unless method == 'cookies' || method == 'assigns'
+ unless method == "cookies" || method == "assigns"
@html_document = nil
end
@@ -510,6 +372,7 @@ module ActionDispatch
# simultaneously.
def open_session
dup.tap do |session|
+ session.reset!
yield session if block_given?
end
end
@@ -530,14 +393,15 @@ module ActionDispatch
integration_session.default_url_options = options
end
- def respond_to?(method, include_private = false)
- integration_session.respond_to?(method, include_private) || super
+ private
+ def respond_to_missing?(method, _)
+ integration_session.respond_to?(method) || super
end
# Delegate unhandled messages to the current session instance.
- def method_missing(sym, *args, &block)
- if integration_session.respond_to?(sym)
- integration_session.__send__(sym, *args, &block).tap do
+ 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
@@ -699,42 +563,50 @@ module ActionDispatch
# 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'
+ # require "test_helper"
#
# class ApiTest < ActionDispatch::IntegrationTest
- # test 'creates articles' do
+ # test "creates articles" do
# assert_difference -> { Article.count } do
- # post articles_path, params: { article: { title: 'Ahoy!' } }, as: :json
+ # post articles_path, params: { article: { title: "Ahoy!" } }, as: :json
# end
#
# assert_response :success
- # assert_equal({ id: Arcticle.last.id, title: 'Ahoy!' }, response.parsed_body)
+ # assert_equal({ id: Article.last.id, title: "Ahoy!" }, response.parsed_body)
# end
# end
#
- # The `as` option sets the format to JSON, sets the content type to
- # 'application/json' and encodes the parameters as JSON.
+ # 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 as what
- # the last request was encoded as. If the request wasn't encoded `as` something,
- # it's the same as calling `body`.
+ # Calling +parsed_body+ on the response parses the response body based on the
+ # last response MIME type.
#
- # For any custom MIME Types you've registered, you can even add your own encoders with:
+ # 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`.
+ # 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.
class IntegrationTest < ActiveSupport::TestCase
+ include TestProcess::FixtureFile
+
module UrlOptions
extend ActiveSupport::Concern
def url_options
@@ -757,7 +629,11 @@ module ActionDispatch
module ClassMethods
def app
- defined?(@@app) ? @@app : ActionDispatch.test_app
+ if defined?(@@app) && @@app
+ @@app
+ else
+ ActionDispatch.test_app
+ end
end
def app=(app)
@@ -765,7 +641,7 @@ module ActionDispatch
end
def register_encoder(*args)
- Integration::Session::RequestEncoder.register_encoder(*args)
+ RequestEncoder.register_encoder(*args)
end
end
diff --git a/actionpack/lib/action_dispatch/testing/request_encoder.rb b/actionpack/lib/action_dispatch/testing/request_encoder.rb
new file mode 100644
index 0000000000..6c65bec62f
--- /dev/null
+++ b/actionpack/lib/action_dispatch/testing/request_encoder.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module ActionDispatch
+ class RequestEncoder # :nodoc:
+ class IdentityEncoder
+ def content_type; end
+ def accept_header; end
+ def encode_params(params); params; end
+ def response_parser; -> body { body }; end
+ end
+
+ @encoders = { identity: IdentityEncoder.new }
+
+ attr_reader :response_parser
+
+ def initialize(mime_name, param_encoder, response_parser)
+ @mime = Mime[mime_name]
+
+ unless @mime
+ raise ArgumentError, "Can't register a request encoder for " \
+ "unregistered MIME Type: #{mime_name}. See `Mime::Type.register`."
+ end
+
+ @response_parser = response_parser || -> body { body }
+ @param_encoder = param_encoder || :"to_#{@mime.symbol}".to_proc
+ end
+
+ def content_type
+ @mime.to_s
+ end
+
+ def accept_header
+ @mime.to_s
+ end
+
+ def encode_params(params)
+ @param_encoder.call(params) if params
+ end
+
+ def self.parser(content_type)
+ type = Mime::Type.lookup(content_type).ref if content_type
+ encoder(type).response_parser
+ end
+
+ def self.encoder(name)
+ @encoders[name] || @encoders[:identity]
+ end
+
+ def self.register_encoder(mime_name, param_encoder: nil, response_parser: nil)
+ @encoders[mime_name] = new(mime_name, param_encoder, response_parser)
+ end
+
+ register_encoder :json, response_parser: -> body { JSON.parse(body) }
+ end
+end
diff --git a/actionpack/lib/action_dispatch/testing/test_process.rb b/actionpack/lib/action_dispatch/testing/test_process.rb
index 1ecd7d14a7..0b98f27f11 100644
--- a/actionpack/lib/action_dispatch/testing/test_process.rb
+++ b/actionpack/lib/action_dispatch/testing/test_process.rb
@@ -1,8 +1,30 @@
-require 'action_dispatch/middleware/cookies'
-require 'action_dispatch/middleware/flash'
+# frozen_string_literal: true
+
+require "action_dispatch/middleware/cookies"
+require "action_dispatch/middleware/flash"
module ActionDispatch
module TestProcess
+ module FixtureFile
+ # Shortcut for <tt>Rack::Test::UploadedFile.new(File.join(ActionDispatch::IntegrationTest.fixture_path, path), type)</tt>:
+ #
+ # post :change_avatar, params: { avatar: fixture_file_upload('files/spongebob.png', 'image/png') }
+ #
+ # 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('files/spongebob.png', 'image/png', :binary) }
+ def fixture_file_upload(path, mime_type = nil, binary = false)
+ if self.class.respond_to?(:fixture_path) && self.class.fixture_path &&
+ !File.exist?(path)
+ path = File.join(self.class.fixture_path, path)
+ end
+ Rack::Test::UploadedFile.new(path, mime_type, binary)
+ end
+ end
+
+ include FixtureFile
+
def assigns(key = nil)
raise NoMethodError,
"assigns has been extracted to a gem. To continue using it,
@@ -24,20 +46,5 @@ module ActionDispatch
def redirect_to_url
@response.redirect_url
end
-
- # Shortcut for <tt>Rack::Test::UploadedFile.new(File.join(ActionDispatch::IntegrationTest.fixture_path, path), type)</tt>:
- #
- # post :change_avatar, avatar: fixture_file_upload('files/spongebob.png', 'image/png')
- #
- # To upload binary files on Windows, pass <tt>:binary</tt> as the last parameter.
- # This will not affect other platforms:
- #
- # post :change_avatar, avatar: fixture_file_upload('files/spongebob.png', 'image/png', :binary)
- def fixture_file_upload(path, mime_type = nil, binary = false)
- if self.class.respond_to?(:fixture_path) && self.class.fixture_path
- path = File.join(self.class.fixture_path, path)
- end
- Rack::Test::UploadedFile.new(path, mime_type, binary)
- end
end
end
diff --git a/actionpack/lib/action_dispatch/testing/test_request.rb b/actionpack/lib/action_dispatch/testing/test_request.rb
index ad1a7f7109..6c5b7af50e 100644
--- a/actionpack/lib/action_dispatch/testing/test_request.rb
+++ b/actionpack/lib/action_dispatch/testing/test_request.rb
@@ -1,15 +1,17 @@
-require 'active_support/core_ext/hash/indifferent_access'
-require 'rack/utils'
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/indifferent_access"
+require "rack/utils"
module ActionDispatch
class TestRequest < Request
- DEFAULT_ENV = Rack::MockRequest.env_for('/',
- 'HTTP_HOST' => 'test.host',
- 'REMOTE_ADDR' => '0.0.0.0',
- 'HTTP_USER_AGENT' => 'Rails Testing',
+ DEFAULT_ENV = Rack::MockRequest.env_for("/",
+ "HTTP_HOST" => "test.host",
+ "REMOTE_ADDR" => "0.0.0.0",
+ "HTTP_USER_AGENT" => "Rails Testing",
)
- # Create a new test request with default `env` values
+ # Create a new test request with default +env+ values.
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
@@ -22,23 +24,23 @@ module ActionDispatch
private_class_method :default_env
def request_method=(method)
- @env['REQUEST_METHOD'] = method.to_s.upcase
+ super(method.to_s.upcase)
end
def host=(host)
- @env['HTTP_HOST'] = host
+ set_header("HTTP_HOST", host)
end
def port=(number)
- @env['SERVER_PORT'] = number.to_i
+ set_header("SERVER_PORT", number.to_i)
end
def request_uri=(uri)
- @env['REQUEST_URI'] = uri
+ set_header("REQUEST_URI", uri)
end
def path=(path)
- @env['PATH_INFO'] = path
+ set_header("PATH_INFO", path)
end
def action=(action_name)
@@ -46,24 +48,24 @@ module ActionDispatch
end
def if_modified_since=(last_modified)
- @env['HTTP_IF_MODIFIED_SINCE'] = last_modified
+ set_header("HTTP_IF_MODIFIED_SINCE", last_modified)
end
def if_none_match=(etag)
- @env['HTTP_IF_NONE_MATCH'] = etag
+ set_header("HTTP_IF_NONE_MATCH", etag)
end
def remote_addr=(addr)
- @env['REMOTE_ADDR'] = addr
+ set_header("REMOTE_ADDR", addr)
end
def user_agent=(user_agent)
- @env['HTTP_USER_AGENT'] = user_agent
+ set_header("HTTP_USER_AGENT", user_agent)
end
def accept=(mime_types)
- @env.delete('action_dispatch.request.accepts')
- @env['HTTP_ACCEPT'] = Array(mime_types).collect(&:to_s).join(",")
+ delete_header("action_dispatch.request.accepts")
+ set_header("HTTP_ACCEPT", Array(mime_types).collect(&:to_s).join(","))
end
end
end
diff --git a/actionpack/lib/action_dispatch/testing/test_response.rb b/actionpack/lib/action_dispatch/testing/test_response.rb
index 9d4b73a43d..6f7c86fdcf 100644
--- a/actionpack/lib/action_dispatch/testing/test_response.rb
+++ b/actionpack/lib/action_dispatch/testing/test_response.rb
@@ -1,3 +1,7 @@
+# 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
@@ -10,19 +14,12 @@ module ActionDispatch
new response.status, response.headers, response.body
end
- # Was the response successful?
- alias_method :success?, :successful?
-
- # Was the URL not found?
- alias_method :missing?, :not_found?
-
- # Was there a server-side error?
- alias_method :error?, :server_error?
-
- attr_writer :response_parser # :nodoc:
-
def parsed_body
- @parsed_body ||= @response_parser.call(body)
+ @parsed_body ||= response_parser.call(body)
+ end
+
+ def response_parser
+ @response_parser ||= RequestEncoder.parser(content_type)
end
end
end
diff --git a/actionpack/lib/action_pack.rb b/actionpack/lib/action_pack.rb
index 941877d10d..36ee77c693 100644
--- a/actionpack/lib/action_pack.rb
+++ b/actionpack/lib/action_pack.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
#--
-# Copyright (c) 2004-2016 David Heinemeier Hansson
+# Copyright (c) 2004-2019 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@@ -21,4 +23,4 @@
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++
-require 'action_pack/version'
+require "action_pack/version"
diff --git a/actionpack/lib/action_pack/gem_version.rb b/actionpack/lib/action_pack/gem_version.rb
index 157f401f54..3bbb1734d9 100644
--- a/actionpack/lib/action_pack/gem_version.rb
+++ b/actionpack/lib/action_pack/gem_version.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionPack
# Returns the version of the currently loaded Action Pack as a <tt>Gem::Version</tt>
def self.gem_version
@@ -5,7 +7,7 @@ module ActionPack
end
module VERSION
- MAJOR = 5
+ MAJOR = 6
MINOR = 0
TINY = 0
PRE = "beta3"
diff --git a/actionpack/lib/action_pack/version.rb b/actionpack/lib/action_pack/version.rb
index 7088cd2760..fd039fe140 100644
--- a/actionpack/lib/action_pack/version.rb
+++ b/actionpack/lib/action_pack/version.rb
@@ -1,4 +1,6 @@
-require_relative 'gem_version'
+# frozen_string_literal: true
+
+require_relative "gem_version"
module ActionPack
# Returns the version of the currently loaded ActionPack as a <tt>Gem::Version</tt>