aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib
diff options
context:
space:
mode:
Diffstat (limited to 'actionpack/lib')
-rw-r--r--actionpack/lib/abstract_controller.rb2
-rw-r--r--actionpack/lib/abstract_controller/base.rb47
-rw-r--r--actionpack/lib/abstract_controller/callbacks.rb58
-rw-r--r--actionpack/lib/abstract_controller/collector.rb13
-rw-r--r--actionpack/lib/abstract_controller/helpers.rb19
-rw-r--r--actionpack/lib/abstract_controller/railties/routes_helpers.rb4
-rw-r--r--actionpack/lib/abstract_controller/rendering.rb41
-rw-r--r--actionpack/lib/abstract_controller/translation.rb15
-rw-r--r--actionpack/lib/action_controller.rb9
-rw-r--r--actionpack/lib/action_controller/api.rb146
-rw-r--r--actionpack/lib/action_controller/base.rb21
-rw-r--r--actionpack/lib/action_controller/caching.rb6
-rw-r--r--actionpack/lib/action_controller/form_builder.rb48
-rw-r--r--actionpack/lib/action_controller/log_subscriber.rb12
-rw-r--r--actionpack/lib/action_controller/metal.rb153
-rw-r--r--actionpack/lib/action_controller/metal/basic_implicit_render.rb11
-rw-r--r--actionpack/lib/action_controller/metal/conditional_get.rb124
-rw-r--r--actionpack/lib/action_controller/metal/cookies.rb2
-rw-r--r--actionpack/lib/action_controller/metal/data_streaming.rb24
-rw-r--r--actionpack/lib/action_controller/metal/etag_with_template_digest.rb50
-rw-r--r--actionpack/lib/action_controller/metal/exceptions.rb10
-rw-r--r--actionpack/lib/action_controller/metal/force_ssl.rb14
-rw-r--r--actionpack/lib/action_controller/metal/head.rb27
-rw-r--r--actionpack/lib/action_controller/metal/helpers.rb14
-rw-r--r--actionpack/lib/action_controller/metal/hide_actions.rb40
-rw-r--r--actionpack/lib/action_controller/metal/http_authentication.rb91
-rw-r--r--actionpack/lib/action_controller/metal/implicit_render.rb29
-rw-r--r--actionpack/lib/action_controller/metal/instrumentation.rb14
-rw-r--r--actionpack/lib/action_controller/metal/live.rb74
-rw-r--r--actionpack/lib/action_controller/metal/mime_responds.rb295
-rw-r--r--actionpack/lib/action_controller/metal/params_wrapper.rb24
-rw-r--r--actionpack/lib/action_controller/metal/rack_delegation.rb32
-rw-r--r--actionpack/lib/action_controller/metal/redirecting.rb9
-rw-r--r--actionpack/lib/action_controller/metal/renderers.rb36
-rw-r--r--actionpack/lib/action_controller/metal/rendering.rb44
-rw-r--r--actionpack/lib/action_controller/metal/request_forgery_protection.rb147
-rw-r--r--actionpack/lib/action_controller/metal/responder.rb297
-rw-r--r--actionpack/lib/action_controller/metal/streaming.rb8
-rw-r--r--actionpack/lib/action_controller/metal/strong_parameters.rb354
-rw-r--r--actionpack/lib/action_controller/metal/testing.rb10
-rw-r--r--actionpack/lib/action_controller/metal/url_for.rb26
-rw-r--r--actionpack/lib/action_controller/middleware.rb39
-rw-r--r--actionpack/lib/action_controller/model_naming.rb12
-rw-r--r--actionpack/lib/action_controller/renderer.rb111
-rw-r--r--actionpack/lib/action_controller/template_assertions.rb9
-rw-r--r--actionpack/lib/action_controller/test_case.rb627
-rw-r--r--actionpack/lib/action_dispatch.rb3
-rw-r--r--actionpack/lib/action_dispatch/http/cache.rb50
-rw-r--r--actionpack/lib/action_dispatch/http/filter_parameters.rb14
-rw-r--r--actionpack/lib/action_dispatch/http/filter_redirect.rb15
-rw-r--r--actionpack/lib/action_dispatch/http/headers.rb39
-rw-r--r--actionpack/lib/action_dispatch/http/mime_negotiation.rb56
-rw-r--r--actionpack/lib/action_dispatch/http/mime_type.rb147
-rw-r--r--actionpack/lib/action_dispatch/http/mime_types.rb5
-rw-r--r--actionpack/lib/action_dispatch/http/parameter_filter.rb28
-rw-r--r--actionpack/lib/action_dispatch/http/parameters.rb86
-rw-r--r--actionpack/lib/action_dispatch/http/request.rb221
-rw-r--r--actionpack/lib/action_dispatch/http/response.rb268
-rw-r--r--actionpack/lib/action_dispatch/http/upload.rb8
-rw-r--r--actionpack/lib/action_dispatch/http/url.rb165
-rw-r--r--actionpack/lib/action_dispatch/journey/formatter.rb49
-rw-r--r--actionpack/lib/action_dispatch/journey/gtg/transition_table.rb6
-rw-r--r--actionpack/lib/action_dispatch/journey/nfa/dot.rb2
-rw-r--r--actionpack/lib/action_dispatch/journey/nfa/transition_table.rb47
-rw-r--r--actionpack/lib/action_dispatch/journey/nodes/node.rb18
-rw-r--r--actionpack/lib/action_dispatch/journey/parser.rb54
-rw-r--r--actionpack/lib/action_dispatch/journey/parser.y22
-rw-r--r--actionpack/lib/action_dispatch/journey/parser_extras.rb4
-rw-r--r--actionpack/lib/action_dispatch/journey/path/pattern.rb78
-rw-r--r--actionpack/lib/action_dispatch/journey/route.rb90
-rw-r--r--actionpack/lib/action_dispatch/journey/router.rb48
-rw-r--r--actionpack/lib/action_dispatch/journey/router/strexp.rb27
-rw-r--r--actionpack/lib/action_dispatch/journey/router/utils.rb8
-rw-r--r--actionpack/lib/action_dispatch/journey/routes.rb33
-rw-r--r--actionpack/lib/action_dispatch/journey/scanner.rb10
-rw-r--r--actionpack/lib/action_dispatch/journey/visitors.rb129
-rw-r--r--actionpack/lib/action_dispatch/journey/visualizer/fsm.css4
-rw-r--r--actionpack/lib/action_dispatch/middleware/callbacks.rb2
-rw-r--r--actionpack/lib/action_dispatch/middleware/cookies.rb346
-rw-r--r--actionpack/lib/action_dispatch/middleware/debug_exceptions.rb74
-rw-r--r--actionpack/lib/action_dispatch/middleware/exception_wrapper.rb89
-rw-r--r--actionpack/lib/action_dispatch/middleware/flash.rb105
-rw-r--r--actionpack/lib/action_dispatch/middleware/load_interlock.rb21
-rw-r--r--actionpack/lib/action_dispatch/middleware/params_parser.rb58
-rw-r--r--actionpack/lib/action_dispatch/middleware/public_exceptions.rb14
-rw-r--r--actionpack/lib/action_dispatch/middleware/reloader.rb6
-rw-r--r--actionpack/lib/action_dispatch/middleware/remote_ip.rb116
-rw-r--r--actionpack/lib/action_dispatch/middleware/request_id.rb19
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/abstract_store.rb14
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/cache_store.rb21
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/cookie_store.rb50
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb4
-rw-r--r--actionpack/lib/action_dispatch/middleware/show_exceptions.rb20
-rw-r--r--actionpack/lib/action_dispatch/middleware/ssl.rb129
-rw-r--r--actionpack/lib/action_dispatch/middleware/stack.rb87
-rw-r--r--actionpack/lib/action_dispatch/middleware/static.rb127
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb16
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/_source.erb40
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb46
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb10
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb6
-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.erb25
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb1
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/routes/_route.html.erb8
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb124
-rw-r--r--actionpack/lib/action_dispatch/request/session.rb100
-rw-r--r--actionpack/lib/action_dispatch/request/utils.rb70
-rw-r--r--actionpack/lib/action_dispatch/routing.rb9
-rw-r--r--actionpack/lib/action_dispatch/routing/inspector.rb38
-rw-r--r--actionpack/lib/action_dispatch/routing/mapper.rb872
-rw-r--r--actionpack/lib/action_dispatch/routing/polymorphic_routes.rb63
-rw-r--r--actionpack/lib/action_dispatch/routing/redirection.rb6
-rw-r--r--actionpack/lib/action_dispatch/routing/route_set.rb352
-rw-r--r--actionpack/lib/action_dispatch/routing/routes_proxy.rb9
-rw-r--r--actionpack/lib/action_dispatch/routing/url_for.rb31
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions.rb18
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions/dom.rb27
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions/response.rb20
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions/routing.rb52
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions/selector.rb430
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions/tag.rb135
-rw-r--r--actionpack/lib/action_dispatch/testing/integration.rb325
-rw-r--r--actionpack/lib/action_dispatch/testing/test_process.rb8
-rw-r--r--actionpack/lib/action_dispatch/testing/test_request.rb33
-rw-r--r--actionpack/lib/action_dispatch/testing/test_response.rb9
-rw-r--r--actionpack/lib/action_pack.rb2
-rw-r--r--actionpack/lib/action_pack/gem_version.rb6
129 files changed, 4812 insertions, 4319 deletions
diff --git a/actionpack/lib/abstract_controller.rb b/actionpack/lib/abstract_controller.rb
index fe9802e395..56c4033387 100644
--- a/actionpack/lib/abstract_controller.rb
+++ b/actionpack/lib/abstract_controller.rb
@@ -1,7 +1,5 @@
require 'action_pack'
require 'active_support/rails'
-require 'active_support/core_ext/module/attr_internal'
-require 'active_support/core_ext/module/anonymous'
require 'active_support/i18n'
module AbstractController
diff --git a/actionpack/lib/abstract_controller/base.rb b/actionpack/lib/abstract_controller/base.rb
index 4026dab2ce..4501202b8c 100644
--- a/actionpack/lib/abstract_controller/base.rb
+++ b/actionpack/lib/abstract_controller/base.rb
@@ -1,8 +1,8 @@
require 'erubis'
-require 'set'
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
class Error < StandardError #:nodoc:
@@ -12,7 +12,7 @@ module AbstractController
class ActionNotFound < StandardError
end
- # <tt>AbstractController::Base</tt> is a low-level API. Nobody should be
+ # AbstractController::Base is a low-level API. Nobody should be
# using it directly, and subclasses (like ActionController::Base) are
# expected to provide their own +render+ method, since rendering means
# different things depending on the context.
@@ -57,21 +57,11 @@ module AbstractController
controller.public_instance_methods(true)
end
- # The list of hidden actions. Defaults to an empty array.
- # This can be modified by other modules or subclasses
- # to specify particular actions as hidden.
- #
- # ==== Returns
- # * <tt>Array</tt> - An array of method names that should not be considered actions.
- def hidden_actions
- []
- end
-
# A list of method names that should be considered actions. This
# includes all public instance methods on a controller, less
- # any internal methods (see #internal_methods), adding back in
+ # any internal methods (see internal_methods), adding back in
# any methods that are internal, but still exist on the class
- # itself. Finally, #hidden_actions are removed.
+ # itself.
#
# ==== Returns
# * <tt>Set</tt> - A set of all methods that should be considered actions.
@@ -82,30 +72,31 @@ 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 { |x| x.to_s } -
- # And always exclude explicitly hidden actions
- hidden_actions.to_a
+ public_instance_methods(false)).uniq.map(&:to_s)
- # Clear out AS callback method pollution
- Set.new(methods.reject { |method| method =~ /_one_time_conditions/ })
+ methods.to_set
end
end
# action_methods are cached and there is sometimes need to refresh
- # them. clear_action_methods! allows you to do that, so next time
+ # them. ::clear_action_methods! allows you to do that, so next time
# you run action_methods, they will be recalculated
def clear_action_methods!
@action_methods = nil
end
# Returns the full controller name, underscored, without the ending Controller.
- # For instance, MyApp::MyPostsController would return "my_app/my_posts" for
- # controller_path.
+ #
+ # class MyApp::MyPostsController < AbstractController::Base
+ #
+ # end
+ #
+ # MyApp::MyPostsController.controller_path # => "my_app/my_posts"
#
# ==== Returns
# * <tt>String</tt>
def controller_path
- @controller_path ||= name.sub(/Controller$/, '').underscore unless anonymous?
+ @controller_path ||= name.sub(/Controller$/, ''.freeze).underscore unless anonymous?
end
# Refresh the cached action_methods when a new action_method is added.
@@ -137,12 +128,12 @@ module AbstractController
process_action(action_name, *args)
end
- # Delegates to the class' #controller_path
+ # Delegates to the class' ::controller_path
def controller_path
self.class.controller_path
end
- # Delegates to the class' #action_methods
+ # Delegates to the class' ::action_methods
def action_methods
self.class.action_methods
end
@@ -157,9 +148,6 @@ module AbstractController
#
# ==== Parameters
# * <tt>action_name</tt> - The name of an action to be tested
- #
- # ==== Returns
- # * <tt>TrueClass</tt>, <tt>FalseClass</tt>
def available_action?(action_name)
_find_action_name(action_name).present?
end
@@ -180,9 +168,6 @@ module AbstractController
# ==== Parameters
# * <tt>name</tt> - The name of an action to be tested
#
- # ==== Returns
- # * <tt>TrueClass</tt>, <tt>FalseClass</tt>
- #
# :api: private
def action_method?(name)
self.class.action_methods.include?(name)
diff --git a/actionpack/lib/abstract_controller/callbacks.rb b/actionpack/lib/abstract_controller/callbacks.rb
index ca5c80cd71..287550db42 100644
--- a/actionpack/lib/abstract_controller/callbacks.rb
+++ b/actionpack/lib/abstract_controller/callbacks.rb
@@ -9,7 +9,7 @@ module AbstractController
included do
define_callbacks :process_action,
- terminator: ->(controller,_) { controller.response_body },
+ terminator: ->(controller, result_lambda) { result_lambda.call if result_lambda.is_a?(Proc); controller.response_body },
skip_after_callbacks_if_terminated: true
end
@@ -22,10 +22,21 @@ module AbstractController
end
module ClassMethods
- # If :only or :except are used, convert the options into the
- # :unless and :if options of ActiveSupport::Callbacks.
- # The basic idea is that :only => :index gets converted to
- # :if => proc {|c| c.action_name == "index" }.
+ # If +:only+ or +:except+ are used, convert the options into the
+ # +:if+ and +:unless+ options of ActiveSupport::Callbacks.
+ #
+ # The basic idea is that <tt>:only => :index</tt> gets converted to
+ # <tt>:if => proc {|c| c.action_name == "index" }</tt>.
+ #
+ # Note that <tt>:only</tt> has priority over <tt>:if</tt> in case they
+ # are used together.
+ #
+ # only: :index, if: -> { true } # the :if option will be ignored.
+ #
+ # Note that <tt>:if</tt> has priority over <tt>:except</tt> in case they
+ # are used together.
+ #
+ # except: :index, if: -> { true } # the :except option will be ignored.
#
# ==== Options
# * <tt>only</tt> - The callback should be run only for this action
@@ -50,11 +61,16 @@ module AbstractController
# impossible to skip a callback defined using an anonymous proc
# using #skip_action_callback
def skip_action_callback(*names)
- skip_before_action(*names)
- skip_after_action(*names)
- skip_around_action(*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
- alias_method :skip_filter, :skip_action_callback
# Take callback names and an optional callback proc, normalize them,
# then call the block with each callback. This allows us to abstract
@@ -169,14 +185,22 @@ module AbstractController
set_callback(:process_action, callback, name, options)
end
end
- alias_method :"#{callback}_filter", :"#{callback}_action"
+
+ 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))
end
end
- alias_method :"prepend_#{callback}_filter", :"prepend_#{callback}_action"
+
+ 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.
@@ -185,11 +209,19 @@ module AbstractController
skip_callback(:process_action, callback, name, options)
end
end
- alias_method :"skip_#{callback}_filter", :"skip_#{callback}_action"
+
+ 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"
- alias_method :"append_#{callback}_filter", :"#{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 ddd56b354a..55654be224 100644
--- a/actionpack/lib/abstract_controller/collector.rb
+++ b/actionpack/lib/abstract_controller/collector.rb
@@ -4,11 +4,10 @@ module AbstractController
module Collector
def self.generate_method_for_mime(mime)
sym = mime.is_a?(Symbol) ? mime : mime.to_sym
- const = sym.upcase
class_eval <<-RUBY, __FILE__, __LINE__ + 1
- def #{sym}(*args, &block) # def html(*args, &block)
- custom(Mime::#{const}, *args, &block) # custom(Mime::HTML, *args, &block)
- end # end
+ def #{sym}(*args, &block)
+ custom(Mime[:#{sym}], *args, &block)
+ end
RUBY
end
@@ -23,9 +22,7 @@ module AbstractController
protected
def method_missing(symbol, &block)
- const_name = symbol.upcase
-
- unless Mime.const_defined?(const_name)
+ 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. " \
"If you meant to respond to a variant like :tablet or :phone, not a custom format, " \
@@ -33,8 +30,6 @@ module AbstractController
"format.html { |html| html.tablet { ... } }"
end
- mime_constant = Mime.const_get(const_name)
-
if Mime::SET.include?(mime_constant)
AbstractController::Collector.generate_method_for_mime(mime_constant)
send(symbol, &block)
diff --git a/actionpack/lib/abstract_controller/helpers.rb b/actionpack/lib/abstract_controller/helpers.rb
index e77e4e01e9..d84c238a62 100644
--- a/actionpack/lib/abstract_controller/helpers.rb
+++ b/actionpack/lib/abstract_controller/helpers.rb
@@ -27,9 +27,6 @@ module AbstractController
end
module ClassMethods
- MissingHelperError = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('AbstractController::Helpers::ClassMethods::MissingHelperError',
- 'AbstractController::Helpers::MissingHelperError')
-
# When a class is inherited, wrap its helper module in a new module.
# This ensures that the parent class's module can be changed
# independently of the child class's.
@@ -153,7 +150,17 @@ module AbstractController
rescue LoadError => e
raise AbstractController::Helpers::MissingHelperError.new(e, file_name)
end
- file_name.camelize.constantize
+
+ mod_name = file_name.camelize
+ begin
+ mod_name.constantize
+ rescue LoadError
+ # dependencies.rb gives a similar error message but its wording is
+ # not as clear because it mentions autoloading. To the user all it
+ # matters is that a helper module couldn't be loaded, autoloading
+ # is an internal mechanism that should not leak.
+ raise NameError, "Couldn't find #{mod_name}, expected it to be defined in helpers/#{file_name}.rb"
+ end
when Module
arg
else
@@ -174,10 +181,10 @@ module AbstractController
end
def default_helper_module!
- module_name = name.sub(/Controller$/, '')
+ module_name = name.sub(/Controller$/, ''.freeze)
module_path = module_name.underscore
helper module_path
- rescue MissingSourceFile => e
+ 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"
diff --git a/actionpack/lib/abstract_controller/railties/routes_helpers.rb b/actionpack/lib/abstract_controller/railties/routes_helpers.rb
index 568c47e43a..14b574e322 100644
--- a/actionpack/lib/abstract_controller/railties/routes_helpers.rb
+++ b/actionpack/lib/abstract_controller/railties/routes_helpers.rb
@@ -6,9 +6,9 @@ module AbstractController
define_method(:inherited) do |klass|
super(klass)
if namespace = klass.parents.detect { |m| m.respond_to?(:railtie_routes_url_helpers) }
- klass.send(:include, namespace.railtie_routes_url_helpers(include_path_helpers))
+ klass.include(namespace.railtie_routes_url_helpers(include_path_helpers))
else
- klass.send(:include, routes.url_helpers(include_path_helpers))
+ klass.include(routes.url_helpers(include_path_helpers))
end
end
end
diff --git a/actionpack/lib/abstract_controller/rendering.rb b/actionpack/lib/abstract_controller/rendering.rb
index 9d10140ed2..a73f188623 100644
--- a/actionpack/lib/abstract_controller/rendering.rb
+++ b/actionpack/lib/abstract_controller/rendering.rb
@@ -17,24 +17,28 @@ module AbstractController
extend ActiveSupport::Concern
include ActionView::ViewPaths
- # Normalize arguments, options and then delegates render_to_body and
- # sticks the result in self.response_body.
+ # 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)
- self.response_body = render_to_body(options)
- _process_format(rendered_format, options) if rendered_format
- self.response_body
+ rendered_body = render_to_body(options)
+ if options[:html]
+ _set_html_content_type
+ else
+ _set_rendered_content_type rendered_format
+ end
+ self.response_body = rendered_body
end
# Raw rendering of a template to a string.
#
# It is similar to render, except that it does not
- # set the response_body and it should be guaranteed
+ # set the +response_body+ and it should be guaranteed
# to always return a string.
#
- # If a component extends the semantics of response_body
- # (as Action Controller extends it to be anything that
+ # If a component extends the semantics of +response_body+
+ # (as ActionController extends it to be anything that
# responds to the method each), this method needs to be
# overridden in order to still return a string.
# :api: plugin
@@ -51,14 +55,14 @@ module AbstractController
# Returns Content-Type of rendered content
# :api: public
def rendered_format
- Mime::TEXT
+ Mime[:text]
end
- DEFAULT_PROTECTED_INSTANCE_VARIABLES = Set.new %w(
+ DEFAULT_PROTECTED_INSTANCE_VARIABLES = Set.new %i(
@_action_name @_response_body @_formats @_prefixes @_config
@_view_context_class @_view_renderer @_lookup_context
@_routes @_db_runtime
- ).map(&:to_sym)
+ )
# This method should return a hash with assigns.
# You can overwrite this configuration per controller.
@@ -73,8 +77,9 @@ module AbstractController
}
end
- # Normalize args by converting render "foo" to render :action => "foo" and
- # render "foo/bar" to render :file => "foo/bar".
+ # 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={})
if action.is_a? Hash
@@ -98,7 +103,13 @@ module AbstractController
# Process the rendered format.
# :api: private
- def _process_format(format, options = {})
+ def _process_format(format)
+ end
+
+ def _set_html_content_type # :nodoc:
+ end
+
+ def _set_rendered_content_type(format) # :nodoc:
end
# Normalize args and options.
@@ -106,7 +117,7 @@ module AbstractController
def _normalize_render(*args, &block)
options = _normalize_args(*args, &block)
#TODO: remove defined? when we restore AP <=> AV dependency
- if defined?(request) && request && request.variant.present?
+ if defined?(request) && request.variant.present?
options[:variant] = request.variant
end
_normalize_options(options)
diff --git a/actionpack/lib/abstract_controller/translation.rb b/actionpack/lib/abstract_controller/translation.rb
index 02028d8e05..56b8ce895e 100644
--- a/actionpack/lib/abstract_controller/translation.rb
+++ b/actionpack/lib/abstract_controller/translation.rb
@@ -8,14 +8,15 @@ module AbstractController
# <tt>I18n.translate("people.index.foo")</tt>. This makes it less repetitive
# to translate many keys within the same controller / action and gives you a
# simple framework for scoping them consistently.
- def translate(*args)
- key = args.first
- if key.is_a?(String) && (key[0] == '.')
- key = "#{ controller_path.tr('/', '.') }.#{ action_name }#{ key }"
- args[0] = key
+ def translate(key, options = {})
+ if key.to_s.first == '.'
+ path = controller_path.tr('/', '.')
+ defaults = [:"#{path}#{key}"]
+ defaults << options[:default] if options[:default]
+ options[:default] = defaults
+ key = "#{path}.#{action_name}#{key}"
end
-
- I18n.translate(*args)
+ I18n.translate(key, options)
end
alias :t :translate
diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb
index 50bc26a80f..3d3af555c9 100644
--- a/actionpack/lib/action_controller.rb
+++ b/actionpack/lib/action_controller.rb
@@ -7,33 +7,34 @@ require 'action_controller/metal/strong_parameters'
module ActionController
extend ActiveSupport::Autoload
+ autoload :API
autoload :Base
autoload :Caching
autoload :Metal
autoload :Middleware
+ autoload :Renderer
+ autoload :FormBuilder
autoload_under "metal" do
- autoload :Compatibility
autoload :ConditionalGet
autoload :Cookies
autoload :DataStreaming
+ autoload :EtagWithTemplateDigest
autoload :Flash
autoload :ForceSSL
autoload :Head
autoload :Helpers
- autoload :HideActions
autoload :HttpAuthentication
+ autoload :BasicImplicitRender
autoload :ImplicitRender
autoload :Instrumentation
autoload :MimeResponds
autoload :ParamsWrapper
- autoload :RackDelegation
autoload :Redirecting
autoload :Renderers
autoload :Rendering
autoload :RequestForgeryProtection
autoload :Rescue
- autoload :Responder
autoload :Streaming
autoload :StrongParameters
autoload :Testing
diff --git a/actionpack/lib/action_controller/api.rb b/actionpack/lib/action_controller/api.rb
new file mode 100644
index 0000000000..1a46d49a49
--- /dev/null
+++ b/actionpack/lib/action_controller/api.rb
@@ -0,0 +1,146 @@
+require 'action_view'
+require 'action_controller'
+require 'action_controller/log_subscriber'
+
+module ActionController
+ # API Controller is a lightweight version of <tt>ActionController::Base</tt>,
+ # created for applications that don't require all functionalities that a complete
+ # \Rails controller provides, allowing you to create controllers with just the
+ # features that you need for API only applications.
+ #
+ # An API Controller is different from a normal controller in the sense that
+ # by default it doesn't include a number of features that are usually required
+ # by browser access only: layouts and templates rendering, cookies, sessions,
+ # 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.
+ #
+ # By default, only the ApplicationController in a \Rails application inherits
+ # from <tt>ActionController::API</tt>. All other controllers in turn inherit
+ # from ApplicationController.
+ #
+ # A sample controller could look like this:
+ #
+ # class PostsController < ApplicationController
+ # def index
+ # @posts = Post.all
+ # render json: @posts
+ # end
+ # end
+ #
+ # Request, response and parameters objects all work the exact same way as
+ # <tt>ActionController::Base</tt>.
+ #
+ # == Renders
+ #
+ # The default API Controller stack includes all renderers, which means you
+ # can use <tt>render :json</tt> and brothers freely in your controllers. Keep
+ # in mind that templates are not going to be rendered, so you need to ensure
+ # your controller is calling either <tt>render</tt> or <tt>redirect</tt> in
+ # all actions, otherwise it will return 204 No Content response.
+ #
+ # def show
+ # @post = Post.find(params[:id])
+ # render json: @post
+ # end
+ #
+ # == Redirects
+ #
+ # Redirects are used to move from one action to another. You can use the
+ # <tt>redirect</tt> method in your controllers in the same way as
+ # <tt>ActionController::Base</tt>. For example:
+ #
+ # def create
+ # redirect_to root_url and return if not_authorized?
+ # # do stuff here
+ # end
+ #
+ # == Adding new behavior
+ #
+ # In some scenarios you may want to add back some functionality provided by
+ # <tt>ActionController::Base</tt> that is not present by default in
+ # <tt>ActionController::API</tt>, for instance <tt>MimeResponds</tt>. This
+ # module gives you the <tt>respond_to</tt> method. Adding it is quite simple,
+ # you just need to include the module in a specific controller or in
+ # +ApplicationController+ in case you want it available in your entire
+ # application:
+ #
+ # class ApplicationController < ActionController::API
+ # include ActionController::MimeResponds
+ # end
+ #
+ # class PostsController < ApplicationController
+ # def index
+ # @posts = Post.all
+ #
+ # respond_to do |format|
+ # format.json { render json: @posts }
+ # format.xml { render xml: @posts }
+ # end
+ # end
+ # end
+ #
+ # 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.
+ class API < Metal
+ abstract!
+
+ # Shortcut helper that returns all the ActionController::API modules except
+ # the ones passed as arguments:
+ #
+ # class MyAPIBaseController < ActionController::Metal
+ # ActionController::API.without_modules(:ForceSSL, :UrlFor).each do |left|
+ # include left
+ # end
+ # end
+ #
+ # This gives better control over what you want to exclude and makes it easier
+ # to create an API controller class, instead of listing the modules required
+ # manually.
+ def self.without_modules(*modules)
+ modules = modules.map do |m|
+ m.is_a?(Symbol) ? ActionController.const_get(m) : m
+ end
+
+ MODULES - modules
+ end
+
+ MODULES = [
+ AbstractController::Rendering,
+
+ UrlFor,
+ Redirecting,
+ Rendering,
+ Renderers::All,
+ ConditionalGet,
+ BasicImplicitRender,
+ StrongParameters,
+
+ ForceSSL,
+ DataStreaming,
+
+ # Before callbacks should also be executed as early as possible, so
+ # also include them at the bottom.
+ AbstractController::Callbacks,
+
+ # Append rescue at the bottom to wrap as much as possible.
+ Rescue,
+
+ # Add instrumentations hooks at the bottom, to ensure they instrument
+ # all the methods properly.
+ Instrumentation,
+
+ # Params wrapper should come before instrumentation so they are
+ # properly showed in logs
+ ParamsWrapper
+ ]
+
+ MODULES.each do |mod|
+ include mod
+ end
+
+ ActiveSupport.run_load_hooks(:action_controller, self)
+ end
+end
diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb
index e6fe6b0b00..04e5922ce8 100644
--- a/actionpack/lib/action_controller/base.rb
+++ b/actionpack/lib/action_controller/base.rb
@@ -44,15 +44,15 @@ module ActionController
# The full request object is available via the request accessor and is primarily used to query for HTTP headers:
#
# def server_ip
- # location = request.env["SERVER_ADDR"]
+ # location = request.env["REMOTE_ADDR"]
# render plain: "This server hosted at #{location}"
# end
#
# == Parameters
#
- # All request parameters, whether they come from a GET or POST request, or from the URL, 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.
+ # 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.
#
# It's also possible to construct multi-dimensional parameter hashes by specifying keys using brackets, such as:
#
@@ -183,7 +183,7 @@ module ActionController
# Shortcut helper that returns all the modules included in
# ActionController::Base except the ones passed as arguments:
#
- # class MetalController
+ # class MyBaseController < ActionController::Metal
# ActionController::Base.without_modules(:ParamsWrapper, :Streaming).each do |left|
# include left
# end
@@ -206,14 +206,13 @@ module ActionController
AbstractController::AssetPaths,
Helpers,
- HideActions,
UrlFor,
Redirecting,
ActionView::Layouts,
Rendering,
Renderers::All,
ConditionalGet,
- RackDelegation,
+ EtagWithTemplateDigest,
Caching,
MimeResponds,
ImplicitRender,
@@ -221,6 +220,7 @@ module ActionController
Cookies,
Flash,
+ FormBuilder,
RequestForgeryProtection,
ForceSSL,
Streaming,
@@ -248,20 +248,17 @@ module ActionController
MODULES.each do |mod|
include mod
end
+ setup_renderer!
# Define some internal variables that should not be propagated to the view.
PROTECTED_IVARS = AbstractController::Rendering::DEFAULT_PROTECTED_INSTANCE_VARIABLES + [
- :@_status, :@_headers, :@_params, :@_env, :@_response, :@_request,
+ :@_params, :@_response, :@_request,
:@_view_runtime, :@_stream, :@_url_options, :@_action_has_layout ]
def _protected_ivars # :nodoc:
PROTECTED_IVARS
end
- def self.protected_instance_variables
- PROTECTED_IVARS
- end
-
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 12d798d0c1..0b8fa2ea09 100644
--- a/actionpack/lib/action_controller/caching.rb
+++ b/actionpack/lib/action_controller/caching.rb
@@ -1,6 +1,5 @@
require 'fileutils'
require 'uri'
-require 'set'
module ActionController
# \Caching is a cheap way of speeding up slow applications by keeping the result of
@@ -8,7 +7,7 @@ module ActionController
#
# You can read more about each approach by clicking the modules below.
#
- # Note: To turn off all caching, set
+ # Note: To turn off all caching provided by Action Controller, set
# config.action_controller.perform_caching = false
#
# == \Caching stores
@@ -16,7 +15,7 @@ module ActionController
# All the caching stores from ActiveSupport::Cache are available to be used as backends
# for Action Controller caching.
#
- # Configuration examples (MemoryStore is the default):
+ # Configuration examples (FileStore is the default):
#
# config.action_controller.cache_store = :memory_store
# config.action_controller.cache_store = :file_store, '/path/to/cache/directory'
@@ -46,7 +45,6 @@ module ActionController
end
end
- include RackDelegation
include AbstractController::Callbacks
include ConfigMethods
diff --git a/actionpack/lib/action_controller/form_builder.rb b/actionpack/lib/action_controller/form_builder.rb
new file mode 100644
index 0000000000..f2656ca894
--- /dev/null
+++ b/actionpack/lib/action_controller/form_builder.rb
@@ -0,0 +1,48 @@
+module ActionController
+ # Override the default form builder for all views rendered by this
+ # controller and any of its descendants. Accepts a subclass of
+ # +ActionView::Helpers::FormBuilder+.
+ #
+ # For example, given a form builder:
+ #
+ # class AdminFormBuilder < ActionView::Helpers::FormBuilder
+ # def special_field(name)
+ # end
+ # end
+ #
+ # The controller specifies a form builder as its default:
+ #
+ # class AdminAreaController < ApplicationController
+ # default_form_builder AdminFormBuilder
+ # end
+ #
+ # Then in the view any form using +form_for+ will be an instance of the
+ # specified form builder:
+ #
+ # <%= form_for(@instance) do |builder| %>
+ # <%= builder.special_field(:name) %>
+ # <% end %>
+ module FormBuilder
+ extend ActiveSupport::Concern
+
+ included do
+ class_attribute :_default_form_builder, instance_accessor: false
+ end
+
+ module ClassMethods
+ # Set the form builder to be used as the default for all forms
+ # in the views rendered by this controller and its subclasses.
+ #
+ # ==== Parameters
+ # * <tt>builder</tt> - Default form builder, an instance of +ActionView::Helpers::FormBuilder+
+ def default_form_builder(builder)
+ self._default_form_builder = builder
+ end
+ end
+
+ # Default form builder for the controller
+ def default_form_builder
+ self.class._default_form_builder
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/log_subscriber.rb b/actionpack/lib/action_controller/log_subscriber.rb
index 89fa75f025..4c9f14e409 100644
--- a/actionpack/lib/action_controller/log_subscriber.rb
+++ b/actionpack/lib/action_controller/log_subscriber.rb
@@ -1,4 +1,3 @@
-
module ActionController
class LogSubscriber < ActiveSupport::LogSubscriber
INTERNAL_PARAMS = %w(controller action format _method only_path)
@@ -26,7 +25,7 @@ module ActionController
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(" | ")})" unless additions.blank?
+ message << " (#{additions.join(" | ".freeze)})" unless additions.blank?
message
end
end
@@ -54,15 +53,6 @@ module ActionController
end
end
- def deep_munge(event)
- debug do
- "Value for params[:#{event.payload[:keys].join('][:')}] was set "\
- "to nil, because it was one of [], [null] or [null, null, ...]. "\
- "Go to http://guides.rubyonrails.org/security.html#unsafe-query-generation "\
- "for more information."\
- end
- end
-
%w(write_fragment read_fragment exist_fragment?
expire_fragment expire_page write_page).each do |method|
class_eval <<-METHOD, __FILE__, __LINE__ + 1
diff --git a/actionpack/lib/action_controller/metal.rb b/actionpack/lib/action_controller/metal.rb
index 9a427ebfdb..8e040bb465 100644
--- a/actionpack/lib/action_controller/metal.rb
+++ b/actionpack/lib/action_controller/metal.rb
@@ -1,5 +1,7 @@
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
@@ -11,22 +13,14 @@ module ActionController
#
class MiddlewareStack < ActionDispatch::MiddlewareStack #:nodoc:
class Middleware < ActionDispatch::MiddlewareStack::Middleware #:nodoc:
- def initialize(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?
- super
+ def initialize(klass, args, actions, strategy, block)
+ @actions = actions
+ @strategy = strategy
+ super(klass, args, block)
end
def valid?(action)
- if @only.present?
- @only.include?(action)
- elsif @except.present?
- !@except.include?(action)
- else
- true
- end
+ @strategy.call @actions, action
end
end
@@ -37,6 +31,32 @@ module ActionController
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
+
+ Middleware.new(get_class(klass), args, list, strategy, block)
+ end
end
# <tt>ActionController::Metal</tt> is the simplest possible controller, providing a
@@ -98,11 +118,10 @@ module ActionController
class Metal < AbstractController::Base
abstract!
- attr_internal_writer :env
-
def env
- @_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>.
@@ -114,23 +133,23 @@ module ActionController
@controller_name ||= name.demodulize.sub(/Controller$/, '').underscore
end
+ def self.make_response!(request)
+ ActionDispatch::Response.create.tap do |res|
+ res.request = request
+ end
+ end
+
# Delegates to the class' <tt>controller_name</tt>
def controller_name
self.class.controller_name
end
- # The details below can be overridden to support a specific
- # Request and Response object. The default ActionController::Base
- # implementation includes RackDelegation, which makes a request
- # and response object available. You might wish to control the
- # environment and response manually for performance reasons.
-
- attr_internal :headers, :response, :request
+ attr_internal :response, :request
delegate :session, :to => "@_request"
+ delegate :headers, :status=, :location=, :content_type=,
+ :status, :location, :content_type, :to => "@_response"
def initialize
- @_headers = {"Content-Type" => "text/html"}
- @_status = 200
@_request = nil
@_response = nil
@_routes = nil
@@ -145,58 +164,51 @@ module ActionController
@_params = val
end
- # Basic implementations for content_type=, location=, and headers are
- # provided to reduce the dependency on the RackDelegation module
- # in Renderer and Redirector.
+ alias :response_code :status # :nodoc:
- def content_type=(type)
- headers["Content-Type"] = type.to_s
- end
-
- def content_type
- headers["Content-Type"]
- end
-
- def location
- headers["Location"]
- end
-
- def location=(url)
- headers["Location"] = url
- end
-
- # basic url_for that can be overridden for more robust functionality
+ # Basic url_for that can be overridden for more robust functionality
def url_for(string)
string
end
- def status
- @_status
- end
-
- def status=(status)
- @_status = Rack::Utils.status_code(status)
- end
-
def response_body=(body)
body = [body] unless body.nil? || body.respond_to?(:each)
+ response.reset_body!
+ body.each { |part|
+ next if part.empty?
+ response.write part
+ }
super
end
+ # Tests if render or redirect has already happened.
def performed?
- response_body || (response && response.committed?)
+ response_body || response.committed?
end
- def dispatch(name, request) #:nodoc:
- @_request = request
- @_env = request.env
- @_env['action_controller.instance'] = self
+ def dispatch(name, request, response) #:nodoc:
+ set_request!(request)
+ set_response!(response)
process(name)
+ request.commit_flash
to_a
end
+ def set_response!(response) # :nodoc:
+ @_response = response
+ end
+
+ def set_request!(request) #:nodoc:
+ @_request = request
+ @_request.controller_instance = self
+ end
+
def to_a #:nodoc:
- response ? response.to_a : [status, headers, response_body]
+ response.to_a
+ end
+
+ def reset_session
+ @_request.reset_session
end
class_attribute :middleware_stack
@@ -224,20 +236,33 @@ module ActionController
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, klass = ActionDispatch::Request)
+ def self.action(name)
if middleware_stack.any?
middleware_stack.build(name) do |env|
- new.dispatch(name, klass.new(env))
+ req = ActionDispatch::Request.new(env)
+ res = make_response! req
+ new.dispatch(name, req, res)
end
else
- lambda { |env| new.dispatch(name, klass.new(env)) }
+ lambda { |env|
+ req = ActionDispatch::Request.new(env)
+ res = make_response! req
+ new.dispatch(name, req, res)
+ }
end
end
- def _status_code
- @_status
+ # Direct dispatch to the controller. Instantiates the controller, then
+ # executes the action named +name+.
+ def self.dispatch(name, req, res)
+ if middleware_stack.any?
+ middleware_stack.build(name) { |env| new.dispatch(name, req, res) }.call req.env
+ else
+ new.dispatch(name, req, res)
+ end
end
end
end
diff --git a/actionpack/lib/action_controller/metal/basic_implicit_render.rb b/actionpack/lib/action_controller/metal/basic_implicit_render.rb
new file mode 100644
index 0000000000..6c6f8381ff
--- /dev/null
+++ b/actionpack/lib/action_controller/metal/basic_implicit_render.rb
@@ -0,0 +1,11 @@
+module ActionController
+ module BasicImplicitRender
+ def send_action(method, *args)
+ super.tap { default_render unless performed? }
+ end
+
+ def default_render(*args)
+ head :no_content
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/metal/conditional_get.rb b/actionpack/lib/action_controller/metal/conditional_get.rb
index 6e0cd51d8b..89d589c486 100644
--- a/actionpack/lib/action_controller/metal/conditional_get.rb
+++ b/actionpack/lib/action_controller/metal/conditional_get.rb
@@ -4,7 +4,6 @@ module ActionController
module ConditionalGet
extend ActiveSupport::Concern
- include RackDelegation
include Head
included do
@@ -13,9 +12,9 @@ module ActionController
end
module ClassMethods
- # Allows you to consider additional controller-wide information when generating an etag.
+ # Allows you to consider additional controller-wide information when generating an ETag.
# For example, if you serve pages tailored depending on who's logged in at the moment, you
- # may want to add the current user id to be part of the etag to prevent authorized displaying
+ # may want to add the current user id to be part of the ETag to prevent unauthorized displaying
# of cached pages.
#
# class InvoicesController < ApplicationController
@@ -32,7 +31,7 @@ module ActionController
end
end
- # Sets the etag, +last_modified+, or both on the response and renders a
+ # Sets the +etag+, +last_modified+, or both on the response and renders a
# <tt>304 Not Modified</tt> response if the request is already fresh.
#
# === Parameters:
@@ -40,44 +39,63 @@ module ActionController
# * <tt>:etag</tt>.
# * <tt>:last_modified</tt>.
# * <tt>:public</tt> By default the Cache-Control header is private, set this to
- # +true+ if you want your application to be cachable by other devices (proxy caches).
+ # +true+ if you want your application to be cacheable by other devices (proxy caches).
+ # * <tt>:template</tt> By default, the template digest for the current
+ # controller/action is included in ETags. If the action renders a
+ # different template, you can include its digest instead. If the action
+ # doesn't render a template at all, you can pass <tt>template: false</tt>
+ # to skip any attempt to check for a template digest.
#
# === Example:
#
# def show
# @article = Article.find(params[:id])
- # fresh_when(etag: @article, last_modified: @article.created_at, public: true)
+ # fresh_when(etag: @article, last_modified: @article.updated_at, public: true)
# end
#
- # This will render the show template if the request isn't sending a matching etag or
+ # This will render the show template if the request isn't sending a matching ETag or
# If-Modified-Since header and just a <tt>304 Not Modified</tt> response if there's a match.
#
- # You can also just pass a record where +last_modified+ will be set by calling
- # +updated_at+ and the etag by passing the object itself.
+ # You can also just pass a record. In this case +last_modified+ will be set
+ # by calling +updated_at+ and +etag+ by passing the object itself.
#
# def show
# @article = Article.find(params[:id])
# fresh_when(@article)
# end
#
- # When passing a record, you can still set whether the public header:
+ # You can also pass an object that responds to +maximum+, such as a
+ # collection of active records. In this case +last_modified+ will be set by
+ # calling +maximum(:updated_at)+ on the collection (the timestamp of the
+ # most recently updated record) and the +etag+ by passing the object itself.
+ #
+ # def index
+ # @articles = Article.all
+ # fresh_when(@articles)
+ # end
+ #
+ # When passing a record or a collection, you can still set the public header:
#
# def show
# @article = Article.find(params[:id])
# fresh_when(@article, public: true)
# end
- def fresh_when(record_or_options, additional_options = {})
- if record_or_options.is_a? Hash
- options = record_or_options
- options.assert_valid_keys(:etag, :last_modified, :public)
- else
- record = record_or_options
- options = { etag: record, last_modified: record.try(:updated_at) }.merge!(additional_options)
+ #
+ # When rendering a different template than the default controller/action
+ # style, you can indicate which digest to include in the ETag:
+ #
+ # before_action { fresh_when @article, template: 'widgets/show' }
+ #
+ def fresh_when(object = nil, etag: object, last_modified: nil, public: false, template: nil)
+ last_modified ||= object.try(:updated_at) || object.try(:maximum, :updated_at)
+
+ if etag || template
+ response.etag = combine_etags(etag: etag, last_modified: last_modified,
+ public: public, template: template)
end
- response.etag = combine_etags(options[:etag]) if options[:etag]
- response.last_modified = options[:last_modified] if options[:last_modified]
- response.cache_control[:public] = true if options[:public]
+ response.last_modified = last_modified if last_modified
+ response.cache_control[:public] = true if public
head :not_modified if request.fresh?(response)
end
@@ -92,14 +110,19 @@ module ActionController
# * <tt>:etag</tt>.
# * <tt>:last_modified</tt>.
# * <tt>:public</tt> By default the Cache-Control header is private, set this to
- # +true+ if you want your application to be cachable by other devices (proxy caches).
+ # +true+ if you want your application to be cacheable by other devices (proxy caches).
+ # * <tt>:template</tt> By default, the template digest for the current
+ # controller/action is included in ETags. If the action renders a
+ # different template, you can include its digest instead. If the action
+ # doesn't render a template at all, you can pass <tt>template: false</tt>
+ # to skip any attempt to check for a template digest.
#
# === Example:
#
# def show
# @article = Article.find(params[:id])
#
- # if stale?(etag: @article, last_modified: @article.created_at)
+ # if stale?(etag: @article, last_modified: @article.updated_at)
# @statistics = @article.really_expensive_call
# respond_to do |format|
# # all the supported formats
@@ -107,8 +130,8 @@ module ActionController
# end
# end
#
- # You can also just pass a record where +last_modified+ will be set by calling
- # updated_at and the etag by passing the object itself.
+ # You can also just pass a record. In this case +last_modified+ will be set
+ # by calling +updated_at+ and +etag+ by passing the object itself.
#
# def show
# @article = Article.find(params[:id])
@@ -121,7 +144,23 @@ module ActionController
# end
# end
#
- # When passing a record, you can still set whether the public header:
+ # You can also pass an object that responds to +maximum+, such as a
+ # collection of active records. In this case +last_modified+ will be set by
+ # calling +maximum(:updated_at)+ on the collection (the timestamp of the
+ # most recently updated record) and the +etag+ by passing the object itself.
+ #
+ # def index
+ # @articles = Article.all
+ #
+ # if stale?(@articles)
+ # @statistics = @articles.really_expensive_call
+ # respond_to do |format|
+ # # all the supported formats
+ # end
+ # end
+ # end
+ #
+ # When passing a record or a collection, you can still set the public header:
#
# def show
# @article = Article.find(params[:id])
@@ -133,8 +172,16 @@ module ActionController
# end
# end
# end
- def stale?(record_or_options, additional_options = {})
- fresh_when(record_or_options, additional_options)
+ #
+ # When rendering a different template than the default controller/action
+ # style, you can indicate which digest to include in the ETag:
+ #
+ # def show
+ # super if stale? @article, template: 'widgets/show'
+ # end
+ #
+ def stale?(object = nil, etag: object, last_modified: nil, public: nil, template: nil)
+ fresh_when(object, etag: etag, last_modified: last_modified, public: public, template: template)
!request.fresh?(response)
end
@@ -167,9 +214,28 @@ module ActionController
response.cache_control.replace(:no_cache => true)
end
+ # Cache or yield the block. The cache is supposed to never expire.
+ #
+ # You can use this method when you have a HTTP response that never changes,
+ # and the browser and proxies should cache it indefinitely.
+ #
+ # * +public+: By default, HTTP responses are private, cached only on the
+ # user's web browser. To allow proxies to cache the response, set +true+ to
+ # indicate that they can serve the cached response to all users.
+ #
+ # * +version+: the version passed as a key for the cache.
+ def http_cache_forever(public: false, version: 'v1')
+ expires_in 100.years, public: public
+
+ yield if stale?(etag: "#{version}-#{request.fullpath}",
+ last_modified: Time.parse('2011-01-01').utc,
+ public: public)
+ end
+
private
- def combine_etags(etag)
- [ etag, *etaggers.map { |etagger| instance_exec(&etagger) }.compact ]
+ def combine_etags(options)
+ etags = etaggers.map { |etagger| instance_exec(options, &etagger) }.compact
+ etags.unshift options[:etag]
end
end
end
diff --git a/actionpack/lib/action_controller/metal/cookies.rb b/actionpack/lib/action_controller/metal/cookies.rb
index d787f014cd..f8efb2b076 100644
--- a/actionpack/lib/action_controller/metal/cookies.rb
+++ b/actionpack/lib/action_controller/metal/cookies.rb
@@ -2,8 +2,6 @@ module ActionController #:nodoc:
module Cookies
extend ActiveSupport::Concern
- include RackDelegation
-
included do
helper_method :cookies
end
diff --git a/actionpack/lib/action_controller/metal/data_streaming.rb b/actionpack/lib/action_controller/metal/data_streaming.rb
index 1abd8d3a33..957e7a3019 100644
--- a/actionpack/lib/action_controller/metal/data_streaming.rb
+++ b/actionpack/lib/action_controller/metal/data_streaming.rb
@@ -72,27 +72,7 @@ module ActionController #:nodoc:
self.status = options[:status] || 200
self.content_type = options[:content_type] if options.key?(:content_type)
- self.response_body = FileBody.new(path)
- end
-
- # Avoid having to pass an open file handle as the response body.
- # Rack::Sendfile will usually intercept the response and uses
- # the path directly, so there is no reason to open the file.
- class FileBody #:nodoc:
- attr_reader :to_path
-
- def initialize(path)
- @to_path = path
- end
-
- # Stream the file's contents if Rack::Sendfile isn't present.
- def each
- File.open(to_path, 'rb') do |file|
- while chunk = file.read(16384)
- yield chunk
- end
- end
- end
+ response.send_file path
end
# Sends the given binary data to the browser. This method is similar to
@@ -126,7 +106,7 @@ module ActionController #:nodoc:
# See +send_file+ for more information on HTTP Content-* headers and caching.
def send_data(data, options = {}) #:doc:
send_file_headers! options
- render options.slice(:status, :content_type).merge(:text => data)
+ render options.slice(:status, :content_type).merge(body: data)
end
private
diff --git a/actionpack/lib/action_controller/metal/etag_with_template_digest.rb b/actionpack/lib/action_controller/metal/etag_with_template_digest.rb
new file mode 100644
index 0000000000..669cf55bca
--- /dev/null
+++ b/actionpack/lib/action_controller/metal/etag_with_template_digest.rb
@@ -0,0 +1,50 @@
+module ActionController
+ # When our views change, they should bubble up into HTTP cache freshness
+ # and bust browser caches. So the template digest for the current action
+ # is automatically included in the ETag.
+ #
+ # Enabled by default for apps that use Action View. Disable by setting
+ #
+ # config.action_controller.etag_with_template_digest = false
+ #
+ # Override the template to digest by passing +:template+ to +fresh_when+
+ # and +stale?+ calls. For example:
+ #
+ # # We're going to render widgets/show, not posts/show
+ # fresh_when @post, template: 'widgets/show'
+ #
+ # # We're not going to render a template, so omit it from the ETag.
+ # fresh_when @post, template: false
+ #
+ module EtagWithTemplateDigest
+ extend ActiveSupport::Concern
+
+ include ActionController::ConditionalGet
+
+ included do
+ class_attribute :etag_with_template_digest
+ self.etag_with_template_digest = true
+
+ ActiveSupport.on_load :action_view, yield: true do
+ etag do |options|
+ determine_template_etag(options) if etag_with_template_digest
+ end
+ end
+ end
+
+ private
+ def determine_template_etag(options)
+ if template = pick_template_for_etag(options)
+ lookup_and_digest_template(template)
+ end
+ end
+
+ def pick_template_for_etag(options)
+ options.fetch(:template) { "#{controller_name}/#{action_name}" }
+ end
+
+ def lookup_and_digest_template(template)
+ ActionView::Digestor.digest name: template, finder: lookup_context
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/metal/exceptions.rb b/actionpack/lib/action_controller/metal/exceptions.rb
index 3844dbf2a6..5260dc0336 100644
--- a/actionpack/lib/action_controller/metal/exceptions.rb
+++ b/actionpack/lib/action_controller/metal/exceptions.rb
@@ -5,12 +5,10 @@ module ActionController
class BadRequest < ActionControllerError #:nodoc:
attr_reader :original_exception
- def initialize(type = nil, e = nil)
- return super() unless type && e
-
- super("Invalid #{type} parameters: #{e.message}")
+ def initialize(msg = nil, e = nil)
+ super(msg)
@original_exception = e
- set_backtrace e.backtrace
+ set_backtrace e.backtrace if e
end
end
@@ -25,7 +23,7 @@ module ActionController
end
end
- class ActionController::UrlGenerationError < RoutingError #:nodoc:
+ class ActionController::UrlGenerationError < ActionControllerError #:nodoc:
end
class MethodNotAllowed < ActionControllerError #:nodoc:
diff --git a/actionpack/lib/action_controller/metal/force_ssl.rb b/actionpack/lib/action_controller/metal/force_ssl.rb
index a2cb6d1e66..e31d65aac2 100644
--- a/actionpack/lib/action_controller/metal/force_ssl.rb
+++ b/actionpack/lib/action_controller/metal/force_ssl.rb
@@ -55,10 +55,10 @@ module ActionController
# 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.
+ # * <tt>if</tt> - A symbol naming an instance method or a proc; the
+ # callback will be called only when it returns a true value.
+ # * <tt>unless</tt> - A symbol naming an instance method or a proc; the
+ # callback will be called only when it returns a false value.
def force_ssl(options = {})
action_options = options.slice(*ACTION_OPTIONS)
redirect_options = options.except(*ACTION_OPTIONS)
@@ -71,8 +71,8 @@ module ActionController
# 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.
+ # * <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 = {
@@ -85,7 +85,7 @@ module ActionController
if host_or_options.is_a?(Hash)
options.merge!(host_or_options)
elsif host_or_options
- options.merge!(:host => host_or_options)
+ options[:host] = host_or_options
end
secure_url = ActionDispatch::Http::URL.url_for(options.slice(*URL_OPTIONS))
diff --git a/actionpack/lib/action_controller/metal/head.rb b/actionpack/lib/action_controller/metal/head.rb
index 3d2badf9c2..b2110bf946 100644
--- a/actionpack/lib/action_controller/metal/head.rb
+++ b/actionpack/lib/action_controller/metal/head.rb
@@ -17,8 +17,18 @@ module ActionController
#
# See Rack::Utils::SYMBOL_TO_STATUS_CODE for a full list of valid +status+ symbols.
def head(status, options = {})
- options, status = status, nil if status.is_a?(Hash)
- status ||= options.delete(:status) || :ok
+ 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
+ end
+
+ status ||= :ok
+
location = options.delete(:location)
content_type = options.delete(:content_type)
@@ -29,15 +39,14 @@ module ActionController
self.status = status
self.location = url_for(location) if location
- if include_content?(self._status_code)
+ self.response_body = ""
+
+ if include_content?(self.response_code)
self.content_type = content_type || (Mime[formats.first] if formats)
- self.response.charset = false if self.response
- self.response_body = " "
- else
- headers.delete('Content-Type')
- headers.delete('Content-Length')
- self.response_body = ""
+ self.response.charset = false
end
+
+ true
end
private
diff --git a/actionpack/lib/action_controller/metal/helpers.rb b/actionpack/lib/action_controller/metal/helpers.rb
index a9c3e438fb..d3853e2e83 100644
--- a/actionpack/lib/action_controller/metal/helpers.rb
+++ b/actionpack/lib/action_controller/metal/helpers.rb
@@ -7,8 +7,8 @@ module ActionController
# extract complicated logic or reusable functionality is strongly encouraged. By default, each controller
# will include all helpers. These helpers are only accessible on the controller through <tt>.helpers</tt>
#
- # In previous versions of \Rails the controller will include a helper whose
- # name matches that of the controller, e.g., <tt>MyController</tt> will automatically
+ # In previous versions of \Rails the controller will include a helper which
+ # matches the name of the controller, e.g., <tt>MyController</tt> will automatically
# include <tt>MyHelper</tt>. To return old behavior set +config.action_controller.include_all_helpers+ to +false+.
#
# Additional helpers can be specified using the +helper+ class method in ActionController::Base or any
@@ -44,7 +44,7 @@ module ActionController
# the output might look like this:
#
# 23 Aug 11:30 | Carolina Railhawks Soccer Match
- # N/A | Carolina Railhaws Training Workshop
+ # N/A | Carolina Railhawks Training Workshop
#
module Helpers
extend ActiveSupport::Concern
@@ -73,7 +73,7 @@ module ActionController
# Provides a proxy to access helpers methods from outside the view.
def helpers
- @helper_proxy ||= begin
+ @helper_proxy ||= begin
proxy = ActionView::Base.new
proxy.config = config.inheritable_copy
proxy.extend(_helpers)
@@ -93,10 +93,14 @@ module ActionController
super(args)
end
+ # Returns a list of helper names in a given path.
+ #
+ # ActionController::Base.all_helpers_from_path 'app/helpers'
+ # # => ["application", "chart", "rubygems"]
def all_helpers_from_path(path)
helpers = Array(path).flat_map do |_path|
extract = /^#{Regexp.quote(_path.to_s)}\/?(.*)_helper.rb$/
- names = Dir["#{_path}/**/*_helper.rb"].map { |file| file.sub(extract, '\1') }
+ names = Dir["#{_path}/**/*_helper.rb"].map { |file| file.sub(extract, '\1'.freeze) }
names.sort!
end
helpers.uniq!
diff --git a/actionpack/lib/action_controller/metal/hide_actions.rb b/actionpack/lib/action_controller/metal/hide_actions.rb
deleted file mode 100644
index af36ffa240..0000000000
--- a/actionpack/lib/action_controller/metal/hide_actions.rb
+++ /dev/null
@@ -1,40 +0,0 @@
-
-module ActionController
- # Adds the ability to prevent public methods on a controller to be called as actions.
- module HideActions
- extend ActiveSupport::Concern
-
- included do
- class_attribute :hidden_actions
- self.hidden_actions = Set.new.freeze
- end
-
- private
-
- # Overrides AbstractController::Base#action_method? to return false if the
- # action name is in the list of hidden actions.
- def method_for_action(action_name)
- self.class.visible_action?(action_name) && super
- end
-
- module ClassMethods
- # Sets all of the actions passed in as hidden actions.
- #
- # ==== Parameters
- # * <tt>args</tt> - A list of actions
- def hide_action(*args)
- self.hidden_actions = hidden_actions.dup.merge(args.map(&:to_s)).freeze
- end
-
- def visible_action?(action_name)
- not hidden_actions.include?(action_name)
- end
-
- # Overrides AbstractController::Base#action_methods to remove any methods
- # that are listed as hidden methods.
- def action_methods
- @action_methods ||= Set.new(super.reject { |name| hidden_actions.include?(name) }).freeze
- end
- end
- end
-end
diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb
index 25c123edf7..0a36fecd27 100644
--- a/actionpack/lib/action_controller/metal/http_authentication.rb
+++ b/actionpack/lib/action_controller/metal/http_authentication.rb
@@ -34,7 +34,7 @@ module ActionController
#
# def authenticate
# case request.format
- # when Mime::XML, Mime::ATOM
+ # when Mime[:xml], Mime[:atom]
# if user = authenticate_with_http_basic { |u, p| @account.users.authenticate(u, p) }
# @current_user = user
# else
@@ -53,10 +53,8 @@ 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::Basic.encode_credentials(users(:dhh).name, users(:dhh).password)
- # )
+ # @request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials(users(:dhh).name, users(:dhh).password)
+ # get "/notes/1.xml"
#
# assert_equal 200, status
# end
@@ -76,16 +74,16 @@ module ActionController
end
end
- def authenticate_or_request_with_http_basic(realm = "Application", &login_procedure)
- authenticate_with_http_basic(&login_procedure) || request_http_basic_authentication(realm)
+ def authenticate_or_request_with_http_basic(realm = "Application", message = nil, &login_procedure)
+ authenticate_with_http_basic(&login_procedure) || request_http_basic_authentication(realm, message)
end
def authenticate_with_http_basic(&login_procedure)
HttpAuthentication::Basic.authenticate(request, &login_procedure)
end
- def request_http_basic_authentication(realm = "Application")
- HttpAuthentication::Basic.authentication_request(self, realm)
+ def request_http_basic_authentication(realm = "Application", message = nil)
+ HttpAuthentication::Basic.authentication_request(self, realm, message)
end
end
@@ -96,7 +94,7 @@ module ActionController
end
def has_basic_credentials?(request)
- request.authorization.present? && (auth_scheme(request) == 'Basic')
+ request.authorization.present? && (auth_scheme(request).downcase == 'basic')
end
def user_name_and_password(request)
@@ -108,21 +106,22 @@ module ActionController
end
def auth_scheme(request)
- request.authorization.split(' ', 2).first
+ request.authorization.to_s.split(' ', 2).first
end
def auth_param(request)
- request.authorization.split(' ', 2).second
+ request.authorization.to_s.split(' ', 2).second
end
def encode_credentials(user_name, password)
"Basic #{::Base64.strict_encode64("#{user_name}:#{password}")}"
end
- def authentication_request(controller, realm)
- controller.headers["WWW-Authenticate"] = %(Basic realm="#{realm.gsub(/"/, "")}")
+ def authentication_request(controller, realm, message)
+ message ||= "HTTP Basic: Access denied.\n"
+ controller.headers["WWW-Authenticate"] = %(Basic realm="#{realm.tr('"'.freeze, "".freeze)}")
controller.status = 401
- controller.response_body = "HTTP Basic: Access denied.\n"
+ controller.response_body = message
end
end
@@ -172,8 +171,8 @@ module ActionController
extend self
module ControllerMethods
- def authenticate_or_request_with_http_digest(realm = "Application", &password_procedure)
- authenticate_with_http_digest(realm, &password_procedure) || request_http_digest_authentication(realm)
+ def authenticate_or_request_with_http_digest(realm = "Application", message = nil, &password_procedure)
+ authenticate_with_http_digest(realm, &password_procedure) || request_http_digest_authentication(realm, message)
end
# Authenticate with HTTP Digest, returns true or false
@@ -204,7 +203,7 @@ module ActionController
password = password_procedure.call(credentials[:username])
return false unless password
- method = request.env['rack.methodoverride.original_method'] || request.env['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|
@@ -261,8 +260,8 @@ module ActionController
end
def secret_token(request)
- key_generator = request.env["action_dispatch.key_generator"]
- http_auth_salt = request.env["action_dispatch.http_auth_salt"]
+ key_generator = request.key_generator
+ http_auth_salt = request.http_auth_salt
key_generator.generate_key(http_auth_salt)
end
@@ -316,7 +315,7 @@ module ActionController
nonce(secret_key, t) == value && (t - Time.now.to_i).abs <= seconds_to_timeout
end
- # Opaque based on random generation - but changing each request?
+ # Opaque based on digest of secret key
def opaque(secret_key)
::Digest::MD5.hexdigest(secret_key)
end
@@ -362,7 +361,7 @@ module ActionController
#
# def authenticate
# case request.format
- # when Mime::XML, Mime::ATOM
+ # when Mime[:xml], Mime[:atom]
# if user = authenticate_with_http_token { |t, o| @account.users.authenticate(t, o) }
# @current_user = user
# else
@@ -397,21 +396,22 @@ module ActionController
#
# RewriteRule ^(.*)$ dispatch.fcgi [E=X-HTTP_AUTHORIZATION:%{HTTP:Authorization},QSA,L]
module Token
- TOKEN_REGEX = /^Token /
+ TOKEN_KEY = 'token='
+ TOKEN_REGEX = /^(Token|Bearer) /
AUTHN_PAIR_DELIMITERS = /(?:,|;|\t+)/
extend self
module ControllerMethods
- def authenticate_or_request_with_http_token(realm = "Application", &login_procedure)
- authenticate_with_http_token(&login_procedure) || request_http_token_authentication(realm)
+ def authenticate_or_request_with_http_token(realm = "Application", message = nil, &login_procedure)
+ authenticate_with_http_token(&login_procedure) || request_http_token_authentication(realm, message)
end
def authenticate_with_http_token(&login_procedure)
Token.authenticate(self, &login_procedure)
end
- def request_http_token_authentication(realm = "Application")
- Token.authentication_request(self, realm)
+ def request_http_token_authentication(realm = "Application", message = nil)
+ Token.authentication_request(self, realm, message)
end
end
@@ -436,15 +436,17 @@ module ActionController
end
end
- # Parses the token and options out of the token authorization header. If
- # the header looks like this:
+ # Parses the token and options out of the token authorization header.
+ # The value for the Authorization header is expected to have the prefix
+ # <tt>"Token"</tt> or <tt>"Bearer"</tt>. If the header looks like this:
# Authorization: Token token="abc", nonce="def"
- # Then the returned token is "abc", and the options is {nonce: "def"}
+ # Then the returned token is <tt>"abc"</tt>, and the options are
+ # <tt>{nonce: "def"}</tt>
#
# request - ActionDispatch::Request instance with the current headers.
#
- # Returns an Array of [String, Hash] if a token is present.
- # Returns nil if no token is found.
+ # Returns an +Array+ of <tt>[String, Hash]</tt> if a token is present.
+ # Returns +nil+ if no token is found.
def token_and_options(request)
authorization_request = request.authorization.to_s
if authorization_request[TOKEN_REGEX]
@@ -462,16 +464,22 @@ module ActionController
raw_params.map { |param| param.split %r/=(.+)?/ }
end
- # This removes the `"` characters wrapping the value.
+ # This removes the <tt>"</tt> characters wrapping the value.
def rewrite_param_values(array_params)
array_params.each { |param| (param[1] || "").gsub! %r/^"|"$/, '' }
end
# This method takes an authorization body and splits up the key-value
- # pairs by the standardized `:`, `;`, or `\t` delimiters defined in
- # `AUTHN_PAIR_DELIMITERS`.
+ # pairs by the standardized <tt>:</tt>, <tt>;</tt>, or <tt>\t</tt>
+ # delimiters defined in +AUTHN_PAIR_DELIMITERS+.
def raw_params(auth)
- 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}"
+ end
+
+ _raw_params
end
# Encodes the given token and options into an Authorization header value.
@@ -481,21 +489,22 @@ module ActionController
#
# Returns String.
def encode_credentials(token, options = {})
- values = ["token=#{token.to_s.inspect}"] + options.map do |key, value|
+ values = ["#{TOKEN_KEY}#{token.to_s.inspect}"] + options.map do |key, value|
"#{key}=#{value.to_s.inspect}"
end
"Token #{values * ", "}"
end
- # Sets a WWW-Authenticate to let the client know a token is desired.
+ # Sets a WWW-Authenticate header to let the client know a token is desired.
#
# controller - ActionController::Base instance for the outgoing response.
# realm - String realm to use in the header.
#
# Returns nothing.
- def authentication_request(controller, realm)
- controller.headers["WWW-Authenticate"] = %(Token realm="#{realm.gsub(/"/, "")}")
- controller.__send__ :render, :text => "HTTP Token: Access denied.\n", :status => :unauthorized
+ def authentication_request(controller, realm, message = nil)
+ message ||= "HTTP Token: Access denied.\n"
+ controller.headers["WWW-Authenticate"] = %(Token realm="#{realm.tr('"'.freeze, "".freeze)}")
+ controller.__send__ :render, plain: message, status: :unauthorized
end
end
end
diff --git a/actionpack/lib/action_controller/metal/implicit_render.rb b/actionpack/lib/action_controller/metal/implicit_render.rb
index ae04b53825..17fcc2fa02 100644
--- a/actionpack/lib/action_controller/metal/implicit_render.rb
+++ b/actionpack/lib/action_controller/metal/implicit_render.rb
@@ -1,13 +1,30 @@
module ActionController
module ImplicitRender
- def send_action(method, *args)
- ret = super
- default_render unless performed?
- ret
- end
+ include BasicImplicitRender
+
+ # Renders the template corresponding to the controller action, if it exists.
+ # The action name, format, and variant are all taken into account.
+ # For example, the "new" action with an HTML format and variant "phone"
+ # would try to render the <tt>new.html+phone.erb</tt> template.
+ #
+ # If no template is found <tt>ActionController::BasicImplicitRender</tt>'s implementation is called, unless
+ # a block is passed. In that case, it will override the super implementation.
+ #
+ # default_render do
+ # head 404 # No template was found
+ # end
def default_render(*args)
- render(*args)
+ if template_exists?(action_name.to_s, _prefixes, variants: request.variant)
+ render(*args)
+ else
+ if block_given?
+ yield(*args)
+ else
+ logger.info "No template found for #{self.class.name}\##{action_name}, rendering head :no_content" if logger
+ super
+ end
+ end
end
def method_for_action(action_name)
diff --git a/actionpack/lib/action_controller/metal/instrumentation.rb b/actionpack/lib/action_controller/metal/instrumentation.rb
index b0e164bc57..3dbf34eb2a 100644
--- a/actionpack/lib/action_controller/metal/instrumentation.rb
+++ b/actionpack/lib/action_controller/metal/instrumentation.rb
@@ -11,7 +11,6 @@ module ActionController
extend ActiveSupport::Concern
include AbstractController::Logger
- include ActionController::RackDelegation
attr_internal :view_runtime
@@ -21,17 +20,20 @@ module ActionController
:action => self.action_name,
:params => request.filtered_parameters,
:format => request.format.try(:ref),
- :method => request.method,
+ :method => request.request_method,
:path => (request.fullpath rescue "unknown")
}
ActiveSupport::Notifications.instrument("start_processing.action_controller", raw_payload.dup)
ActiveSupport::Notifications.instrument("process_action.action_controller", raw_payload) do |payload|
- result = super
- payload[:status] = response.status
- append_info_to_payload(payload)
- result
+ begin
+ result = super
+ payload[:status] = response.status
+ result
+ ensure
+ append_info_to_payload(payload)
+ end
end
end
diff --git a/actionpack/lib/action_controller/metal/live.rb b/actionpack/lib/action_controller/metal/live.rb
index 706ce04062..7db8d13e24 100644
--- a/actionpack/lib/action_controller/metal/live.rb
+++ b/actionpack/lib/action_controller/metal/live.rb
@@ -33,6 +33,20 @@ module ActionController
# the main thread. Make sure your actions are thread safe, and this shouldn't
# be a problem (don't share state across threads, etc).
module Live
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ def make_response!(request)
+ if request.env["HTTP_VERSION"] == "HTTP/1.0"
+ super
+ else
+ Live::Response.new.tap do |res|
+ res.request = request
+ end
+ end
+ end
+ end
+
# This class provides the ability to write an SSE (Server Sent Event)
# to an IO stream. The class is initialized with a stream and can be used
# to either write a JSON string or an object which can be converted to JSON.
@@ -102,7 +116,7 @@ module ActionController
end
end
- message = json.gsub("\n", "\ndata: ")
+ message = json.gsub("\n".freeze, "\ndata: ".freeze)
@stream.write "data: #{message}\n\n"
end
end
@@ -131,8 +145,8 @@ module ActionController
def write(string)
unless @response.committed?
- @response.headers["Cache-Control"] = "no-cache"
- @response.headers.delete "Content-Length"
+ @response.set_header "Cache-Control", "no-cache"
+ @response.delete_header "Content-Length"
end
super
@@ -189,12 +203,6 @@ module ActionController
!@aborted
end
- def await_close
- synchronize do
- @cv.wait_until { @closed }
- end
- end
-
def on_error(&block)
@error_callback = block
end
@@ -205,29 +213,6 @@ module ActionController
end
class Response < ActionDispatch::Response #:nodoc: all
- class Header < DelegateClass(Hash) # :nodoc:
- def initialize(response, header)
- @response = response
- super(header)
- end
-
- def []=(k,v)
- if @response.committed?
- raise ActionDispatch::IllegalStateError, 'header already sent'
- end
-
- super
- end
-
- def merge(other)
- self.class.new @response, __getobj__.merge(other)
- end
-
- def to_hash
- __getobj__.dup
- end
- end
-
private
def before_committed
@@ -248,14 +233,6 @@ module ActionController
body.each { |part| buf.write part }
buf
end
-
- def merge_default_headers(original, default)
- Header.new self, super
- end
-
- def handle_conditional_get!
- super unless committed?
- end
end
def process(name)
@@ -303,10 +280,12 @@ module ActionController
logger = ActionController::Base.logger
return unless logger
- 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 ")
- logger.fatal("#{message}\n\n")
+ 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"
+ end
end
def response_body=(body)
@@ -315,12 +294,7 @@ module ActionController
end
def set_response!(request)
- if request.env["HTTP_VERSION"] == "HTTP/1.0"
- super
- else
- @_response = Live::Response.new
- @_response.request = request
- end
+ @_response = self.class.make_response! request
end
end
end
diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb
index 00e7e980f8..6e346fadfe 100644
--- a/actionpack/lib/action_controller/metal/mime_responds.rb
+++ b/actionpack/lib/action_controller/metal/mime_responds.rb
@@ -1,62 +1,7 @@
-require 'active_support/core_ext/array/extract_options'
require 'abstract_controller/collector'
module ActionController #:nodoc:
module MimeResponds
- extend ActiveSupport::Concern
-
- included do
- class_attribute :responder, :mimes_for_respond_to
- self.responder = ActionController::Responder
- clear_respond_to
- end
-
- module ClassMethods
- # Defines mime types that are rendered by default when invoking
- # <tt>respond_with</tt>.
- #
- # respond_to :html, :xml, :json
- #
- # Specifies that all actions in the controller respond to requests
- # for <tt>:html</tt>, <tt>:xml</tt> and <tt>:json</tt>.
- #
- # To specify on per-action basis, use <tt>:only</tt> and
- # <tt>:except</tt> with an array of actions or a single action:
- #
- # respond_to :html
- # respond_to :xml, :json, except: [ :edit ]
- #
- # This specifies that all actions respond to <tt>:html</tt>
- # and all actions except <tt>:edit</tt> respond to <tt>:xml</tt> and
- # <tt>:json</tt>.
- #
- # respond_to :json, only: :create
- #
- # This specifies that the <tt>:create</tt> action and no other responds
- # to <tt>:json</tt>.
- def respond_to(*mimes)
- options = mimes.extract_options!
-
- only_actions = Array(options.delete(:only)).map(&:to_s)
- except_actions = Array(options.delete(:except)).map(&:to_s)
-
- new = mimes_for_respond_to.dup
- mimes.each do |mime|
- mime = mime.to_sym
- new[mime] = {}
- new[mime][:only] = only_actions unless only_actions.empty?
- new[mime][:except] = except_actions unless except_actions.empty?
- end
- self.mimes_for_respond_to = new.freeze
- end
-
- # Clear all mime types in <tt>respond_to</tt>.
- #
- def clear_respond_to
- self.mimes_for_respond_to = Hash.new.freeze
- end
- end
-
# Without web-service support, an action which collects the data for displaying a list of people
# might look something like this:
#
@@ -146,11 +91,11 @@ module ActionController #:nodoc:
# and accept Rails' defaults, life will be much easier.
#
# If you need to use a MIME type which isn't supported by default, you can register your own handlers in
- # config/initializers/mime_types.rb as follows.
+ # +config/initializers/mime_types.rb+ as follows.
#
# Mime::Type.register "image/jpg", :jpg
#
- # Respond to also allows you to specify a common block for different formats by using any:
+ # Respond to also allows you to specify a common block for different formats by using +any+:
#
# def index
# @people = Person.all
@@ -169,18 +114,6 @@ module ActionController #:nodoc:
#
# render json: @people
#
- # Since this is a common pattern, you can use the class method respond_to
- # with the respond_with method to have the same results:
- #
- # class PeopleController < ApplicationController
- # respond_to :html, :xml, :json
- #
- # def index
- # @people = Person.all
- # respond_with(@people)
- # end
- # end
- #
# Formats can have different variants.
#
# The request variant is a specialization of the request format, like <tt>:tablet</tt>,
@@ -217,22 +150,22 @@ module ActionController #:nodoc:
# format.html.phone { redirect_to progress_path }
# format.html.none { render "trash" }
# end
- #
- # Variants also support common `any`/`all` block that formats have.
+ #
+ # Variants also support common +any+/+all+ block that formats have.
#
# It works for both inline:
#
# respond_to do |format|
- # format.html.any { render text: "any" }
- # format.html.phone { render text: "phone" }
+ # format.html.any { render html: "any" }
+ # format.html.phone { render html: "phone" }
# end
#
# and block syntax:
#
# respond_to do |format|
# format.html do |variant|
- # variant.any(:tablet, :phablet){ render text: "any" }
- # variant.phone { render text: "phone" }
+ # variant.any(:tablet, :phablet){ render html: "any" }
+ # variant.phone { render html: "phone" }
# end
# end
#
@@ -241,201 +174,26 @@ 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:
+ # +:tablet+ variant declared, +:phone+ variant will be picked:
#
# respond_to do |format|
# format.html.none
# format.html.phone # this gets rendered
# end
#
- # Be sure to check the documentation of +respond_with+ and
- # <tt>ActionController::MimeResponds.respond_to</tt> for more examples.
- def respond_to(*mimes, &block)
+ # Be sure to check the documentation of <tt>ActionController::MimeResponds.respond_to</tt>
+ # for more examples.
+ def respond_to(*mimes)
raise ArgumentError, "respond_to takes either types or a block, never both" if mimes.any? && block_given?
- if collector = retrieve_collector_from_mimes(mimes, &block)
- response = collector.response
- response ? response.call : render({})
- end
- end
-
- # For a given controller action, respond_with generates an appropriate
- # response based on the mime-type requested by the client.
- #
- # If the method is called with just a resource, as in this example -
- #
- # class PeopleController < ApplicationController
- # respond_to :html, :xml, :json
- #
- # def index
- # @people = Person.all
- # respond_with @people
- # end
- # end
- #
- # then the mime-type of the response is typically selected based on the
- # request's Accept header and the set of available formats declared
- # by previous calls to the controller's class method +respond_to+. Alternatively
- # the mime-type can be selected by explicitly setting <tt>request.format</tt> in
- # the controller.
- #
- # If an acceptable format is not identified, the application returns a
- # '406 - not acceptable' status. Otherwise, the default response is to render
- # a template named after the current action and the selected format,
- # e.g. <tt>index.html.erb</tt>. If no template is available, the behavior
- # depends on the selected format:
- #
- # * for an html response - if the request method is +get+, an exception
- # is raised but for other requests such as +post+ the response
- # depends on whether the resource has any validation errors (i.e.
- # assuming that an attempt has been made to save the resource,
- # e.g. by a +create+ action) -
- # 1. If there are no errors, i.e. the resource
- # was saved successfully, the response +redirect+'s to the resource
- # i.e. its +show+ action.
- # 2. If there are validation errors, the response
- # renders a default action, which is <tt>:new</tt> for a
- # +post+ request or <tt>:edit</tt> for +patch+ or +put+.
- # Thus an example like this -
- #
- # respond_to :html, :xml
- #
- # def create
- # @user = User.new(params[:user])
- # flash[:notice] = 'User was successfully created.' if @user.save
- # respond_with(@user)
- # end
- #
- # is equivalent, in the absence of <tt>create.html.erb</tt>, to -
- #
- # def create
- # @user = User.new(params[:user])
- # respond_to do |format|
- # if @user.save
- # flash[:notice] = 'User was successfully created.'
- # format.html { redirect_to(@user) }
- # format.xml { render xml: @user }
- # else
- # format.html { render action: "new" }
- # format.xml { render xml: @user }
- # end
- # end
- # end
- #
- # * for a JavaScript request - if the template isn't found, an exception is
- # raised.
- # * for other requests - i.e. data formats such as xml, json, csv etc, if
- # the resource passed to +respond_with+ responds to <code>to_<format></code>,
- # the method attempts to render the resource in the requested format
- # directly, e.g. for an xml request, the response is equivalent to calling
- # <code>render xml: resource</code>.
- #
- # === Nested resources
- #
- # As outlined above, the +resources+ argument passed to +respond_with+
- # can play two roles. It can be used to generate the redirect url
- # for successful html requests (e.g. for +create+ actions when
- # no template exists), while for formats other than html and JavaScript
- # it is the object that gets rendered, by being converted directly to the
- # required format (again assuming no template exists).
- #
- # For redirecting successful html requests, +respond_with+ also supports
- # the use of nested resources, which are supplied in the same way as
- # in <code>form_for</code> and <code>polymorphic_url</code>. For example -
- #
- # def create
- # @project = Project.find(params[:project_id])
- # @task = @project.comments.build(params[:task])
- # flash[:notice] = 'Task was successfully created.' if @task.save
- # respond_with(@project, @task)
- # end
- #
- # This would cause +respond_with+ to redirect to <code>project_task_url</code>
- # instead of <code>task_url</code>. For request formats other than html or
- # JavaScript, if multiple resources are passed in this way, it is the last
- # one specified that is rendered.
- #
- # === Customizing response behavior
- #
- # Like +respond_to+, +respond_with+ may also be called with a block that
- # can be used to overwrite any of the default responses, e.g. -
- #
- # def create
- # @user = User.new(params[:user])
- # flash[:notice] = "User was successfully created." if @user.save
- #
- # respond_with(@user) do |format|
- # format.html { render }
- # end
- # end
- #
- # The argument passed to the block is an ActionController::MimeResponds::Collector
- # object which stores the responses for the formats defined within the
- # block. Note that formats with responses defined explicitly in this way
- # do not have to first be declared using the class method +respond_to+.
- #
- # Also, a hash passed to +respond_with+ immediately after the specified
- # resource(s) is interpreted as a set of options relevant to all
- # formats. Any option accepted by +render+ can be used, e.g.
- # respond_with @people, status: 200
- # However, note that these options are ignored after an unsuccessful attempt
- # to save a resource, e.g. when automatically rendering <tt>:new</tt>
- # after a post request.
- #
- # Two additional options are relevant specifically to +respond_with+ -
- # 1. <tt>:location</tt> - overwrites the default redirect location used after
- # a successful html +post+ request.
- # 2. <tt>:action</tt> - overwrites the default render action used after an
- # unsuccessful html +post+ request.
- def respond_with(*resources, &block)
- if self.class.mimes_for_respond_to.empty?
- raise "In order to use respond_with, first you need to declare the " \
- "formats your controller responds to in the class level."
- end
-
- if collector = retrieve_collector_from_mimes(&block)
- options = resources.size == 1 ? {} : resources.extract_options!
- options = options.clone
- options[:default_response] = collector.response
- (options.delete(:responder) || self.class.responder).call(self, resources, options)
- end
- end
-
- protected
-
- # Collect mimes declared in the class method respond_to valid for the
- # current action.
- def collect_mimes_from_class_level #:nodoc:
- action = action_name.to_s
-
- self.class.mimes_for_respond_to.keys.select do |mime|
- config = self.class.mimes_for_respond_to[mime]
-
- if config[:except]
- !config[:except].include?(action)
- elsif config[:only]
- config[:only].include?(action)
- else
- true
- end
- end
- end
-
- # Returns a Collector object containing the appropriate mime-type response
- # for the current request, based on the available responses defined by a block.
- # In typical usage this is the block passed to +respond_with+ or +respond_to+.
- #
- # Sends :not_acceptable to the client and returns nil if no suitable format
- # is available.
- def retrieve_collector_from_mimes(mimes=nil, &block) #:nodoc:
- mimes ||= collect_mimes_from_class_level
collector = Collector.new(mimes, request.variant)
- block.call(collector) if block_given?
- format = collector.negotiate_format(request)
+ yield collector if block_given?
- if format
+ if format = collector.negotiate_format(request)
_process_format(format)
- collector
+ _set_rendered_content_type format
+ response = collector.response
+ response ? response.call : render({})
else
raise ActionController::UnknownFormat
end
@@ -444,8 +202,8 @@ module ActionController #:nodoc:
# A container for responses available from the current controller for
# requests for different mime-types sent to a particular action.
#
- # The public controller methods +respond_with+ and +respond_to+ may be called
- # with a block that is used to define responses to different mime-types, e.g.
+ # The public controller methods +respond_to+ may be called with a block
+ # that is used to define responses to different mime-types, e.g.
# for +respond_to+ :
#
# respond_to do |format|
@@ -471,7 +229,7 @@ module ActionController #:nodoc:
@responses = {}
@variant = variant
- mimes.each { |mime| @responses["Mime::#{mime.upcase}".constantize] = nil }
+ mimes.each { |mime| @responses[Mime[mime]] = nil }
end
def any(*args, &block)
@@ -531,16 +289,17 @@ module ActionController #:nodoc:
end
def variant
- if @variant.nil?
+ if @variant.empty?
@variants[:none] || @variants[:any]
- elsif (@variants.keys & @variant).any?
- @variant.each do |v|
- return @variants[v] if @variants.key?(v)
- end
else
- @variants[:any]
+ @variants[variant_key]
end
end
+
+ private
+ def variant_key
+ @variant.find { |variant| @variants.key?(variant) } || :any
+ 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 2ca8955741..c38fc40b81 100644
--- a/actionpack/lib/action_controller/metal/params_wrapper.rb
+++ b/actionpack/lib/action_controller/metal/params_wrapper.rb
@@ -1,22 +1,20 @@
require 'active_support/core_ext/hash/slice'
require 'active_support/core_ext/hash/except'
require 'active_support/core_ext/module/anonymous'
-require 'active_support/core_ext/struct'
require 'action_dispatch/http/mime_type'
module ActionController
- # Wraps the parameters hash into a nested hash. This will allow clients to submit
- # POST requests without having to specify any root elements.
+ # Wraps the parameters hash into a nested hash. This will allow clients to
+ # submit requests without having to specify any root elements.
#
# This functionality is enabled in +config/initializers/wrap_parameters.rb+
- # and can be customized. If you are upgrading to \Rails 3.1, this file will
- # need to be created for the functionality to be enabled.
+ # and can be customized.
#
# You could also turn it on per controller by setting the format array to
# a non-empty array:
#
# class UsersController < ApplicationController
- # wrap_parameters format: [:json, :xml]
+ # wrap_parameters format: [:json, :xml, :url_encoded_form, :multipart_form]
# end
#
# If you enable +ParamsWrapper+ for +:json+ format, instead of having to
@@ -42,7 +40,7 @@ module ActionController
# wrap_parameters :person, include: [:username, :password]
# end
#
- # On ActiveRecord models with no +:include+ or +:exclude+ option set,
+ # On Active Record models with no +:include+ or +:exclude+ option set,
# it will only wrap the parameters returned by the class method
# <tt>attribute_names</tt>.
#
@@ -86,7 +84,7 @@ module ActionController
new name, format, include, exclude, nil, nil
end
- def initialize(name, format, include, exclude, klass, model) # nodoc
+ def initialize(name, format, include, exclude, klass, model) # :nodoc:
super
@include_set = include
@name_set = name
@@ -132,7 +130,7 @@ module ActionController
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 singularize name as the controller. For example, +UsersController+
+ # 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
@@ -244,7 +242,7 @@ module ActionController
request.parameters.merge! wrapped_hash
request.request_parameters.merge! wrapped_hash
- # This will make the wrapped hash displayed in the log file
+ # This will display the wrapped hash in the log file
request.filtered_parameters.merge! wrapped_filtered_hash
end
super
@@ -252,7 +250,7 @@ module ActionController
private
- # Returns the wrapper key which will use to stored wrapped parameters.
+ # Returns the wrapper key which will be used to store wrapped parameters.
def _wrapper_key
_wrapper_options.name
end
@@ -278,7 +276,9 @@ module ActionController
# Checks if we should perform parameters wrapping.
def _wrapper_enabled?
- ref = request.content_mime_type.try(:ref)
+ return false unless request.has_content_type?
+
+ ref = request.content_mime_type.ref
_wrapper_formats.include?(ref) && _wrapper_key && !request.request_parameters[_wrapper_key]
end
end
diff --git a/actionpack/lib/action_controller/metal/rack_delegation.rb b/actionpack/lib/action_controller/metal/rack_delegation.rb
deleted file mode 100644
index 6921834044..0000000000
--- a/actionpack/lib/action_controller/metal/rack_delegation.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-require 'action_dispatch/http/request'
-require 'action_dispatch/http/response'
-
-module ActionController
- module RackDelegation
- extend ActiveSupport::Concern
-
- delegate :headers, :status=, :location=, :content_type=,
- :status, :location, :content_type, :_status_code, :to => "@_response"
-
- def dispatch(action, request)
- set_response!(request)
- super(action, request)
- end
-
- def response_body=(body)
- response.body = body if response
- super
- end
-
- def reset_session
- @_request.reset_session
- end
-
- private
-
- def set_response!(request)
- @_response = ActionDispatch::Response.new
- @_response.request = request
- end
- end
-end
diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb
index ca8c0278d0..0febc905f1 100644
--- a/actionpack/lib/action_controller/metal/redirecting.rb
+++ b/actionpack/lib/action_controller/metal/redirecting.rb
@@ -11,7 +11,6 @@ module ActionController
extend ActiveSupport::Concern
include AbstractController::Logger
- include ActionController::RackDelegation
include ActionController::UrlFor
# Redirects the browser to the target specified in +options+. This parameter can be any one of:
@@ -72,11 +71,11 @@ module ActionController
raise AbstractController::DoubleRenderError if response_body
self.status = _extract_redirect_to_status(options, response_status)
- self.location = _compute_redirect_to_location(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>"
end
- def _compute_redirect_to_location(options) #:nodoc:
+ def _compute_redirect_to_location(request, options) #:nodoc:
case options
# The scheme name consist of a letter followed by any combination of
# letters, digits, and the plus ("+"), period ("."), or hyphen ("-")
@@ -90,11 +89,13 @@ module ActionController
when :back
request.headers["Referer"] or raise RedirectBackError
when Proc
- _compute_redirect_to_location options.call
+ _compute_redirect_to_location request, options.call
else
url_for(options)
end.delete("\0\r\n")
end
+ module_function :_compute_redirect_to_location
+ public :_compute_redirect_to_location
private
def _extract_redirect_to_status(options, response_status)
diff --git a/actionpack/lib/action_controller/metal/renderers.rb b/actionpack/lib/action_controller/metal/renderers.rb
index 02c4e563f5..22e0bb5955 100644
--- a/actionpack/lib/action_controller/metal/renderers.rb
+++ b/actionpack/lib/action_controller/metal/renderers.rb
@@ -34,14 +34,15 @@ module ActionController
end
def render_to_body(options)
- _handle_render_options(options) || super
+ _render_to_body_with_renderer(options) || super
end
- def _handle_render_options(options)
+ def _render_to_body_with_renderer(options)
_renderers.each do |name|
if options.key?(name)
_process_options(options)
- return send("_render_option_#{name}", options.delete(name), options)
+ method_name = Renderers._render_with_renderer_method_name(name)
+ return send(method_name, options.delete(name), options)
end
end
nil
@@ -51,6 +52,10 @@ module ActionController
# Default values are <tt>:json</tt>, <tt>:js</tt>, <tt>:xml</tt>.
RENDERERS = Set.new
+ def self._render_with_renderer_method_name(key)
+ "_render_with_renderer_#{key}"
+ end
+
# Adds a new renderer to call within controller actions.
# A renderer is invoked by passing its name as an option to
# <tt>AbstractController::Rendering#render</tt>. To create a renderer
@@ -63,11 +68,11 @@ module ActionController
# ActionController::Renderers.add :csv do |obj, options|
# filename = options[:filename] || 'data'
# str = obj.respond_to?(:to_csv) ? obj.to_csv : obj.to_s
- # send_data str, type: Mime::CSV,
+ # send_data str, type: Mime[:csv],
# disposition: "attachment; filename=#{filename}.csv"
# end
#
- # Note that we used Mime::CSV for the csv mime type as it comes with Rails.
+ # Note that we used Mime[:csv] for the csv mime type as it comes with Rails.
# For a custom renderer, you'll need to register a mime type with
# <tt>Mime::Type.register</tt>.
#
@@ -81,22 +86,21 @@ module ActionController
# end
# end
# To use renderers and their mime types in more concise ways, see
- # <tt>ActionController::MimeResponds::ClassMethods.respond_to</tt> and
- # <tt>ActionController::MimeResponds#respond_with</tt>
+ # <tt>ActionController::MimeResponds::ClassMethods.respond_to</tt>
def self.add(key, &block)
- define_method("_render_option_#{key}", &block)
+ define_method(_render_with_renderer_method_name(key), &block)
RENDERERS << key.to_sym
end
# This method is the opposite of add method.
#
- # Usage:
+ # To remove a csv renderer:
#
# ActionController::Renderers.remove(:csv)
def self.remove(key)
RENDERERS.delete(key.to_sym)
- method = "_render_option_#{key}"
- remove_method(method) if method_defined?(method)
+ method_name = _render_with_renderer_method_name(key)
+ remove_method(method_name) if method_defined?(method_name)
end
module All
@@ -112,24 +116,24 @@ module ActionController
json = json.to_json(options) unless json.kind_of?(String)
if options[:callback].present?
- if self.content_type.nil? || self.content_type == Mime::JSON
- self.content_type = Mime::JS
+ if content_type.nil? || content_type == Mime[:json]
+ self.content_type = Mime[:js]
end
"/**/#{options[:callback]}(#{json})"
else
- self.content_type ||= Mime::JSON
+ self.content_type ||= Mime[:json]
json
end
end
add :js do |js, options|
- self.content_type ||= Mime::JS
+ self.content_type ||= Mime[:js]
js.respond_to?(:to_js) ? js.to_js(options) : js
end
add :xml do |xml, options|
- self.content_type ||= Mime::XML
+ self.content_type ||= Mime[:xml]
xml.respond_to?(:to_xml) ? xml.to_xml(options) : xml
end
end
diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb
index 7bbff0450a..cce6fe7787 100644
--- a/actionpack/lib/action_controller/metal/rendering.rb
+++ b/actionpack/lib/action_controller/metal/rendering.rb
@@ -1,9 +1,29 @@
+require 'active_support/core_ext/string/filters'
+
module ActionController
module Rendering
extend ActiveSupport::Concern
RENDER_FORMATS_IN_PRIORITY = [:body, :text, :plain, :html]
+ module ClassMethods
+ # Documentation at ActionController::Renderer#render
+ delegate :render, to: :renderer
+
+ # Returns a renderer instance (inherited from ActionController::Renderer)
+ # for the controller.
+ attr_reader :renderer
+
+ def setup_renderer! # :nodoc:
+ @renderer = Renderer.for(self)
+ end
+
+ def inherited(klass)
+ klass.setup_renderer!
+ super
+ end
+ end
+
# Before processing, set the request formats in current controller formats.
def process_action(*) #:nodoc:
self.formats = request.formats.map(&:ref).compact
@@ -42,13 +62,13 @@ module ActionController
nil
end
- def _process_format(format, options = {})
- super
+ def _set_html_content_type
+ self.content_type = Mime[:html].to_s
+ end
- if options[:plain]
- self.content_type = Mime::TEXT
- else
- self.content_type ||= format.to_s
+ def _set_rendered_content_type(format)
+ unless response.content_type
+ self.content_type = format.to_s
end
end
@@ -63,11 +83,23 @@ module ActionController
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
+ end
+
if options[:html]
options[:html] = ERB::Util.html_escape(options[:html])
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
end
diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb
index 0efa0fb259..64f6f7cf51 100644
--- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb
+++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb
@@ -1,5 +1,6 @@
require 'rack/session/abstract/id'
require 'action_controller/metal/exceptions'
+require 'active_support/security_utils'
module ActionController #:nodoc:
class InvalidAuthenticityToken < ActionControllerError #:nodoc:
@@ -9,12 +10,17 @@ module ActionController #:nodoc:
end
# Controller actions are protected from Cross-Site Request Forgery (CSRF) attacks
- # by including a token in the rendered html for your application. This token is
+ # by including a token in the rendered HTML for your application. This token is
# stored as a random string in the session, to which an attacker does not have
# access. When a request reaches your application, \Rails verifies the received
- # token with the token in the session. Only HTML and JavaScript requests are checked,
- # so this will not protect your XML API (presumably you'll have a different
- # authentication scheme there anyway).
+ # token with the token in the session. All requests are checked except GET requests
+ # as these should be idempotent. Keep in mind that all session-oriented requests
+ # should be CSRF protected, including JavaScript and HTML requests.
+ #
+ # Since HTML and JavaScript requests are typically made from the browser, we
+ # need to ensure to verify request authenticity for the web browser. We can
+ # use session-oriented authentication for these types of requests, by using
+ # the `protect_from_forgery` method in our controllers.
#
# GET requests are not protected since they don't have side effects like writing
# to the database and don't leak sensitive information. JavaScript requests are
@@ -25,26 +31,25 @@ module ActionController #:nodoc:
# Ajax) requests are allowed to make GET requests for JavaScript responses.
#
# It's important to remember that XML or JSON requests are also affected and if
- # you're building an API you'll need something like:
+ # you're building an API you should change forgery protection method in
+ # <tt>ApplicationController</tt> (by default: <tt>:exception</tt>):
#
# class ApplicationController < ActionController::Base
- # protect_from_forgery
- # skip_before_action :verify_authenticity_token, if: :json_request?
- #
- # protected
- #
- # def json_request?
- # request.format.json?
- # end
+ # protect_from_forgery unless: -> { request.format.json? }
# end
#
- # CSRF protection is turned on with the <tt>protect_from_forgery</tt> method,
- # which checks the token and resets the session if it doesn't match what was expected.
- # A call to this method is generated for new \Rails applications by default.
+ # CSRF protection is turned on with the <tt>protect_from_forgery</tt> method.
+ # By default <tt>protect_from_forgery</tt> protects your session with
+ # <tt>:null_session</tt> method, which provides an empty session
+ # during request.
+ #
+ # We may want to disable CSRF protection for APIs since they are typically
+ # designed to be state-less. That is, the request API client will handle
+ # the session for you instead of Rails.
#
# The token parameter is named <tt>authenticity_token</tt> by default. The name and
# value of this token must be added to every layout that renders forms by including
- # <tt>csrf_meta_tags</tt> in the html +head+.
+ # <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].
@@ -85,13 +90,21 @@ module ActionController #:nodoc:
#
# class FooController < ApplicationController
# protect_from_forgery except: :index
+ # end
+ #
+ # You can disable forgery protection on controller by skipping the verification before_action:
#
- # You can disable CSRF protection on controller by skipping the verification before_action:
# skip_before_action :verify_authenticity_token
#
# Valid Options:
#
- # * <tt>:only/:except</tt> - Passed to the <tt>before_action</tt> call. Set which actions are verified.
+ # * <tt>:only/:except</tt> - Only apply forgery protection to a subset of actions. Like <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 is added to the front of the
+ # callback chain. If you need to make the verification depend on other callbacks, like authentication methods
+ # (say cookies vs OAuth), this might not work for you. Pass <tt>prepend: false</tt> to just add the
+ # verification callback in the position of the protect_from_forgery call. This means any callbacks added
+ # before are run first.
# * <tt>:with</tt> - Set the method to handle unverified request.
#
# Valid unverified request handling methods are:
@@ -99,9 +112,11 @@ module ActionController #:nodoc:
# * <tt>:reset_session</tt> - Resets the session.
# * <tt>:null_session</tt> - Provides an empty session during request but doesn't reset it completely. Used as default if <tt>:with</tt> option is not specified.
def protect_from_forgery(options = {})
+ options = options.reverse_merge(prepend: true)
+
self.forgery_protection_strategy = protection_method_class(options[:with] || :null_session)
self.request_forgery_protection_token ||= :authenticity_token
- prepend_before_action :verify_authenticity_token, options
+ before_action :verify_authenticity_token, options
append_after_action :verify_same_origin_request
end
@@ -123,17 +138,17 @@ module ActionController #:nodoc:
# This is the method that defines the application behavior when a request is found to be unverified.
def handle_unverified_request
request = @controller.request
- request.session = NullSessionHash.new(request.env)
- request.env['action_dispatch.request.flash_hash'] = nil
- request.env['rack.session.options'] = { skip: true }
- request.env['action_dispatch.cookies'] = NullCookieJar.build(request)
+ request.session = NullSessionHash.new(request)
+ request.flash = nil
+ request.session_options = { skip: true }
+ request.cookie_jar = NullCookieJar.build(request, {})
end
protected
class NullSessionHash < Rack::Session::Abstract::SessionHash #:nodoc:
- def initialize(env)
- super(nil, env)
+ def initialize(req)
+ super(nil, req)
@data = {}
@loaded = true
end
@@ -147,14 +162,6 @@ module ActionController #:nodoc:
end
class NullCookieJar < ActionDispatch::Cookies::CookieJar #:nodoc:
- def self.build(request)
- key_generator = request.env[ActionDispatch::Cookies::GENERATOR_KEY]
- host = request.host
- secure = request.ssl?
-
- new(key_generator, host, secure, options_for_env({}))
- end
-
def write(*)
# nothing
end
@@ -208,6 +215,7 @@ module ActionController #:nodoc:
forgery_protection_strategy.new(self).handle_unverified_request
end
+ #:nodoc:
CROSS_ORIGIN_JAVASCRIPT_WARNING = "Security warning: an embedded " \
"<script> tag on another site requested protected JavaScript. " \
"If you know what you're doing, go ahead and disable forgery " \
@@ -240,20 +248,83 @@ module ActionController #:nodoc:
content_type =~ %r(\Atext/javascript) && !request.xhr?
end
+ AUTHENTICITY_TOKEN_LENGTH = 32
+
# Returns true or false if a request is verified. Checks:
#
- # * is it a GET or HEAD request? Gets should be safe and idempotent
+ # * 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?
!protect_against_forgery? || request.get? || request.head? ||
- form_authenticity_token == form_authenticity_param ||
- form_authenticity_token == request.headers['X-CSRF-Token']
+ valid_authenticity_token?(session, form_authenticity_param) ||
+ valid_authenticity_token?(session, request.headers['X-CSRF-Token'])
end
# Sets the token value for the current session.
def form_authenticity_token
- session[:_csrf_token] ||= SecureRandom.base64(32)
+ masked_authenticity_token(session)
+ end
+
+ # Creates a masked version of the authenticity token that varies
+ # on each request. The masking is used to mitigate SSL attacks
+ # like BREACH.
+ def masked_authenticity_token(session)
+ one_time_pad = SecureRandom.random_bytes(AUTHENTICITY_TOKEN_LENGTH)
+ encrypted_csrf_token = xor_byte_strings(one_time_pad, real_csrf_token(session))
+ masked_token = one_time_pad + encrypted_csrf_token
+ Base64.strict_encode64(masked_token)
+ end
+
+ # Checks the client's masked token to see if it matches the
+ # session token. Essentially the inverse of
+ # +masked_authenticity_token+.
+ def valid_authenticity_token?(session, encoded_masked_token)
+ if encoded_masked_token.nil? || encoded_masked_token.empty? || !encoded_masked_token.is_a?(String)
+ return false
+ end
+
+ begin
+ masked_token = Base64.strict_decode64(encoded_masked_token)
+ rescue ArgumentError # encoded_masked_token is invalid Base64
+ return false
+ end
+
+ # See if it's actually a masked token or not. In order to
+ # deploy this code, we should be able to handle any unmasked
+ # tokens that we've issued without error.
+
+ if masked_token.length == AUTHENTICITY_TOKEN_LENGTH
+ # This is actually an unmasked token. This is expected if
+ # you have just upgraded to masked tokens, but should stop
+ # happening shortly after installing this gem
+ compare_with_real_token masked_token, session
+
+ elsif masked_token.length == AUTHENTICITY_TOKEN_LENGTH * 2
+ # Split the token into the one-time pad and the encrypted
+ # value and decrypt it
+ one_time_pad = masked_token[0...AUTHENTICITY_TOKEN_LENGTH]
+ encrypted_csrf_token = masked_token[AUTHENTICITY_TOKEN_LENGTH..-1]
+ csrf_token = xor_byte_strings(one_time_pad, encrypted_csrf_token)
+
+ compare_with_real_token csrf_token, session
+
+ else
+ false # Token is malformed
+ end
+ end
+
+ def compare_with_real_token(token, session)
+ ActiveSupport::SecurityUtils.secure_compare(token, real_csrf_token(session))
+ end
+
+ def real_csrf_token(session)
+ session[:_csrf_token] ||= SecureRandom.base64(AUTHENTICITY_TOKEN_LENGTH)
+ Base64.strict_decode64(session[:_csrf_token])
+ end
+
+ def xor_byte_strings(s1, s2)
+ s1.bytes.zip(s2.bytes).map { |(c1,c2)| c1 ^ c2 }.pack('c*')
end
# The form's authenticity parameter. Override to provide your own.
diff --git a/actionpack/lib/action_controller/metal/responder.rb b/actionpack/lib/action_controller/metal/responder.rb
deleted file mode 100644
index 5096558c67..0000000000
--- a/actionpack/lib/action_controller/metal/responder.rb
+++ /dev/null
@@ -1,297 +0,0 @@
-require 'active_support/json'
-
-module ActionController #:nodoc:
- # Responsible for exposing a resource to different mime requests,
- # usually depending on the HTTP verb. The responder is triggered when
- # <code>respond_with</code> is called. The simplest case to study is a GET request:
- #
- # class PeopleController < ApplicationController
- # respond_to :html, :xml, :json
- #
- # def index
- # @people = Person.all
- # respond_with(@people)
- # end
- # end
- #
- # When a request comes in, for example for an XML response, three steps happen:
- #
- # 1) the responder searches for a template at people/index.xml;
- #
- # 2) if the template is not available, it will invoke <code>#to_xml</code> on the given resource;
- #
- # 3) if the responder does not <code>respond_to :to_xml</code>, call <code>#to_format</code> on it.
- #
- # === Built-in HTTP verb semantics
- #
- # The default \Rails responder holds semantics for each HTTP verb. Depending on the
- # content type, verb and the resource status, it will behave differently.
- #
- # Using \Rails default responder, a POST request for creating an object could
- # be written as:
- #
- # def create
- # @user = User.new(params[:user])
- # flash[:notice] = 'User was successfully created.' if @user.save
- # respond_with(@user)
- # end
- #
- # Which is exactly the same as:
- #
- # def create
- # @user = User.new(params[:user])
- #
- # respond_to do |format|
- # if @user.save
- # flash[:notice] = 'User was successfully created.'
- # format.html { redirect_to(@user) }
- # format.xml { render xml: @user, status: :created, location: @user }
- # else
- # format.html { render action: "new" }
- # format.xml { render xml: @user.errors, status: :unprocessable_entity }
- # end
- # end
- # end
- #
- # The same happens for PATCH/PUT and DELETE requests.
- #
- # === Nested resources
- #
- # You can supply nested resources as you do in <code>form_for</code> and <code>polymorphic_url</code>.
- # Consider the project has many tasks example. The create action for
- # TasksController would be like:
- #
- # def create
- # @project = Project.find(params[:project_id])
- # @task = @project.tasks.build(params[:task])
- # flash[:notice] = 'Task was successfully created.' if @task.save
- # respond_with(@project, @task)
- # end
- #
- # Giving several resources ensures that the responder will redirect to
- # <code>project_task_url</code> instead of <code>task_url</code>.
- #
- # Namespaced and singleton resources require a symbol to be given, as in
- # polymorphic urls. If a project has one manager which has many tasks, it
- # should be invoked as:
- #
- # respond_with(@project, :manager, @task)
- #
- # Note that if you give an array, it will be treated as a collection,
- # so the following is not equivalent:
- #
- # respond_with [@project, :manager, @task]
- #
- # === Custom options
- #
- # <code>respond_with</code> also allows you to pass options that are forwarded
- # to the underlying render call. Those options are only applied for success
- # scenarios. For instance, you can do the following in the create method above:
- #
- # def create
- # @project = Project.find(params[:project_id])
- # @task = @project.tasks.build(params[:task])
- # flash[:notice] = 'Task was successfully created.' if @task.save
- # respond_with(@project, @task, status: 201)
- # end
- #
- # This will return status 201 if the task was saved successfully. If not,
- # it will simply ignore the given options and return status 422 and the
- # resource errors. You can also override the location to redirect to:
- #
- # respond_with(@project, location: root_path)
- #
- # To customize the failure scenario, you can pass a block to
- # <code>respond_with</code>:
- #
- # def create
- # @project = Project.find(params[:project_id])
- # @task = @project.tasks.build(params[:task])
- # respond_with(@project, @task, status: 201) do |format|
- # if @task.save
- # flash[:notice] = 'Task was successfully created.'
- # else
- # format.html { render "some_special_template" }
- # end
- # end
- # end
- #
- # Using <code>respond_with</code> with a block follows the same syntax as <code>respond_to</code>.
- class Responder
- attr_reader :controller, :request, :format, :resource, :resources, :options
-
- DEFAULT_ACTIONS_FOR_VERBS = {
- :post => :new,
- :patch => :edit,
- :put => :edit
- }
-
- def initialize(controller, resources, options={})
- @controller = controller
- @request = @controller.request
- @format = @controller.formats.first
- @resource = resources.last
- @resources = resources
- @options = options
- @action = options.delete(:action)
- @default_response = options.delete(:default_response)
- end
-
- delegate :head, :render, :redirect_to, :to => :controller
- delegate :get?, :post?, :patch?, :put?, :delete?, :to => :request
-
- # Undefine :to_json and :to_yaml since it's defined on Object
- undef_method(:to_json) if method_defined?(:to_json)
- undef_method(:to_yaml) if method_defined?(:to_yaml)
-
- # Initializes a new responder and invokes the proper format. If the format is
- # not defined, call to_format.
- #
- def self.call(*args)
- new(*args).respond
- end
-
- # Main entry point for responder responsible to dispatch to the proper format.
- #
- def respond
- method = "to_#{format}"
- respond_to?(method) ? send(method) : to_format
- end
-
- # HTML format does not render the resource, it always attempt to render a
- # template.
- #
- def to_html
- default_render
- rescue ActionView::MissingTemplate => e
- navigation_behavior(e)
- end
-
- # to_js simply tries to render a template. If no template is found, raises the error.
- def to_js
- default_render
- end
-
- # All other formats follow the procedure below. First we try to render a
- # template, if the template is not available, we verify if the resource
- # responds to :to_format and display it.
- #
- def to_format
- if get? || !has_errors? || response_overridden?
- default_render
- else
- display_errors
- end
- rescue ActionView::MissingTemplate => e
- api_behavior(e)
- end
-
- protected
-
- # This is the common behavior for formats associated with browsing, like :html, :iphone and so forth.
- def navigation_behavior(error)
- if get?
- raise error
- elsif has_errors? && default_action
- render :action => default_action
- else
- redirect_to navigation_location
- end
- end
-
- # This is the common behavior for formats associated with APIs, such as :xml and :json.
- def api_behavior(error)
- raise error unless resourceful?
- raise MissingRenderer.new(format) unless has_renderer?
-
- if get?
- display resource
- elsif post?
- display resource, :status => :created, :location => api_location
- else
- head :no_content
- end
- end
-
- # Checks whether the resource responds to the current format or not.
- #
- def resourceful?
- resource.respond_to?("to_#{format}")
- end
-
- # Returns the resource location by retrieving it from the options or
- # returning the resources array.
- #
- def resource_location
- options[:location] || resources
- end
- alias :navigation_location :resource_location
- alias :api_location :resource_location
-
- # If a response block was given, use it, otherwise call render on
- # controller.
- #
- def default_render
- if @default_response
- @default_response.call(options)
- else
- controller.default_render(options)
- end
- end
-
- # Display is just a shortcut to render a resource with the current format.
- #
- # display @user, status: :ok
- #
- # For XML requests it's equivalent to:
- #
- # render xml: @user, status: :ok
- #
- # Options sent by the user are also used:
- #
- # respond_with(@user, status: :created)
- # display(@user, status: :ok)
- #
- # Results in:
- #
- # render xml: @user, status: :created
- #
- def display(resource, given_options={})
- controller.render given_options.merge!(options).merge!(format => resource)
- end
-
- def display_errors
- controller.render format => resource_errors, :status => :unprocessable_entity
- end
-
- # Check whether the resource has errors.
- #
- def has_errors?
- resource.respond_to?(:errors) && !resource.errors.empty?
- end
-
- # Check whether the necessary Renderer is available
- def has_renderer?
- Renderers::RENDERERS.include?(format)
- end
-
- # By default, render the <code>:edit</code> action for HTML requests with errors, unless
- # the verb was POST.
- #
- def default_action
- @action ||= DEFAULT_ACTIONS_FOR_VERBS[request.request_method_symbol]
- end
-
- def resource_errors
- respond_to?("#{format}_resource_errors", true) ? send("#{format}_resource_errors") : resource.errors
- end
-
- def json_resource_errors
- {:errors => resource.errors}
- end
-
- def response_overridden?
- @default_response.present?
- end
- end
-end
diff --git a/actionpack/lib/action_controller/metal/streaming.rb b/actionpack/lib/action_controller/metal/streaming.rb
index 04401cad7b..a6115674aa 100644
--- a/actionpack/lib/action_controller/metal/streaming.rb
+++ b/actionpack/lib/action_controller/metal/streaming.rb
@@ -110,9 +110,9 @@ module ActionController #:nodoc:
# This means that, if you have <code>yield :title</code> in your layout
# and you want to use streaming, you would have to render the whole template
# (and eventually trigger all queries) before streaming the title and all
- # assets, which kills the purpose of streaming. For this reason Rails 3.1
- # introduces a new helper called +provide+ that does the same as +content_for+
- # but tells the layout to stop searching for other entries and continue rendering.
+ # assets, which kills the purpose of streaming. For this purpose, you can use
+ # a helper called +provide+ that does the same as +content_for+ but tells the
+ # layout to stop searching for other entries and continue rendering.
#
# For instance, the template above using +provide+ would be:
#
@@ -199,7 +199,7 @@ module ActionController #:nodoc:
def _process_options(options) #:nodoc:
super
if options[:stream]
- if env["HTTP_VERSION"] == "HTTP/1.0"
+ if request.version == "HTTP/1.0"
options.delete(:stream)
else
headers["Cache-Control"] ||= "no-cache"
diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb
index bc27ecaa20..130ba61786 100644
--- a/actionpack/lib/action_controller/metal/strong_parameters.rb
+++ b/actionpack/lib/action_controller/metal/strong_parameters.rb
@@ -1,6 +1,6 @@
require 'active_support/core_ext/hash/indifferent_access'
require 'active_support/core_ext/array/wrap'
-require 'active_support/deprecation'
+require 'active_support/core_ext/string/filters'
require 'active_support/rescuable'
require 'action_dispatch/http/upload'
require 'stringio'
@@ -11,9 +11,9 @@ module ActionController
#
# params = ActionController::Parameters.new(a: {})
# params.fetch(:b)
- # # => ActionController::ParameterMissing: param not found: b
+ # # => ActionController::ParameterMissing: param is missing or the value is empty: b
# params.require(:a)
- # # => ActionController::ParameterMissing: param not found: a
+ # # => ActionController::ParameterMissing: param is missing or the value is empty: a
class ParameterMissing < KeyError
attr_reader :param # :nodoc:
@@ -23,11 +23,13 @@ module ActionController
end
end
- # Raised when a supplied parameter is not expected.
+ # Raised when a supplied parameter is not expected and
+ # ActionController::Parameters.action_on_unpermitted_parameters
+ # is set to <tt>:raise</tt>.
#
# params = ActionController::Parameters.new(a: "123", b: "456")
# params.permit(:c)
- # # => ActionController::UnpermittedParameters: found unexpected keys: a, b
+ # # => ActionController::UnpermittedParameters: found unpermitted parameters: a, b
class UnpermittedParameters < IndexError
attr_reader :params # :nodoc:
@@ -91,17 +93,22 @@ module ActionController
# params.permit(:c)
# # => ActionController::UnpermittedParameters: found unpermitted keys: a, b
#
- # <tt>ActionController::Parameters</tt> is inherited from
- # <tt>ActiveSupport::HashWithIndifferentAccess</tt>, this means
- # that you can fetch values using either <tt>:key</tt> or <tt>"key"</tt>.
+ # Please note that these options *are not thread-safe*. In a multi-threaded
+ # environment they should only be set once at boot-time and never mutated at
+ # runtime.
+ #
+ # You can fetch values of <tt>ActionController::Parameters</tt> using either
+ # <tt>:key</tt> or <tt>"key"</tt>.
#
# params = ActionController::Parameters.new(key: 'value')
# params[:key] # => "value"
# params["key"] # => "value"
- class Parameters < ActiveSupport::HashWithIndifferentAccess
+ class Parameters
cattr_accessor :permit_all_parameters, instance_accessor: false
cattr_accessor :action_on_unpermitted_parameters, instance_accessor: false
+ delegate :keys, :key?, :has_key?, :empty?, :inspect, to: :@parameters
+
# By default, never raise an UnpermittedParameters exception if these
# params are present. The default includes both 'controller' and 'action'
# because they are added by Rails and should be of no concern. One way
@@ -113,11 +120,13 @@ module ActionController
self.always_permitted_parameters = %w( controller action )
def self.const_missing(const_name)
- super unless const_name == :NEVER_UNPERMITTED_PARAMS
- ActiveSupport::Deprecation.warn "`ActionController::Parameters::NEVER_UNPERMITTED_PARAMS`"\
- " has been deprecated. Use "\
- "`ActionController::Parameters.always_permitted_parameters` instead."
- self.always_permitted_parameters
+ return super unless const_name == :NEVER_UNPERMITTED_PARAMS
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ `ActionController::Parameters::NEVER_UNPERMITTED_PARAMS` has been deprecated.
+ Use `ActionController::Parameters.always_permitted_parameters` instead.
+ MSG
+
+ always_permitted_parameters
end
# Returns a new instance of <tt>ActionController::Parameters</tt>.
@@ -136,11 +145,56 @@ module ActionController
# params = ActionController::Parameters.new(name: 'Francesco')
# params.permitted? # => true
# Person.new(params) # => #<Person id: nil, name: "Francesco">
- def initialize(attributes = nil)
- super(attributes)
+ def initialize(parameters = {})
+ @parameters = parameters.with_indifferent_access
@permitted = self.class.permit_all_parameters
end
+ # Returns true if another +Parameters+ object contains the same content and
+ # permitted flag, or other Hash-like object contains the same content. This
+ # override is in place so you can perform a comparison with `Hash`.
+ def ==(other_hash)
+ if other_hash.respond_to?(:permitted?)
+ super
+ else
+ @parameters == other_hash
+ end
+ end
+
+ # Returns a safe +Hash+ representation of this parameter with all
+ # unpermitted keys removed.
+ #
+ # params = ActionController::Parameters.new({
+ # name: 'Senjougahara Hitagi',
+ # oddity: 'Heavy stone crab'
+ # })
+ # params.to_h # => {}
+ #
+ # safe_params = params.permit(:name)
+ # safe_params.to_h # => {"name"=>"Senjougahara Hitagi"}
+ def to_h
+ if permitted?
+ @parameters.to_h
+ else
+ slice(*self.class.always_permitted_parameters).permit!.to_h
+ end
+ end
+
+ # Returns an unsafe, unfiltered +Hash+ representation of this parameter.
+ def to_unsafe_h
+ @parameters.to_h
+ 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>
+ def each_pair(&block)
+ @parameters.each_pair do |key, value|
+ yield key, convert_hashes_to_parameters(key, value)
+ end
+ end
+ alias_method :each, :each_pair
+
# Attribute that keeps track of converted arrays, if any, to avoid double
# looping in the common use case permit + mass-assignment. Defined in a
# method to instantiate it only if needed.
@@ -176,7 +230,6 @@ module ActionController
# Person.new(params) # => #<Person id: nil, name: "Francesco">
def permit!
each_pair do |key, value|
- value = convert_hashes_to_parameters(key, value)
Array.wrap(value).each do |v|
v.permit! if v.respond_to? :permit!
end
@@ -186,19 +239,58 @@ module ActionController
self
end
- # Ensures that a parameter is present. If it's present, returns
- # the parameter at the given +key+, otherwise raises an
- # <tt>ActionController::ParameterMissing</tt> error.
+ # This method accepts both a single key and an array of keys.
+ #
+ # When passed a single key, if it exists and its associated value is
+ # either present or the singleton +false+, returns said value:
#
# ActionController::Parameters.new(person: { name: 'Francesco' }).require(:person)
# # => {"name"=>"Francesco"}
#
+ # Otherwise raises <tt>ActionController::ParameterMissing</tt>:
+ #
+ # ActionController::Parameters.new.require(:person)
+ # # ActionController::ParameterMissing: param is missing or the value is empty: person
+ #
# ActionController::Parameters.new(person: nil).require(:person)
- # # => ActionController::ParameterMissing: param not found: person
+ # # ActionController::ParameterMissing: param is missing or the value is empty: person
+ #
+ # ActionController::Parameters.new(person: "\t").require(:person)
+ # # ActionController::ParameterMissing: param is missing or the value is empty: person
#
# ActionController::Parameters.new(person: {}).require(:person)
- # # => ActionController::ParameterMissing: param not found: person
+ # # ActionController::ParameterMissing: param is missing or the value is empty: person
+ #
+ # When given an array of keys, the method tries to require each one of them
+ # in order. If it succeeds, an array with the respective return values is
+ # returned:
+ #
+ # params = ActionController::Parameters.new(user: { ... }, profile: { ... })
+ # user_params, profile_params = params.require(:user, :profile)
+ #
+ # Otherwise, the method reraises the first exception found:
+ #
+ # params = ActionController::Parameters.new(user: {}, profile: {})
+ # user_params, profile_params = params.require(:user, :profile)
+ # # ActionController::ParameterMissing: param is missing or the value is empty: user
+ #
+ # Technically this method can be used to fetch terminal values:
+ #
+ # # CAREFUL
+ # params = ActionController::Parameters.new(person: { name: 'Finn' })
+ # name = params.require(:person).require(:name) # CAREFUL
+ #
+ # but take into account that at some point those ones have to be permitted:
+ #
+ # def person_params
+ # params.require(:person).permit(:name).tap do |person_params|
+ # person_params.require(:name) # SAFER
+ # end
+ # end
+ #
+ # for example.
def require(key)
+ return key.map { |k| require(k) } if key.is_a?(Array)
value = self[key]
if value.present? || value == false
value
@@ -226,7 +318,7 @@ module ActionController
#
# params.permit(:name)
#
- # +:name+ passes it is a key of +params+ whose associated value is of type
+ # +:name+ passes if it is a key of +params+ whose associated value is of type
# +String+, +Symbol+, +NilClass+, +Numeric+, +TrueClass+, +FalseClass+,
# +Date+, +Time+, +DateTime+, +StringIO+, +IO+,
# +ActionDispatch::Http::UploadedFile+ or +Rack::Test::UploadedFile+.
@@ -303,7 +395,13 @@ module ActionController
# params[:person] # => {"name"=>"Francesco"}
# params[:none] # => nil
def [](key)
- convert_hashes_to_parameters(key, super)
+ convert_hashes_to_parameters(key, @parameters[key])
+ end
+
+ # Assigns a value to a given +key+. The given key may still get filtered out
+ # when +permit+ is called.
+ def []=(key, value)
+ @parameters[key] = value
end
# Returns a parameter for the given +key+. If the +key+
@@ -314,13 +412,19 @@ module ActionController
#
# params = ActionController::Parameters.new(person: { name: 'Francesco' })
# params.fetch(:person) # => {"name"=>"Francesco"}
- # params.fetch(:none) # => ActionController::ParameterMissing: param not found: none
+ # params.fetch(:none) # => ActionController::ParameterMissing: param is missing or the value is empty: none
# params.fetch(:none, 'Francesco') # => "Francesco"
# params.fetch(:none) { 'Francesco' } # => "Francesco"
- def fetch(key, *args)
- convert_hashes_to_parameters(key, super, false)
- rescue KeyError
- raise ActionController::ParameterMissing.new(key)
+ def fetch(key, *args, &block)
+ convert_value_to_parameters(
+ @parameters.fetch(key) {
+ if block_given?
+ yield
+ else
+ args.fetch(0) { raise ActionController::ParameterMissing.new(key) }
+ end
+ }
+ )
end
# Returns a new <tt>ActionController::Parameters</tt> instance that
@@ -331,11 +435,117 @@ module ActionController
# params.slice(:a, :b) # => {"a"=>1, "b"=>2}
# params.slice(:d) # => {}
def slice(*keys)
- self.class.new(super).tap do |new_instance|
- new_instance.permitted = @permitted
+ new_instance_with_inherited_permitted_status(@parameters.slice(*keys))
+ end
+
+ # Returns current <tt>ActionController::Parameters</tt> instance which
+ # contains only the given +keys+.
+ def slice!(*keys)
+ @parameters.slice!(*keys)
+ self
+ end
+
+ # Returns a new <tt>ActionController::Parameters</tt> instance that
+ # filters out the given +keys+.
+ #
+ # params = ActionController::Parameters.new(a: 1, b: 2, c: 3)
+ # params.except(:a, :b) # => {"c"=>3}
+ # params.except(:d) # => {"a"=>1,"b"=>2,"c"=>3}
+ def except(*keys)
+ new_instance_with_inherited_permitted_status(@parameters.except(*keys))
+ end
+
+ # Removes and returns the key/value pairs matching the given keys.
+ #
+ # params = ActionController::Parameters.new(a: 1, b: 2, c: 3)
+ # params.extract!(:a, :b) # => {"a"=>1, "b"=>2}
+ # params # => {"c"=>3}
+ def extract!(*keys)
+ new_instance_with_inherited_permitted_status(@parameters.extract!(*keys))
+ end
+
+ # Returns a new <tt>ActionController::Parameters</tt> with the results of
+ # running +block+ once for every value. The keys are unchanged.
+ #
+ # params = ActionController::Parameters.new(a: 1, b: 2, c: 3)
+ # params.transform_values { |x| x * 2 }
+ # # => {"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
end
+ # Performs values transformation and returns the altered
+ # <tt>ActionController::Parameters</tt> instance.
+ def transform_values!(&block)
+ @parameters.transform_values!(&block)
+ self
+ end
+
+ # Returns a new <tt>ActionController::Parameters</tt> instance with the
+ # results of running +block+ once for every key. The values are unchanged.
+ def transform_keys(&block)
+ if block
+ new_instance_with_inherited_permitted_status(
+ @parameters.transform_keys(&block)
+ )
+ else
+ @parameters.transform_keys
+ end
+ end
+
+ # Performs keys transformation and returns the altered
+ # <tt>ActionController::Parameters</tt> instance.
+ def transform_keys!(&block)
+ @parameters.transform_keys!(&block)
+ self
+ end
+
+ # Deletes 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, &block)
+ convert_value_to_parameters(@parameters.delete(key))
+ end
+
+ # Returns a new instance of <tt>ActionController::Parameters</tt> with only
+ # items that the block evaluates to true.
+ def select(&block)
+ new_instance_with_inherited_permitted_status(@parameters.select(&block))
+ end
+
+ # Equivalent to Hash#keep_if, but returns nil if no changes were made.
+ def select!(&block)
+ @parameters.select!(&block)
+ self
+ end
+ alias_method :keep_if, :select!
+
+ # Returns a new instance of <tt>ActionController::Parameters</tt> with items
+ # that the block evaluates to true removed.
+ def reject(&block)
+ new_instance_with_inherited_permitted_status(@parameters.reject(&block))
+ end
+
+ # Removes items that the block evaluates to true and returns self.
+ def reject!(&block)
+ @parameters.reject!(&block)
+ self
+ end
+ alias_method :delete_if, :reject!
+
+ # Returns values that were assigned to the given +keys+. Note that all the
+ # +Hash+ objects will be converted to <tt>ActionController::Parameters</tt>.
+ def values_at(*keys)
+ convert_value_to_parameters(@parameters.values_at(*keys))
+ end
+
# Returns an exact copy of the <tt>ActionController::Parameters</tt>
# instance. +permitted+ state is kept on the duped object.
#
@@ -350,46 +560,72 @@ module ActionController
end
end
+ # Returns a new <tt>ActionController::Parameters</tt> with all keys from
+ # +other_hash+ merges into current hash.
+ def merge(other_hash)
+ new_instance_with_inherited_permitted_status(
+ @parameters.merge(other_hash)
+ )
+ end
+
+ # This is required by ActiveModel attribute assignment, so that user can
+ # pass +Parameters+ to a mass assignment methods in a model. It should not
+ # matter as we are using +HashWithIndifferentAccess+ internally.
+ def stringify_keys # :nodoc:
+ dup
+ end
+
protected
def permitted=(new_permitted)
@permitted = new_permitted
end
+ def fields_for_style?
+ @parameters.all? { |k, v| k =~ /\A-?\d+\z/ && v.is_a?(Hash) }
+ end
+
private
- def convert_hashes_to_parameters(key, value, assign_if_converted=true)
+ def new_instance_with_inherited_permitted_status(hash)
+ self.class.new(hash).tap do |new_instance|
+ new_instance.permitted = @permitted
+ end
+ end
+
+ def convert_hashes_to_parameters(key, value)
converted = convert_value_to_parameters(value)
- self[key] = converted if assign_if_converted && !converted.equal?(value)
+ @parameters[key] = converted unless converted.equal?(value)
converted
end
def convert_value_to_parameters(value)
- if value.is_a?(Array) && !converted_arrays.member?(value)
+ case value
+ when Array
+ return value if converted_arrays.member?(value)
converted = value.map { |_| convert_value_to_parameters(_) }
converted_arrays << converted
converted
- elsif value.is_a?(Parameters) || !value.is_a?(Hash)
- value
- else
+ when Hash
self.class.new(value)
+ else
+ value
end
end
def each_element(object)
- if object.is_a?(Array)
- object.map { |el| yield el }.compact
- elsif fields_for_style?(object)
- hash = object.class.new
- object.each { |k,v| hash[k] = yield v }
- hash
- else
- yield object
+ case object
+ when Array
+ object.grep(Parameters).map { |el| yield el }.compact
+ when Parameters
+ if object.fields_for_style?
+ hash = object.class.new
+ object.each { |k,v| hash[k] = yield v }
+ hash
+ else
+ yield object
+ end
end
end
- def fields_for_style?(object)
- object.is_a?(Hash) && object.all? { |k, v| k =~ /\A-?\d+\z/ && v.is_a?(Hash) }
- end
-
def unpermitted_parameters!(params)
unpermitted_keys = unpermitted_keys(params)
if unpermitted_keys.any?
@@ -451,14 +687,8 @@ module ActionController
end
def array_of_permitted_scalars?(value)
- if value.is_a?(Array)
- value.all? {|element| permitted_scalar?(element)}
- end
- end
-
- def array_of_permitted_scalars_filter(params, key)
- if has_key?(key) && array_of_permitted_scalars?(self[key])
- params[key] = self[key]
+ if value.is_a?(Array) && value.all? {|element| permitted_scalar?(element)}
+ yield value
end
end
@@ -469,17 +699,17 @@ module ActionController
# Slicing filters out non-declared keys.
slice(*filter.keys).each do |key, value|
next unless value
+ next unless has_key? key
if filter[key] == EMPTY_ARRAY
# Declaration { comment_ids: [] }.
- array_of_permitted_scalars_filter(params, key)
+ array_of_permitted_scalars?(self[key]) do |val|
+ params[key] = val
+ end
else
# Declaration { user: :name } or { user: [:name, :age, { address: ... }] }.
params[key] = each_element(value) do |element|
- if element.is_a?(Hash)
- element = self.class.new(element) unless element.respond_to?(:permit)
- element.permit(*Array.wrap(filter[key]))
- end
+ element.permit(*Array.wrap(filter[key]))
end
end
end
diff --git a/actionpack/lib/action_controller/metal/testing.rb b/actionpack/lib/action_controller/metal/testing.rb
index dd8da4b5dc..b2b3b4283f 100644
--- a/actionpack/lib/action_controller/metal/testing.rb
+++ b/actionpack/lib/action_controller/metal/testing.rb
@@ -2,14 +2,6 @@ module ActionController
module Testing
extend ActiveSupport::Concern
- include RackDelegation
-
- # TODO : Rewrite tests using controller.headers= to use Rack env
- def headers=(new_headers)
- @_response ||= ActionDispatch::Response.new
- @_response.headers.replace(new_headers)
- end
-
# Behavior specific to functional tests
module Functional # :nodoc:
def set_response!(request)
@@ -24,7 +16,7 @@ module ActionController
module ClassMethods
def before_filters
- _process_action_callbacks.find_all{|x| x.kind == :before}.map{|x| x.name}
+ _process_action_callbacks.find_all{|x| x.kind == :before}.map(&:name)
end
end
end
diff --git a/actionpack/lib/action_controller/metal/url_for.rb b/actionpack/lib/action_controller/metal/url_for.rb
index 07265be3fe..dbf7241a14 100644
--- a/actionpack/lib/action_controller/metal/url_for.rb
+++ b/actionpack/lib/action_controller/metal/url_for.rb
@@ -4,7 +4,10 @@ module ActionController
#
# In addition to <tt>AbstractController::UrlFor</tt>, this module accesses the HTTP layer to define
# url options like the +host+. In order to do so, this module requires the host class
- # to implement +env+ and +request+, which need to be a Rack-compatible.
+ # to implement +env+ which needs to be Rack-compatible and +request+
+ # which is either an instance of +ActionDispatch::Request+ or an object
+ # that responds to the +host+, +optional_port+, +protocol+ and
+ # +symbolized_path_parameter+ methods.
#
# class RootUrl
# include ActionController::UrlFor
@@ -28,20 +31,23 @@ module ActionController
:port => request.optional_port,
:protocol => request.protocol,
:_recall => request.path_parameters
- }.merge(super).freeze
+ }.merge!(super).freeze
- if (same_origin = _routes.equal?(env["action_dispatch.routes".freeze])) ||
- (script_name = env["ROUTES_#{_routes.object_id}_SCRIPT_NAME"]) ||
- (original_script_name = env['ORIGINAL_SCRIPT_NAME'.freeze])
+ if (same_origin = _routes.equal?(request.routes)) ||
+ (script_name = request.engine_script_name(_routes)) ||
+ (original_script_name = request.original_script_name)
- @_url_options.dup.tap do |options|
- if original_script_name
- options[:original_script_name] = original_script_name
+ options = @_url_options.dup
+ if original_script_name
+ options[:original_script_name] = original_script_name
+ else
+ if same_origin
+ options[:script_name] = request.script_name.empty? ? "".freeze : request.script_name.dup
else
- options[:script_name] = same_origin ? request.script_name.dup : script_name
+ options[:script_name] = script_name
end
- options.freeze
end
+ options.freeze
else
@_url_options
end
diff --git a/actionpack/lib/action_controller/middleware.rb b/actionpack/lib/action_controller/middleware.rb
deleted file mode 100644
index 437fec3dc6..0000000000
--- a/actionpack/lib/action_controller/middleware.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-module ActionController
- class Middleware < Metal
- class ActionMiddleware
- def initialize(controller, app)
- @controller, @app = controller, app
- end
-
- def call(env)
- request = ActionDispatch::Request.new(env)
- @controller.build(@app).dispatch(:index, request)
- end
- end
-
- class << self
- alias build new
-
- def new(app)
- ActionMiddleware.new(self, app)
- end
- end
-
- attr_internal :app
-
- def process(action)
- response = super
- self.status, self.headers, self.response_body = response if response.is_a?(Array)
- response
- end
-
- def initialize(app)
- super()
- @_app = app
- end
-
- def index
- call(env)
- end
- end
-end \ No newline at end of file
diff --git a/actionpack/lib/action_controller/model_naming.rb b/actionpack/lib/action_controller/model_naming.rb
deleted file mode 100644
index 785221dc3d..0000000000
--- a/actionpack/lib/action_controller/model_naming.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-module ActionController
- module ModelNaming
- # Converts the given object to an ActiveModel compliant one.
- def convert_to_model(object)
- object.respond_to?(:to_model) ? object.to_model : object
- end
-
- def model_name_from_record_or_class(record_or_class)
- (record_or_class.is_a?(Class) ? record_or_class : convert_to_model(record_or_class).class).model_name
- end
- end
-end
diff --git a/actionpack/lib/action_controller/renderer.rb b/actionpack/lib/action_controller/renderer.rb
new file mode 100644
index 0000000000..e4d19e9dba
--- /dev/null
+++ b/actionpack/lib/action_controller/renderer.rb
@@ -0,0 +1,111 @@
+require 'active_support/core_ext/hash/keys'
+
+module ActionController
+ # ActionController::Renderer allows to render arbitrary templates
+ # without requirement of being in controller actions.
+ #
+ # You get a concrete renderer class by invoking ActionController::Base#renderer.
+ # For example,
+ #
+ # ApplicationController.renderer
+ #
+ # It allows you to call method #render directly.
+ #
+ # ApplicationController.renderer.render template: '...'
+ #
+ # You can use a shortcut on controller to replace previous example with:
+ #
+ # ApplicationController.render template: '...'
+ #
+ # #render method allows you to use any options as when rendering in controller.
+ # For example,
+ #
+ # FooController.render :action, locals: { ... }, assigns: { ... }
+ #
+ # The template will be rendered in a Rack environment which is accessible through
+ # ActionController::Renderer#env. You can set it up in two ways:
+ #
+ # * by changing renderer defaults, like
+ #
+ # ApplicationController.renderer.defaults # => hash with default Rack environment
+ #
+ # * by initializing an instance of renderer by passing it a custom environment.
+ #
+ # ApplicationController.renderer.new(method: 'post', https: true)
+ #
+ class Renderer
+ attr_reader :defaults, :controller
+
+ DEFAULTS = {
+ http_host: 'example.org',
+ https: false,
+ method: 'get',
+ script_name: '',
+ input: ''
+ }.freeze
+
+ # Create a new renderer instance for a specific controller class.
+ def self.for(controller, env = {}, defaults = DEFAULTS)
+ new(controller, env, defaults)
+ end
+
+ # Create a new renderer for the same controller but with a new env.
+ def new(env = {})
+ self.class.new controller, env, defaults
+ end
+
+ # 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)
+ end
+
+ # Accepts a custom Rack environment to render templates in.
+ # It will be merged with ActionController::Renderer.defaults
+ def initialize(controller, env, defaults)
+ @controller = controller
+ @defaults = defaults
+ @env = normalize_keys defaults.merge(env)
+ end
+
+ # Render templates with any options from ActionController::Base#render_to_string.
+ def render(*args)
+ raise 'missing controller' unless controller
+
+ request = ActionDispatch::Request.new @env
+ request.routes = controller._routes
+
+ instance = controller.new
+ instance.set_request! request
+ instance.set_response! controller.make_response!(request)
+ instance.render_to_string(*args)
+ end
+
+ private
+ def normalize_keys(env)
+ new_env = {}
+ env.each_pair { |k,v| new_env[rack_key_for(k)] = rack_value_for(k, v) }
+ new_env
+ end
+
+ RACK_KEY_TRANSLATION = {
+ 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' },
+ method: ->(v) { v.upcase },
+ }
+
+ def rack_key_for(key); RACK_KEY_TRANSLATION[key]; end
+
+ def rack_value_for(key, value)
+ RACK_VALUE_TRANSLATION.fetch(key, IDENTITY).call value
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/template_assertions.rb b/actionpack/lib/action_controller/template_assertions.rb
new file mode 100644
index 0000000000..0179f4afcd
--- /dev/null
+++ b/actionpack/lib/action_controller/template_assertions.rb
@@ -0,0 +1,9 @@
+module ActionController
+ module TemplateAssertions
+ def assert_template(options = {}, message = nil)
+ raise NoMethodError,
+ "assert_template has been extracted to a gem. To continue using it,
+ add `gem 'rails-controller-testing'` to your Gemfile."
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb
index 71cb224f22..2cada1f68a 100644
--- a/actionpack/lib/action_controller/test_case.rb
+++ b/actionpack/lib/action_controller/test_case.rb
@@ -1,207 +1,57 @@
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'
module ActionController
- module TemplateAssertions
- extend ActiveSupport::Concern
+ class TestRequest < ActionDispatch::TestRequest #:nodoc:
+ DEFAULT_ENV = ActionDispatch::TestRequest::DEFAULT_ENV.dup
+ DEFAULT_ENV.delete 'PATH_INFO'
- included do
- setup :setup_subscriptions
- teardown :teardown_subscriptions
+ def self.new_session
+ TestSession.new
end
- RENDER_TEMPLATE_INSTANCE_VARIABLES = %w{partials templates layouts files}.freeze
-
- def setup_subscriptions
- RENDER_TEMPLATE_INSTANCE_VARIABLES.each do |instance_variable|
- instance_variable_set("@_#{instance_variable}", Hash.new(0))
- end
-
- @_subscribers = []
-
- @_subscribers << ActiveSupport::Notifications.subscribe("render_template.action_view") do |_name, _start, _finish, _id, payload|
- path = payload[:layout]
- if path
- @_layouts[path] += 1
- if path =~ /^layouts\/(.*)/
- @_layouts[$1] += 1
- end
- end
- end
-
- @_subscribers << ActiveSupport::Notifications.subscribe("!render_template.action_view") do |_name, _start, _finish, _id, payload|
- if virtual_path = payload[:virtual_path]
- partial = virtual_path =~ /^.*\/_[^\/]*$/
-
- if partial
- @_partials[virtual_path] += 1
- @_partials[virtual_path.split("/").last] += 1
- end
-
- @_templates[virtual_path] += 1
- else
- path = payload[:identifier]
- if path
- @_files[path] += 1
- @_files[path.split("/").last] += 1
- end
- end
- end
+ # 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
+ new(default_env.merge(env), new_session)
end
- def teardown_subscriptions
- @_subscribers.each do |subscriber|
- ActiveSupport::Notifications.unsubscribe(subscriber)
- end
+ def self.default_env
+ DEFAULT_ENV
end
+ private_class_method :default_env
- def process(*args)
- reset_template_assertion
- super
- end
+ def initialize(env, session)
+ super(env)
- def reset_template_assertion
- RENDER_TEMPLATE_INSTANCE_VARIABLES.each do |instance_variable|
- instance_variable_get("@_#{instance_variable}").clear
- end
+ self.session = session
+ self.session_options = TestSession::DEFAULT_OPTIONS
+ @custom_param_parsers = {
+ Mime[:xml] => lambda { |raw_post| Hash.from_xml(raw_post)['hash'] }
+ }
end
- # Asserts that the request was rendered with the appropriate template file or partials.
- #
- # # assert that the "new" view template was rendered
- # assert_template "new"
- #
- # # assert that the exact template "admin/posts/new" was rendered
- # assert_template %r{\Aadmin/posts/new\Z}
- #
- # # assert that the layout 'admin' was rendered
- # assert_template layout: 'admin'
- # assert_template layout: 'layouts/admin'
- # assert_template layout: :admin
- #
- # # assert that no layout was rendered
- # assert_template layout: nil
- # assert_template layout: false
- #
- # # assert that the "_customer" partial was rendered twice
- # assert_template partial: '_customer', count: 2
- #
- # # assert that no partials were rendered
- # assert_template partial: false
- #
- # In a view test case, you can also assert that specific locals are passed
- # to partials:
- #
- # # assert that the "_customer" partial was rendered with a specific object
- # assert_template partial: '_customer', locals: { customer: @customer }
- def assert_template(options = {}, message = nil)
- # Force body to be read in case the template is being streamed.
- response.body
-
- case options
- when NilClass, Regexp, String, Symbol
- options = options.to_s if Symbol === options
- rendered = @_templates
- msg = message || sprintf("expecting <%s> but rendering with <%s>",
- options.inspect, rendered.keys)
- matches_template =
- case options
- when String
- !options.empty? && rendered.any? do |t, num|
- options_splited = options.split(File::SEPARATOR)
- t_splited = t.split(File::SEPARATOR)
- t_splited.last(options_splited.size) == options_splited
- end
- when Regexp
- rendered.any? { |t,num| t.match(options) }
- when NilClass
- rendered.blank?
- end
- assert matches_template, msg
- when Hash
- options.assert_valid_keys(:layout, :partial, :locals, :count, :file)
-
- if options.key?(:layout)
- expected_layout = options[:layout]
- msg = message || sprintf("expecting layout <%s> but action rendered <%s>",
- expected_layout, @_layouts.keys)
-
- case expected_layout
- when String, Symbol
- assert_includes @_layouts.keys, expected_layout.to_s, msg
- when Regexp
- assert(@_layouts.keys.any? {|l| l =~ expected_layout }, msg)
- when nil, false
- assert(@_layouts.empty?, msg)
- end
- end
-
- if options[:file]
- assert_includes @_files.keys, options[:file]
- end
-
- if expected_partial = options[:partial]
- if expected_locals = options[:locals]
- if defined?(@_rendered_views)
- view = expected_partial.to_s.sub(/^_/, '').sub(/\/_(?=[^\/]+\z)/, '/')
-
- partial_was_not_rendered_msg = "expected %s to be rendered but it was not." % view
- assert_includes @_rendered_views.rendered_views, view, partial_was_not_rendered_msg
-
- msg = 'expecting %s to be rendered with %s but was with %s' % [expected_partial,
- expected_locals,
- @_rendered_views.locals_for(view)]
- assert(@_rendered_views.view_rendered?(view, options[:locals]), msg)
- else
- warn "the :locals option to #assert_template is only supported in a ActionView::TestCase"
- end
- elsif expected_count = options[:count]
- actual_count = @_partials[expected_partial]
- msg = message || sprintf("expecting %s to be rendered %s time(s) but rendered %s time(s)",
- expected_partial, expected_count, actual_count)
- assert(actual_count == expected_count.to_i, msg)
- else
- msg = message || sprintf("expecting partial <%s> but action rendered <%s>",
- options[:partial], @_partials.keys)
- assert_includes @_partials, expected_partial, msg
- end
- elsif options.key?(:partial)
- assert @_partials.empty?,
- "Expected no partials to be rendered"
- end
- else
- raise ArgumentError, "assert_template only accepts a String, Symbol, Hash, Regexp, or nil"
- end
+ def query_string=(string)
+ set_header Rack::QUERY_STRING, string
end
- end
-
- class TestRequest < ActionDispatch::TestRequest #:nodoc:
- DEFAULT_ENV = ActionDispatch::TestRequest::DEFAULT_ENV.dup
- DEFAULT_ENV.delete 'PATH_INFO'
-
- def initialize(env = {})
- super
- self.session = TestSession.new
- self.session_options = TestSession::DEFAULT_OPTIONS.merge(:id => SecureRandom.hex(16))
+ def content_type=(type)
+ set_header 'CONTENT_TYPE', type
end
- def assign_parameters(routes, controller_path, action, parameters = {})
- parameters = parameters.symbolize_keys.merge(:controller => controller_path, :action => action)
- extra_keys = routes.extra_keys(parameters)
- non_path_parameters = get? ? query_parameters : request_parameters
- parameters.each do |key, value|
- if value.is_a?(Array) && (value.frozen? || value.any?(&:frozen?))
- value = value.map{ |v| v.duplicable? ? v.dup : v }
- elsif value.is_a?(Hash) && (value.frozen? || value.any?{ |k,v| v.frozen? })
- value = Hash[value.map{ |k,v| [k, v.duplicable? ? v.dup : v] }]
- elsif value.frozen? && value.duplicable?
- value = value.dup
- end
+ def assign_parameters(routes, controller_path, action, parameters, generated_path, query_string_keys)
+ non_path_parameters = {}
+ path_parameters = {}
- if extra_keys.include?(key)
+ parameters.each do |key, value|
+ if query_string_keys.include?(key)
non_path_parameters[key] = value
else
if value.is_a?(Array)
@@ -214,72 +64,88 @@ module ActionController
end
end
- # Clear the combined params hash in case it was already referenced.
- @env.delete("action_dispatch.request.parameters")
+ if get?
+ if self.query_string.blank?
+ self.query_string = non_path_parameters.to_query
+ end
+ else
+ if ENCODER.should_multipart?(non_path_parameters)
+ self.content_type = ENCODER.content_type
+ data = ENCODER.build_multipart non_path_parameters
+ else
+ fetch_header('CONTENT_TYPE') do |k|
+ set_header k, 'application/x-www-form-urlencoded'
+ end
- # Clear the filter cache variables so they're not stale
- @filtered_parameters = @filtered_env = @filtered_path = nil
+ case content_mime_type.to_sym
+ when nil
+ raise "Unknown Content-Type: #{content_type}"
+ when :json
+ data = ActiveSupport::JSON.encode(non_path_parameters)
+ when :xml
+ data = non_path_parameters.to_xml
+ when :url_encoded_form
+ data = non_path_parameters.to_query
+ else
+ @custom_param_parsers[content_mime_type] = ->(_) { non_path_parameters }
+ data = non_path_parameters.to_query
+ end
+ end
- params = self.request_parameters.dup
- %w(controller action only_path).each do |k|
- params.delete(k)
- params.delete(k.to_sym)
+ set_header 'CONTENT_LENGTH', data.length.to_s
+ set_header 'rack.input', StringIO.new(data)
end
- data = params.to_query
- @env['CONTENT_LENGTH'] = data.length.to_s
- @env['rack.input'] = StringIO.new(data)
- end
+ fetch_header("PATH_INFO") do |k|
+ set_header k, generated_path
+ end
+ path_parameters[:controller] = controller_path
+ path_parameters[:action] = action
- def recycle!
- @formats = nil
- @env.delete_if { |k, v| k =~ /^(action_dispatch|rack)\.request/ }
- @env.delete_if { |k, v| k =~ /^action_dispatch\.rescue/ }
- @method = @request_method = nil
- @fullpath = @ip = @remote_ip = @protocol = nil
- @env['action_dispatch.request.query_parameters'] = {}
- @set_cookies ||= {}
- @set_cookies.update(Hash[cookie_jar.instance_variable_get("@set_cookies").map{ |k,o| [k,o[:value]] }])
- deleted_cookies = cookie_jar.instance_variable_get("@delete_cookies")
- @set_cookies.reject!{ |k,v| deleted_cookies.include?(k) }
- cookie_jar.update(rack_cookies)
- cookie_jar.update(cookies)
- cookie_jar.update(@set_cookies)
- cookie_jar.recycle!
+ self.path_parameters = path_parameters
end
- private
+ ENCODER = Class.new do
+ include Rack::Test::Utils
+
+ def should_multipart?(params)
+ # FIXME: lifted from Rack-Test. We should push this separation upstream
+ multipart = false
+ query = lambda { |value|
+ case value
+ when Array
+ value.each(&query)
+ when Hash
+ value.values.each(&query)
+ when Rack::Test::UploadedFile
+ multipart = true
+ end
+ }
+ params.values.each(&query)
+ multipart
+ end
- def default_env
- DEFAULT_ENV
- end
- end
+ public :build_multipart
- class TestResponse < ActionDispatch::TestResponse
- def recycle!
- initialize
- end
- end
+ def content_type
+ "multipart/form-data; boundary=#{Rack::Test::MULTIPART_BOUNDARY}"
+ end
+ end.new
- class LiveTestResponse < Live::Response
- def recycle!
- @body = nil
- initialize
- end
+ private
- def body
- @body ||= super
+ def params_parsers
+ super.merge @custom_param_parsers
end
+ end
+ class LiveTestResponse < Live::Response
# Was the response successful?
alias_method :success?, :successful?
# Was the URL not found?
alias_method :missing?, :not_found?
- # Were we redirected?
- alias_method :redirect?, :redirection?
-
# Was there a server-side error?
alias_method :error?, :server_error?
end
@@ -287,7 +153,7 @@ module ActionController
# Methods #destroy and #load! are overridden to avoid calling methods on the
# @store object, which does not exist for the TestSession class.
class TestSession < Rack::Session::Abstract::SessionHash #:nodoc:
- DEFAULT_OPTIONS = Rack::Session::Abstract::ID::DEFAULT_OPTIONS
+ DEFAULT_OPTIONS = Rack::Session::Abstract::Persisted::DEFAULT_OPTIONS
def initialize(session = {})
super(nil, nil)
@@ -312,6 +178,10 @@ module ActionController
clear
end
+ def fetch(key, *args, &block)
+ @data.fetch(key.to_s, *args, &block)
+ end
+
private
def load!
@@ -338,7 +208,7 @@ module ActionController
# class BooksControllerTest < ActionController::TestCase
# def test_create
# # Simulate a POST response with the given HTTP parameters.
- # post(:create, book: { title: "Love Hina" })
+ # post(:create, params: { book: { title: "Love Hina" }})
#
# # Assert that the controller tried to redirect us to
# # the created book's URI.
@@ -368,7 +238,7 @@ module ActionController
# request. You can modify this object before sending the HTTP request. For example,
# you might want to set some session properties before sending a GET request.
# <b>@response</b>::
- # An ActionController::TestResponse object, representing the response
+ # An ActionDispatch::TestResponse object, representing the response
# of the last HTTP response. In the above example, <tt>@response</tt> becomes valid
# after calling +post+. If the various assert methods are not sufficient, then you
# may use this object to inspect the HTTP response in detail.
@@ -391,21 +261,15 @@ module ActionController
# In addition to these specific assertions, you also have easy access to various collections that the regular test/unit assertions
# can be used against. These collections are:
#
- # * assigns: Instance variables assigned in the action that are available for the view.
# * session: Objects being saved in the session.
# * flash: The flash objects currently in the session.
# * cookies: \Cookies being sent to the user on this request.
#
# These collections can be used just like any other hash:
#
- # assert_not_nil assigns(:person) # makes sure that a @person instance variable was set
# 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
#
- # For historic reasons, the assigns hash uses string-based keys. So <tt>assigns[:person]</tt> won't work, but <tt>assigns["person"]</tt> will. To
- # appease our yearning for symbols, though, an alternative accessor has been devised using a method call instead of index referencing.
- # So <tt>assigns(:person)</tt> will work just like <tt>assigns["person"]</tt>, but again, <tt>assigns[:person]</tt> will not work.
- #
# 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
@@ -433,6 +297,7 @@ module ActionController
extend ActiveSupport::Concern
include ActionDispatch::TestProcess
include ActiveSupport::Testing::ConstantLookup
+ include Rails::Dom::Testing::Assertions
attr_reader :response, :request
@@ -456,7 +321,6 @@ module ActionController
end
def controller_class=(new_class)
- prepare_controller_class(new_class) if new_class
self._controller_class = new_class
end
@@ -473,122 +337,141 @@ module ActionController
Class === constant && constant < ActionController::Metal
end
end
-
- def prepare_controller_class(new_class)
- new_class.send :include, ActionController::TestCase::RaiseActionExceptions
- end
-
end
# Simulate a GET request with the given parameters.
#
# - +action+: The controller action to call.
- # - +parameters+: The HTTP parameters that you want to pass. This may
- # be +nil+, a hash, or a string that is appropriately encoded
+ # - +params+: The hash with HTTP parameters that you want to pass. This may be +nil+.
+ # - +body+: The request body with a string that is appropriately encoded
# (<tt>application/x-www-form-urlencoded</tt> or <tt>multipart/form-data</tt>).
# - +session+: A hash of parameters to store in the session. This may be +nil+.
# - +flash+: A hash of parameters to store in the flash. This may be +nil+.
#
# You can also simulate POST, PATCH, PUT, DELETE, and HEAD requests with
# +post+, +patch+, +put+, +delete+, and +head+.
+ # Example sending parameters, session and setting a flash message:
+ #
+ # get :show,
+ # params: { id: 7 },
+ # session: { user_id: 1 },
+ # flash: { notice: 'This is flash message' }
#
# Note that the request method is not verified. The different methods are
# available to make the tests more expressive.
def get(action, *args)
- process(action, "GET", *args)
+ res = process_with_kwargs("GET", action, *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(action, "POST", *args)
+ process_with_kwargs("POST", action, *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(action, "PATCH", *args)
+ process_with_kwargs("PATCH", action, *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(action, "PUT", *args)
+ process_with_kwargs("PUT", action, *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(action, "DELETE", *args)
+ process_with_kwargs("DELETE", action, *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(action, "HEAD", *args)
+ process_with_kwargs("HEAD", action, *args)
end
- def xml_http_request(request_method, action, parameters = nil, session = nil, flash = nil)
+ 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', Mime::ALL].join(', ')
- __send__(request_method, action, parameters, session, flash).tap do
+ @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
- def paramify_values(hash_or_array_or_value)
- case hash_or_array_or_value
- when Hash
- Hash[hash_or_array_or_value.map{|key, value| [key, paramify_values(value)] }]
- when Array
- hash_or_array_or_value.map {|i| paramify_values(i)}
- when Rack::Test::UploadedFile, ActionDispatch::Http::UploadedFile
- hash_or_array_or_value
- else
- hash_or_array_or_value.to_param
- end
- end
-
# Simulate a HTTP request to +action+ by specifying request method,
# parameters and set/volley the response.
#
# - +action+: The controller action to call.
- # - +http_method+: Request method used to send the http request. Possible values
- # are +GET+, +POST+, +PATCH+, +PUT+, +DELETE+, +HEAD+. Defaults to +GET+.
- # - +parameters+: The HTTP parameters. This may be +nil+, a hash, or a
- # string that is appropriately encoded (+application/x-www-form-urlencoded+
- # or +multipart/form-data+).
+ # - +method+: Request method used to send the HTTP request. Possible values
+ # are +GET+, +POST+, +PATCH+, +PUT+, +DELETE+, +HEAD+. Defaults to +GET+. Can be a symbol.
+ # - +params+: The hash with HTTP parameters that you want to pass. This may be +nil+.
+ # - +body+: The request body with a string that is appropriately encoded
+ # (<tt>application/x-www-form-urlencoded</tt> or <tt>multipart/form-data</tt>).
# - +session+: A hash of parameters to store in the session. This may be +nil+.
# - +flash+: A hash of parameters to store in the flash. This may be +nil+.
+ # - +format+: Request format. Defaults to +nil+. Can be string or symbol.
#
# Example calling +create+ action and sending two params:
#
- # process :create, 'POST', user: { name: 'Gaurish Sharma', email: 'user@example.com' }
- #
- # Example sending parameters, +nil+ session and setting a flash message:
- #
- # process :view, 'GET', { id: 7 }, nil, { notice: 'This is flash message' }
+ # process :create,
+ # method: 'POST',
+ # params: {
+ # user: { name: 'Gaurish Sharma', email: 'user@example.com' }
+ # },
+ # session: { user_id: 1 },
+ # flash: { notice: 'This is flash message' }
#
# To simulate +GET+, +POST+, +PATCH+, +PUT+, +DELETE+ and +HEAD+ requests
# prefer using #get, #post, #patch, #put, #delete and #head methods
# respectively which will make tests more expressive.
#
# Note that the request method is not verified.
- def process(action, http_method = 'GET', *args)
+ def process(action, *args)
check_required_ivars
- if args.first.is_a?(String) && http_method != 'HEAD'
- @request.env['RAW_POST_DATA'] = args.shift
+ 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
+
+ if parameters.is_a?(String) && http_method != 'HEAD'
+ body = parameters
+ parameters = nil
+ end
+
+ if parameters.present? || session.present? || flash.present?
+ non_kwarg_request_warning
+ end
+ end
+
+ if body.present?
+ @request.set_header 'RAW_POST_DATA', body
+ end
+
+ if http_method.present?
+ http_method = http_method.to_s.upcase
+ else
+ http_method = "GET"
end
- parameters, session, flash = args
parameters ||= {}
- # Ensure that numbers and symbols passed as params are converted to
- # proper params, as is the case when engaging rack.
- parameters = paramify_values(parameters) if html_format?(parameters)
+ if format.present?
+ parameters[:format] = format
+ end
@html_document = nil
@@ -596,55 +479,91 @@ module ActionController
@controller.extend(Testing::Functional)
end
- @request.recycle!
- @response.recycle!
+ 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.env['REQUEST_METHOD'] = http_method
+ @request.set_header 'REQUEST_METHOD', http_method
+
+ parameters = parameters.symbolize_keys
- controller_class_name = @controller.class.anonymous? ?
- "anonymous" :
- @controller.class.controller_path
+ 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)
- @request.assign_parameters(@routes, controller_class_name, action.to_s, parameters)
+ @request.assign_parameters(@routes, controller_class_name, action.to_s, parameters, generated_path, query_string_keys)
@request.session.update(session) if session
@request.flash.update(flash || {})
+ if xhr
+ @request.set_header 'HTTP_X_REQUESTED_WITH', 'XMLHttpRequest'
+ @request.fetch_header('HTTP_ACCEPT') do |k|
+ @request.set_header k, [Mime[:js], Mime[:html], Mime[:xml], 'text/xml', '*/*'].join(', ')
+ end
+ end
+
@controller.request = @request
@controller.response = @response
- build_request_uri(action, parameters)
-
- name = @request.parameters[:action]
+ @request.fetch_header("SCRIPT_NAME") do |k|
+ @request.set_header k, @controller.config.relative_url_root
+ end
@controller.recycle!
- @controller.process(name)
+ @controller.process(action)
+
+ @request.delete_header 'HTTP_COOKIE'
- if cookies = @request.env['action_dispatch.cookies']
- unless @response.committed?
- cookies.write(@response)
+ 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))
end
end
@response.prepare!
- @assigns = @controller.respond_to?(:view_assigns) ? @controller.view_assigns : {}
-
if flash_value = @request.flash.to_session_value
@request.session['flash'] = flash_value
+ else
+ @request.session.delete('flash')
end
+ if xhr
+ @request.delete_header 'HTTP_X_REQUESTED_WITH'
+ @request.delete_header 'HTTP_ACCEPT'
+ end
+ @request.query_string = ''
+
@response
end
+ def controller_class_name
+ @controller.class.anonymous? ? "anonymous" : @controller.class.controller_path
+ end
+
+ def generated_path(generated_extras)
+ generated_extras[0]
+ end
+
+ def query_parameter_names(generated_extras)
+ generated_extras[1] + [:controller, :action]
+ end
+
def setup_controller_request_and_response
@controller = nil unless defined? @controller
- response_klass = TestResponse
+ @response_klass = ActionDispatch::TestResponse
if klass = self.class.controller_class
if klass < ActionController::Live
- response_klass = LiveTestResponse
+ @response_klass = LiveTestResponse
end
unless @controller
begin
@@ -655,8 +574,8 @@ module ActionController
end
end
- @request = build_request
- @response = build_response response_klass
+ @request = TestRequest.create
+ @response = build_response @response_klass
@response.request = @request
if @controller
@@ -665,12 +584,8 @@ module ActionController
end
end
- def build_request
- TestRequest.new
- end
-
def build_response(klass)
- klass.new
+ klass.create
end
included do
@@ -681,6 +596,51 @@ module ActionController
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)
+ 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 check_required_ivars
# Sanity check for required instance variables so we can give an
# understandable error message.
@@ -691,57 +651,12 @@ module ActionController
end
end
- def build_request_uri(action, parameters)
- unless @request.env["PATH_INFO"]
- options = @controller.respond_to?(:url_options) ? @controller.__send__(:url_options).merge(parameters) : parameters
- options.update(
- :only_path => true,
- :action => action,
- :relative_url_root => nil,
- :_recall => @request.path_parameters)
-
- url, query_string = @routes.url_for(options).split("?", 2)
-
- @request.env["SCRIPT_NAME"] = @controller.config.relative_url_root
- @request.env["PATH_INFO"] = url
- @request.env["QUERY_STRING"] = query_string || ""
- end
- end
-
def html_format?(parameters)
return true unless parameters.key?(:format)
Mime.fetch(parameters[:format]) { Mime['html'] }.html?
end
end
- # When the request.remote_addr remains the default for testing, which is 0.0.0.0, the exception is simply raised inline
- # (skipping the regular exception handling from rescue_action). If the request.remote_addr is anything else, the regular
- # rescue_action process takes place. This means you can test your rescue_action code by setting remote_addr to something else
- # than 0.0.0.0.
- #
- # The exception is stored in the exception accessor for further inspection.
- module RaiseActionExceptions
- def self.included(base) #:nodoc:
- unless base.method_defined?(:exception) && base.method_defined?(:exception=)
- base.class_eval do
- attr_accessor :exception
- protected :exception, :exception=
- end
- end
- end
-
- protected
- def rescue_action_without_handler(e)
- self.exception = e
-
- if request.remote_addr == "0.0.0.0"
- raise(e)
- else
- super(e)
- end
- end
- end
-
include Behavior
end
end
diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb
index 11b5e6be33..f6336c8c7a 100644
--- a/actionpack/lib/action_dispatch.rb
+++ b/actionpack/lib/action_dispatch.rb
@@ -1,5 +1,5 @@
#--
-# Copyright (c) 2004-2014 David Heinemeier Hansson
+# Copyright (c) 2004-2015 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@@ -52,6 +52,7 @@ module ActionDispatch
autoload :DebugExceptions
autoload :ExceptionWrapper
autoload :Flash
+ autoload :LoadInterlock
autoload :ParamsParser
autoload :PublicExceptions
autoload :Reloader
diff --git a/actionpack/lib/action_dispatch/http/cache.rb b/actionpack/lib/action_dispatch/http/cache.rb
index 63a3cbc90b..30ade14c26 100644
--- a/actionpack/lib/action_dispatch/http/cache.rb
+++ b/actionpack/lib/action_dispatch/http/cache.rb
@@ -1,4 +1,3 @@
-
module ActionDispatch
module Http
module Cache
@@ -8,13 +7,13 @@ module ActionDispatch
HTTP_IF_NONE_MATCH = 'HTTP_IF_NONE_MATCH'.freeze
def if_modified_since
- if since = env[HTTP_IF_MODIFIED_SINCE]
+ if since = get_header(HTTP_IF_MODIFIED_SINCE)
Time.rfc2822(since) rescue nil
end
end
def if_none_match
- env[HTTP_IF_NONE_MATCH]
+ get_header HTTP_IF_NONE_MATCH
end
def if_none_match_etags
@@ -51,51 +50,51 @@ module ActionDispatch
end
module Response
- attr_reader :cache_control, :etag
- alias :etag? :etag
+ attr_reader :cache_control
def last_modified
- if last = headers[LAST_MODIFIED]
+ if last = get_header(LAST_MODIFIED)
Time.httpdate(last)
end
end
def last_modified?
- headers.include?(LAST_MODIFIED)
+ has_header? LAST_MODIFIED
end
def last_modified=(utc_time)
- headers[LAST_MODIFIED] = utc_time.httpdate
+ set_header LAST_MODIFIED, utc_time.httpdate
end
def date
- if date_header = headers['Date']
+ if date_header = get_header(DATE)
Time.httpdate(date_header)
end
end
def date?
- headers.include?('Date')
+ has_header? DATE
end
def date=(utc_time)
- headers['Date'] = utc_time.httpdate
+ set_header DATE, utc_time.httpdate
end
def etag=(etag)
key = ActiveSupport::Cache.expand_cache_key(etag)
- @etag = self[ETAG] = %("#{Digest::MD5.hexdigest(key)}")
+ super %("#{Digest::MD5.hexdigest(key)}")
end
+ def etag?; etag; end
+
private
+ DATE = 'Date'.freeze
LAST_MODIFIED = "Last-Modified".freeze
- ETAG = "ETag".freeze
- CACHE_CONTROL = "Cache-Control".freeze
SPECIAL_KEYS = Set.new(%w[extras no-cache max-age public must-revalidate])
def cache_control_segments
- if cache_control = self[CACHE_CONTROL]
+ if cache_control = _cache_control
cache_control.delete(' ').split(',')
else
[]
@@ -122,12 +121,11 @@ module ActionDispatch
def prepare_cache_control!
@cache_control = cache_control_headers
- @etag = self[ETAG]
end
def handle_conditional_get!
if etag? || last_modified? || !@cache_control.empty?
- set_conditional_cache_control!
+ set_conditional_cache_control!(@cache_control)
end
end
@@ -137,24 +135,24 @@ module ActionDispatch
PRIVATE = "private".freeze
MUST_REVALIDATE = "must-revalidate".freeze
- def set_conditional_cache_control!
+ def set_conditional_cache_control!(cache_control)
control = {}
cc_headers = cache_control_headers
if extras = cc_headers.delete(:extras)
- @cache_control[:extras] ||= []
- @cache_control[:extras] += extras
- @cache_control[:extras].uniq!
+ cache_control[:extras] ||= []
+ cache_control[:extras] += extras
+ cache_control[:extras].uniq!
end
control.merge! cc_headers
- control.merge! @cache_control
+ control.merge! cache_control
if control.empty?
- headers[CACHE_CONTROL] = DEFAULT_CACHE_CONTROL
+ self._cache_control = DEFAULT_CACHE_CONTROL
elsif control[:no_cache]
- headers[CACHE_CONTROL] = NO_CACHE
+ self._cache_control = NO_CACHE
if control[:extras]
- headers[CACHE_CONTROL] += ", #{control[:extras].join(', ')}"
+ self._cache_control = _cache_control + ", #{control[:extras].join(', ')}"
end
else
extras = control[:extras]
@@ -166,7 +164,7 @@ module ActionDispatch
options << MUST_REVALIDATE if control[:must_revalidate]
options.concat(extras) if extras
- headers[CACHE_CONTROL] = options.join(", ")
+ self._cache_control = options.join(", ")
end
end
end
diff --git a/actionpack/lib/action_dispatch/http/filter_parameters.rb b/actionpack/lib/action_dispatch/http/filter_parameters.rb
index 2b851cc28d..9dcab79c3a 100644
--- a/actionpack/lib/action_dispatch/http/filter_parameters.rb
+++ b/actionpack/lib/action_dispatch/http/filter_parameters.rb
@@ -1,5 +1,3 @@
-require 'active_support/core_ext/hash/keys'
-require 'active_support/core_ext/object/duplicable'
require 'action_dispatch/http/parameter_filter'
module ActionDispatch
@@ -16,7 +14,7 @@ module ActionDispatch
# env["action_dispatch.parameter_filter"] = [:foo, "bar"]
# => replaces the value to all keys matching /foo|bar/i with "[FILTERED]"
#
- # env["action_dispatch.parameter_filter"] = lambda do |k,v|
+ # env["action_dispatch.parameter_filter"] = -> (k, v) do
# v.reverse! if k =~ /secret/i
# end
# => reverses the value to all keys matching /secret/i
@@ -25,19 +23,19 @@ module ActionDispatch
NULL_PARAM_FILTER = ParameterFilter.new # :nodoc:
NULL_ENV_FILTER = ParameterFilter.new ENV_MATCH # :nodoc:
- def initialize(env)
+ def initialize
super
@filtered_parameters = nil
@filtered_env = nil
@filtered_path = nil
end
- # Return a hash of parameters with all sensitive data replaced.
+ # Returns a hash of parameters with all sensitive data replaced.
def filtered_parameters
@filtered_parameters ||= parameter_filter.filter(parameters)
end
- # Return a hash of request.env with all sensitive data replaced.
+ # Returns a hash of request.env with all sensitive data replaced.
def filtered_env
@filtered_env ||= env_filter.filter(@env)
end
@@ -50,13 +48,13 @@ module ActionDispatch
protected
def parameter_filter
- parameter_filter_for @env.fetch("action_dispatch.parameter_filter") {
+ parameter_filter_for fetch_header("action_dispatch.parameter_filter") {
return NULL_PARAM_FILTER
}
end
def env_filter
- user_key = @env.fetch("action_dispatch.parameter_filter") {
+ user_key = fetch_header("action_dispatch.parameter_filter") {
return NULL_ENV_FILTER
}
parameter_filter_for(Array(user_key) + ENV_MATCH)
diff --git a/actionpack/lib/action_dispatch/http/filter_redirect.rb b/actionpack/lib/action_dispatch/http/filter_redirect.rb
index cd603649c3..f4b806b8b5 100644
--- a/actionpack/lib/action_dispatch/http/filter_redirect.rb
+++ b/actionpack/lib/action_dispatch/http/filter_redirect.rb
@@ -4,9 +4,8 @@ module ActionDispatch
FILTERED = '[FILTERED]'.freeze # :nodoc:
- def filtered_location
- filters = location_filter
- if !filters.empty? && location_filter_match?(filters)
+ def filtered_location # :nodoc:
+ if location_filter_match?
FILTERED
else
location
@@ -15,20 +14,20 @@ module ActionDispatch
private
- def location_filter
+ def location_filters
if request
- request.env['action_dispatch.redirect_filter'] || []
+ request.get_header('action_dispatch.redirect_filter') || []
else
[]
end
end
- def location_filter_match?(filters)
- filters.any? do |filter|
+ def location_filter_match?
+ location_filters.any? do |filter|
if String === filter
location.include?(filter)
elsif Regexp === filter
- location.match(filter)
+ location =~ filter
end
end
end
diff --git a/actionpack/lib/action_dispatch/http/headers.rb b/actionpack/lib/action_dispatch/http/headers.rb
index bc5410dc38..12f81dc1a5 100644
--- a/actionpack/lib/action_dispatch/http/headers.rb
+++ b/actionpack/lib/action_dispatch/http/headers.rb
@@ -30,27 +30,37 @@ module ActionDispatch
HTTP_HEADER = /\A[A-Za-z0-9-]+\z/
include Enumerable
- attr_reader :env
- def initialize(env = {}) # :nodoc:
- @env = env
+ def self.from_hash(hash)
+ new ActionDispatch::Request.new hash
+ end
+
+ def initialize(request) # :nodoc:
+ @req = request
end
# Returns the value for the given key mapped to @env.
def [](key)
- @env[env_name(key)]
+ @req.get_header env_name(key)
end
# Sets the given value for the key mapped to @env.
def []=(key, value)
- @env[env_name(key)] = value
+ @req.set_header env_name(key), value
+ end
+
+ # Add a value to a multivalued header like Vary or Accept-Encoding.
+ def add(key, value)
+ @req.add_header env_name(key), value
end
def key?(key)
- @env.key? env_name(key)
+ @req.has_header? env_name(key)
end
alias :include? :key?
+ DEFAULT = Object.new # :nodoc:
+
# Returns the value for the given key mapped to @env.
#
# If the key is not found and an optional code block is not provided,
@@ -58,18 +68,22 @@ module ActionDispatch
#
# If the code block is provided, then it will be run and
# its result returned.
- def fetch(key, *args, &block)
- @env.fetch env_name(key), *args, &block
+ def fetch(key, default = DEFAULT)
+ @req.fetch_header(env_name(key)) do
+ return default unless default == DEFAULT
+ return yield if block_given?
+ raise NameError, key
+ end
end
def each(&block)
- @env.each(&block)
+ @req.each_header(&block)
end
# Returns a new Http::Headers instance containing the contents of
# <tt>headers_or_env</tt> and the original instance.
def merge(headers_or_env)
- headers = Http::Headers.new(env.dup)
+ headers = @req.dup.headers
headers.merge!(headers_or_env)
headers
end
@@ -79,11 +93,14 @@ module ActionDispatch
# <tt>headers_or_env</tt>.
def merge!(headers_or_env)
headers_or_env.each do |key, value|
- self[env_name(key)] = value
+ @req.set_header env_name(key), value
end
end
+ def env; @req.env.dup; end
+
private
+
# Converts a HTTP header name to an environment variable name if it is
# not contained within the headers hash.
def env_name(key)
diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb
index 9c8f65deac..7acf91902d 100644
--- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb
+++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb
@@ -10,19 +10,18 @@ module ActionDispatch
self.ignore_accept_header = false
end
- attr_reader :variant
-
- # The MIME type of the HTTP request, such as Mime::XML.
+ # 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
- @env["action_dispatch.request.content_type"] ||= begin
- if @env['CONTENT_TYPE'] =~ /^([^,\;]*)/
+ fetch_header("action_dispatch.request.content_type") do |k|
+ v = if get_header('CONTENT_TYPE') =~ /^([^,\;]*)/
Mime::Type.lookup($1.strip.downcase)
else
nil
end
+ set_header k, v
end
end
@@ -30,62 +29,73 @@ module ActionDispatch
content_mime_type && content_mime_type.to_s
end
+ def has_content_type?
+ has_header? 'CONTENT_TYPE'
+ end
+
# Returns the accepted MIME type for the request.
def accepts
- @env["action_dispatch.request.accepts"] ||= begin
- header = @env['HTTP_ACCEPT'].to_s.strip
+ fetch_header("action_dispatch.request.accepts") do |k|
+ header = get_header('HTTP_ACCEPT').to_s.strip
- if header.empty?
+ v = if header.empty?
[content_mime_type]
else
Mime::Type.parse(header)
end
+ set_header k, v
end
end
# Returns the MIME type for the \format used in the request.
#
- # GET /posts/5.xml | request.format => Mime::XML
- # GET /posts/5.xhtml | request.format => Mime::HTML
- # GET /posts/5 | request.format => Mime::HTML or MIME::JS, or request.accepts.first
+ # GET /posts/5.xml | request.format => Mime[:xml]
+ # GET /posts/5.xhtml | request.format => Mime[:html]
+ # GET /posts/5 | request.format => Mime[:html] or Mime[:js], or request.accepts.first
#
def format(view_path = [])
formats.first || Mime::NullType.instance
end
def formats
- @env["action_dispatch.request.formats"] ||= begin
+ fetch_header("action_dispatch.request.formats") do |k|
params_readable = begin
parameters[:format]
rescue ActionController::BadRequest
false
end
- if params_readable
+ v = if params_readable
Array(Mime[parameters[:format]])
elsif use_accept_header && valid_accept_header
accepts
elsif xhr?
- [Mime::JS]
+ [Mime[:js]]
else
- [Mime::HTML]
+ [Mime[:html]]
end
+ set_header k, v
end
end
+
# Sets the \variant for template.
def variant=(variant)
- if variant.is_a?(Symbol)
- @variant = [variant]
- elsif variant.is_a?(Array) && variant.any? && variant.all?{ |v| v.is_a?(Symbol) }
- @variant = variant
+ variant = Array(variant)
+
+ if variant.all? { |v| v.is_a?(Symbol) }
+ @variant = ActiveSupport::ArrayInquirer.new(variant)
else
- raise ArgumentError, "request.variant must be set to a Symbol or an Array of Symbols, not a #{variant.class}. " \
+ 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'"
end
end
+ def variant
+ @variant ||= ActiveSupport::ArrayInquirer.new
+ end
+
# Sets the \format by string extension, which can be used to force custom formats
# that are not controlled by the extension.
#
@@ -99,7 +109,7 @@ module ActionDispatch
# end
def format=(extension)
parameters[:format] = extension.to_s
- @env["action_dispatch.request.formats"] = [Mime::Type.lookup_by_extension(parameters[:format])]
+ set_header "action_dispatch.request.formats", [Mime::Type.lookup_by_extension(parameters[:format])]
end
# Sets the \formats by string extensions. This differs from #format= by allowing you
@@ -118,9 +128,9 @@ module ActionDispatch
# end
def formats=(extensions)
parameters[:format] = extensions.first.to_s
- @env["action_dispatch.request.formats"] = extensions.collect do |extension|
+ set_header "action_dispatch.request.formats", extensions.collect { |extension|
Mime::Type.lookup_by_extension(extension)
- end
+ }
end
# Receives an array of mimes and return the first user sent mime that
diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb
index 9450be838c..b64f660ec5 100644
--- a/actionpack/lib/action_dispatch/http/mime_type.rb
+++ b/actionpack/lib/action_dispatch/http/mime_type.rb
@@ -1,23 +1,31 @@
-require 'set'
require 'singleton'
require 'active_support/core_ext/module/attribute_accessors'
require 'active_support/core_ext/string/starts_ends_with'
module Mime
- class Mimes < Array
- def symbols
- @symbols ||= map { |m| m.to_sym }
+ class Mimes
+ include Enumerable
+
+ def initialize
+ @mimes = []
+ @symbols = nil
end
- %w(<< concat shift unshift push pop []= clear compact! collect!
- delete delete_at delete_if flatten! map! insert reject! reverse!
- replace slice! sort! uniq!).each do |method|
- module_eval <<-CODE, __FILE__, __LINE__ + 1
- def #{method}(*)
- @symbols = nil
- super
- end
- CODE
+ def each
+ @mimes.each { |x| yield x }
+ end
+
+ def <<(type)
+ @mimes << type
+ @symbols = nil
+ end
+
+ def delete_if
+ @mimes.delete_if { |x| yield x }.tap { @symbols = nil }
+ end
+
+ def symbols
+ @symbols ||= map(&:to_sym)
end
end
@@ -35,6 +43,42 @@ 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 <<-eow
+Accessing mime types via constants is deprecated. Please change:
+
+ `Mime::#{sym}`
+
+to:
+
+ `Mime[:#{ext}]`
+ eow
+ Mime[ext]
+ else
+ super
+ end
+ end
+
+ def const_defined?(sym, inherit = true)
+ ext = sym.downcase
+ if Mime[ext]
+ ActiveSupport::Deprecation.warn <<-eow
+Accessing mime types via constants is deprecated. Please change:
+
+ `Mime.const_defined?(#{sym})`
+
+to:
+
+ `Mime[:#{ext}]`
+ eow
+ true
+ else
+ super
+ end
+ end
end
# Encapsulates the notion of a mime type. Can be used at render time, for example, with:
@@ -45,15 +89,12 @@ module Mime
#
# respond_to do |format|
# format.html
- # format.ics { render text: post.to_ics, mime_type: Mime::Type["text/calendar"] }
- # format.xml { render xml: @people }
+ # format.ics { render body: @post.to_ics, mime_type: Mime::Type.lookup("text/calendar") }
+ # format.xml { render xml: @post }
# end
# end
# end
class Type
- @@html_types = Set.new [:html, :all]
- cattr_reader :html_types
-
attr_reader :symbol
@register_callbacks = []
@@ -66,7 +107,7 @@ module Mime
def initialize(index, name, q = nil)
@index = index
@name = name
- q ||= 0.0 if @name == Mime::ALL.to_s # default wildcard match to end of list
+ q ||= 0.0 if @name == '*/*'.freeze # default wildcard match to end of list
@q = ((q || 1.0).to_f * 100).to_i
end
@@ -91,7 +132,7 @@ module Mime
exchange_xml_items if app_xml_idx > text_xml_idx # make sure app_xml is ahead of text_xml in the list
delete_at(text_xml_idx) # delete text_xml from the list
elsif text_xml_idx
- text_xml.name = Mime::XML.to_s
+ text_xml.name = Mime[:xml].to_s
end
# Look for more specific XML-based types and sort them ahead of app/xml
@@ -120,7 +161,7 @@ module Mime
end
def app_xml_idx
- @app_xml_idx ||= index(Mime::XML.to_s)
+ @app_xml_idx ||= index(Mime[:xml].to_s)
end
def text_xml
@@ -160,17 +201,17 @@ module Mime
end
def register(string, symbol, mime_type_synonyms = [], extension_synonyms = [], skip_lookup = false)
- Mime.const_set(symbol.upcase, Type.new(string, symbol, mime_type_synonyms))
+ new_mime = Type.new(string, symbol, mime_type_synonyms)
- new_mime = Mime.const_get(symbol.upcase)
SET << new_mime
- ([string] + mime_type_synonyms).each { |str| LOOKUP[str] = SET.last } unless skip_lookup
- ([symbol] + extension_synonyms).each { |ext| EXTENSION_LOOKUP[ext.to_s] = SET.last }
+ ([string] + mime_type_synonyms).each { |str| LOOKUP[str] = new_mime } unless skip_lookup
+ ([symbol] + extension_synonyms).each { |ext| EXTENSION_LOOKUP[ext.to_s] = new_mime }
@register_callbacks.each do |callback|
callback.call(new_mime)
end
+ new_mime
end
def parse(accept_header)
@@ -200,28 +241,27 @@ module Mime
parse_data_with_trailing_star($1) if accept_header =~ TRAILING_STAR_REGEXP
end
- # For an input of <tt>'text'</tt>, returns <tt>[Mime::JSON, Mime::XML, Mime::ICS,
- # Mime::HTML, Mime::CSS, Mime::CSV, Mime::JS, Mime::YAML, Mime::TEXT]</tt>.
+ # For an input of <tt>'text'</tt>, returns <tt>[Mime[:json], Mime[:xml], Mime[:ics],
+ # Mime[:html], Mime[:css], Mime[:csv], Mime[:js], Mime[:yaml], Mime[:text]</tt>.
#
- # For an input of <tt>'application'</tt>, returns <tt>[Mime::HTML, Mime::JS,
- # Mime::XML, Mime::YAML, Mime::ATOM, Mime::JSON, Mime::RSS, Mime::URL_ENCODED_FORM]</tt>.
- def parse_data_with_trailing_star(input)
- Mime::SET.select { |m| m =~ input }
+ # For an input of <tt>'application'</tt>, returns <tt>[Mime[:html], Mime[:js],
+ # Mime[:xml], Mime[:yaml], Mime[:atom], Mime[:json], Mime[:rss], Mime[:url_encoded_form]</tt>.
+ def parse_data_with_trailing_star(type)
+ Mime::SET.select { |m| m =~ type }
end
# This method is opposite of register method.
#
- # Usage:
+ # To unregister a MIME type:
#
# Mime::Type.unregister(:mobile)
def unregister(symbol)
- symbol = symbol.upcase
- mime = Mime.const_get(symbol)
- Mime.instance_eval { remove_const(symbol) }
-
- SET.delete_if { |v| v.eql?(mime) }
- LOOKUP.delete_if { |_,v| v.eql?(mime) }
- EXTENSION_LOOKUP.delete_if { |_,v| v.eql?(mime) }
+ symbol = symbol.downcase
+ if mime = Mime[symbol]
+ SET.delete_if { |v| v.eql?(mime) }
+ LOOKUP.delete_if { |_, v| v.eql?(mime) }
+ EXTENSION_LOOKUP.delete_if { |_, v| v.eql?(mime) }
+ end
end
end
@@ -243,7 +283,7 @@ module Mime
end
def ref
- to_sym || to_s
+ symbol || to_s
end
def ===(list)
@@ -255,24 +295,23 @@ module Mime
end
def ==(mime_type)
- return false if mime_type.blank?
+ return false unless mime_type
(@synonyms + [ self ]).any? do |synonym|
synonym.to_s == mime_type.to_s || synonym.to_sym == mime_type.to_sym
end
end
def =~(mime_type)
- return false if mime_type.blank?
+ return false unless mime_type
regexp = Regexp.new(Regexp.quote(mime_type.to_s))
- (@synonyms + [ self ]).any? do |synonym|
- synonym.to_s =~ regexp
- end
+ @synonyms.any? { |synonym| synonym.to_s =~ regexp } || @string =~ regexp
end
def html?
- @@html_types.include?(to_sym) || @string =~ /html/
+ symbol == :html || @string =~ /html/
end
+ def all?; false; end
private
@@ -292,6 +331,22 @@ module Mime
end
end
+ class AllType < Type
+ include Singleton
+
+ def initialize
+ super '*/*', :all
+ end
+
+ def all?; true; end
+ def html?; true; end
+ end
+
+ # ALL isn't a real MIME type, so we don't register it for lookup with the
+ # other concrete types. It's a wildcard match that we use for `respond_to`
+ # negotiation internals.
+ ALL = AllType.instance
+
class NullType
include Singleton
diff --git a/actionpack/lib/action_dispatch/http/mime_types.rb b/actionpack/lib/action_dispatch/http/mime_types.rb
index 0e4da36038..87715205d9 100644
--- a/actionpack/lib/action_dispatch/http/mime_types.rb
+++ b/actionpack/lib/action_dispatch/http/mime_types.rb
@@ -27,10 +27,7 @@ Mime::Type.register "application/x-www-form-urlencoded", :url_encoded_form
# http://www.ietf.org/rfc/rfc4627.txt
# http://www.json.org/JSONRequest.html
-Mime::Type.register "application/json", :json, %w( text/x-json application/jsonrequest )
+Mime::Type.register "application/json", :json, %w( text/x-json application/jsonrequest application/vnd.api+json )
Mime::Type.register "application/pdf", :pdf, [], %w(pdf)
Mime::Type.register "application/zip", :zip, [], %w(zip)
-
-# Create Mime::ALL but do not add it to the SET.
-Mime::ALL = Mime::Type.new("*/*", :all, [])
diff --git a/actionpack/lib/action_dispatch/http/parameter_filter.rb b/actionpack/lib/action_dispatch/http/parameter_filter.rb
index b655a54865..e826551f4b 100644
--- a/actionpack/lib/action_dispatch/http/parameter_filter.rb
+++ b/actionpack/lib/action_dispatch/http/parameter_filter.rb
@@ -30,36 +30,46 @@ module ActionDispatch
when Regexp
regexps << item
else
- strings << item.to_s
+ strings << Regexp.escape(item.to_s)
end
end
- regexps << Regexp.new(strings.join('|'), true) unless strings.empty?
- new regexps, blocks
+ deep_regexps, regexps = regexps.partition { |r| r.to_s.include?("\\.".freeze) }
+ deep_strings, strings = strings.partition { |s| s.include?("\\.".freeze) }
+
+ 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, :blocks
+ attr_reader :regexps, :deep_regexps, :blocks
- def initialize(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)
+ 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)
+ value = call(value, parents)
elsif value.is_a?(Array)
- value = value.map { |v| v.is_a?(Hash) ? call(v) : v }
+ value = value.map { |v| v.is_a?(Hash) ? call(v, parents) : v }
elsif blocks.any?
- key = key.dup
+ 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
diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb
index 20ae48d458..248ecfd676 100644
--- a/actionpack/lib/action_dispatch/http/parameters.rb
+++ b/actionpack/lib/action_dispatch/http/parameters.rb
@@ -1,35 +1,41 @@
-require 'active_support/core_ext/hash/keys'
-require 'active_support/core_ext/hash/indifferent_access'
-require 'active_support/deprecation'
-
module ActionDispatch
module Http
module Parameters
PARAMETERS_KEY = 'action_dispatch.request.path_parameters'
+ DEFAULT_PARSERS = {
+ Mime[:json] => lambda { |raw_post|
+ data = ActiveSupport::JSON.decode(raw_post)
+ data.is_a?(Hash) ? data : {:_json => data}
+ }
+ }
+
+ def self.included(klass)
+ class << klass
+ attr_accessor :parameter_parsers
+ end
+
+ klass.parameter_parsers = DEFAULT_PARSERS
+ end
# Returns both GET and POST \parameters in a single hash.
def parameters
- @env["action_dispatch.request.parameters"] ||= begin
- params = begin
- request_parameters.merge(query_parameters)
- rescue EOFError
- query_parameters.dup
- end
- params.merge!(path_parameters)
- end
+ params = get_header("action_dispatch.request.parameters")
+ return params if params
+
+ params = begin
+ request_parameters.merge(query_parameters)
+ rescue EOFError
+ query_parameters.dup
+ end
+ params.merge!(path_parameters)
+ set_header("action_dispatch.request.parameters", params)
+ params
end
alias :params :parameters
def path_parameters=(parameters) #:nodoc:
- @env.delete('action_dispatch.request.parameters')
- @env[PARAMETERS_KEY] = parameters
- end
-
- def symbolized_path_parameters
- ActiveSupport::Deprecation.warn(
- "`symbolized_path_parameters` is deprecated. Please use `path_parameters`"
- )
- path_parameters
+ delete_header('action_dispatch.request.parameters')
+ set_header PARAMETERS_KEY, parameters
end
# Returns a hash with the \parameters used to form the \path of the request.
@@ -37,31 +43,29 @@ module ActionDispatch
#
# {'action' => 'my_action', 'controller' => 'my_controller'}
def path_parameters
- @env[PARAMETERS_KEY] ||= {}
+ get_header(PARAMETERS_KEY) || {}
end
- private
+ private
- # Convert nested Hash to HashWithIndifferentAccess.
- #
- def normalize_encode_params(params)
- case params
- when Hash
- if params.has_key?(:tempfile)
- UploadedFile.new(params)
- else
- params.each_with_object({}) do |(key, val), new_hash|
- new_hash[key] = if val.is_a?(Array)
- val.map! { |el| normalize_encode_params(el) }
- else
- normalize_encode_params(val)
- end
- end.with_indifferent_access
- end
- else
- params
+ def parse_formatted_parameters(parsers)
+ return yield if content_length.zero?
+
+ strategy = parsers.fetch(content_mime_type) { return yield }
+
+ begin
+ strategy.call(raw_post)
+ rescue => e # 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}"
+
+ raise ParamsParser::ParseError.new(e.message, e)
end
end
+
+ def params_parsers
+ ActionDispatch::Request.parameter_parsers
+ end
end
end
end
diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb
index a519d6c1fc..35e3ac304f 100644
--- a/actionpack/lib/action_dispatch/http/request.rb
+++ b/actionpack/lib/action_dispatch/http/request.rb
@@ -13,12 +13,14 @@ require 'action_dispatch/http/url'
require 'active_support/core_ext/array/conversions'
module ActionDispatch
- class Request < Rack::Request
+ class Request
+ include Rack::Request::Helpers
include ActionDispatch::Http::Cache::Request
include ActionDispatch::Http::MimeNegotiation
include ActionDispatch::Http::Parameters
include ActionDispatch::Http::FilterParameters
include ActionDispatch::Http::URL
+ include Rack::Request::Env
autoload :Session, 'action_dispatch/request/session'
autoload :Utils, 'action_dispatch/request/utils'
@@ -29,15 +31,20 @@ module ActionDispatch
PATH_TRANSLATED REMOTE_HOST
REMOTE_IDENT REMOTE_USER REMOTE_ADDR
SERVER_NAME SERVER_PROTOCOL
+ ORIGINAL_SCRIPT_NAME
HTTP_ACCEPT HTTP_ACCEPT_CHARSET HTTP_ACCEPT_ENCODING
HTTP_ACCEPT_LANGUAGE HTTP_CACHE_CONTROL HTTP_FROM
- HTTP_NEGOTIATE HTTP_PRAGMA ].freeze
+ HTTP_NEGOTIATE HTTP_PRAGMA HTTP_CLIENT_IP
+ HTTP_X_FORWARDED_FOR HTTP_VERSION
+ HTTP_X_REQUEST_ID HTTP_X_FORWARDED_HOST
+ SERVER_ADDR
+ ].freeze
ENV_METHODS.each do |env|
class_eval <<-METHOD, __FILE__, __LINE__ + 1
def #{env.sub(/^HTTP_/n, '').downcase} # def accept_charset
- @env["#{env}"] # @env["HTTP_ACCEPT_CHARSET"]
+ get_header "#{env}".freeze # get_header "HTTP_ACCEPT_CHARSET".freeze
end # end
METHOD
end
@@ -50,7 +57,6 @@ module ActionDispatch
@original_fullpath = nil
@fullpath = nil
@ip = nil
- @uuid = nil
end
def check_path_parameters!
@@ -59,13 +65,32 @@ module ActionDispatch
path_parameters.each do |key, value|
next unless value.respond_to?(:valid_encoding?)
unless value.valid_encoding?
- raise ActionController::BadRequest, "Invalid parameter: #{key} => #{value}"
+ 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 controller_class
+ check_path_parameters!
+ params = path_parameters
+
+ if params.key?(:controller)
+ controller_param = params[:controller].underscore
+ params[:action] ||= 'index'
+ const_name = "#{controller_param.camelize}Controller"
+ ActiveSupport::Dependencies.constantize(const_name)
+ else
+ PASS_NOT_FOUND
+ end
+ end
+
def key?(key)
- @env.key?(key)
+ has_header? key
end
# List of HTTP request methods from the following RFCs:
@@ -102,67 +127,72 @@ module ActionDispatch
# the application should use), this \method returns the overridden
# value, not the original.
def request_method
- @request_method ||= check_method(env["REQUEST_METHOD"])
+ @request_method ||= check_method(super)
end
- # Returns a symbol form of the #request_method
- def request_method_symbol
- HTTP_METHOD_LOOKUP[request_method]
+ def routes # :nodoc:
+ get_header("action_dispatch.routes".freeze)
end
- # Returns the original value of the environment's REQUEST_METHOD,
- # even if it was overridden by middleware. See #request_method for
- # more information.
- def method
- @method ||= check_method(env["rack.methodoverride.original_method"] || env['REQUEST_METHOD'])
+ def routes=(routes) # :nodoc:
+ set_header("action_dispatch.routes".freeze, routes)
end
- # Returns a symbol form of the #method
- def method_symbol
- HTTP_METHOD_LOOKUP[method]
+ def engine_script_name(_routes) # :nodoc:
+ get_header(_routes.env_key)
+ end
+
+ def engine_script_name=(name) # :nodoc:
+ set_header(routes.env_key, name.dup)
+ end
+
+ def request_method=(request_method) #:nodoc:
+ if check_method(request_method)
+ @request_method = set_header("REQUEST_METHOD", request_method)
+ end
+ end
+
+ def controller_instance # :nodoc:
+ get_header('action_controller.instance'.freeze)
end
- # Is this a GET (or HEAD) request?
- # Equivalent to <tt>request.request_method_symbol == :get</tt>.
- def get?
- HTTP_METHOD_LOOKUP[request_method] == :get
+ def controller_instance=(controller) # :nodoc:
+ set_header('action_controller.instance'.freeze, controller)
end
- # Is this a POST request?
- # Equivalent to <tt>request.request_method_symbol == :post</tt>.
- def post?
- HTTP_METHOD_LOOKUP[request_method] == :post
+ def http_auth_salt
+ get_header "action_dispatch.http_auth_salt"
end
- # Is this a PATCH request?
- # Equivalent to <tt>request.request_method == :patch</tt>.
- def patch?
- HTTP_METHOD_LOOKUP[request_method] == :patch
+ def show_exceptions? # :nodoc:
+ # We're treating `nil` as "unset", and we want the default setting to be
+ # `true`. This logic should be extracted to `env_config` and calculated
+ # once.
+ !(get_header('action_dispatch.show_exceptions'.freeze) == false)
end
- # Is this a PUT request?
- # Equivalent to <tt>request.request_method_symbol == :put</tt>.
- def put?
- HTTP_METHOD_LOOKUP[request_method] == :put
+ # Returns a symbol form of the #request_method
+ def request_method_symbol
+ HTTP_METHOD_LOOKUP[request_method]
end
- # Is this a DELETE request?
- # Equivalent to <tt>request.request_method_symbol == :delete</tt>.
- def delete?
- HTTP_METHOD_LOOKUP[request_method] == :delete
+ # Returns the original value of the environment's REQUEST_METHOD,
+ # 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'))
end
- # Is this a HEAD request?
- # Equivalent to <tt>request.request_method_symbol == :head</tt>.
- def head?
- HTTP_METHOD_LOOKUP[request_method] == :head
+ # Returns a symbol form of the #method
+ def method_symbol
+ HTTP_METHOD_LOOKUP[method]
end
# Provides access to the request's HTTP headers, for example:
#
# request.headers["Content-Type"] # => "text/plain"
def headers
- Http::Headers.new(@env)
+ @headers ||= Http::Headers.new(self)
end
# Returns a +String+ with the last requested path including their params.
@@ -173,7 +203,7 @@ module ActionDispatch
# # get '/foo?bar'
# request.original_fullpath # => '/foo?bar'
def original_fullpath
- @original_fullpath ||= (env["ORIGINAL_FULLPATH"] || fullpath)
+ @original_fullpath ||= (get_header("ORIGINAL_FULLPATH") || fullpath)
end
# Returns the +String+ full path including params of the last URL requested.
@@ -212,62 +242,78 @@ module ActionDispatch
# (case-insensitive), which may need to be manually added depending on the
# choice of JavaScript libraries and frameworks.
def xml_http_request?
- @env['HTTP_X_REQUESTED_WITH'] =~ /XMLHttpRequest/i
+ get_header('HTTP_X_REQUESTED_WITH') =~ /XMLHttpRequest/i
end
alias :xhr? :xml_http_request?
+ # Returns the IP address of client as a +String+.
def ip
@ip ||= super
end
- # Originating IP address, usually set by the RemoteIp middleware.
+ # Returns the IP address of client as a +String+,
+ # usually set by the RemoteIp middleware.
def remote_ip
- @remote_ip ||= (@env["action_dispatch.remote_ip"] || ip).to_s
+ @remote_ip ||= (get_header("action_dispatch.remote_ip") || ip).to_s
end
- # Returns the unique request id, which is based off either the X-Request-Id header that can
+ def remote_ip=(remote_ip)
+ set_header "action_dispatch.remote_ip".freeze, remote_ip
+ end
+
+ ACTION_DISPATCH_REQUEST_ID = "action_dispatch.request_id".freeze # :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.
- def uuid
- @uuid ||= env["action_dispatch.request_id"]
+ def request_id
+ get_header ACTION_DISPATCH_REQUEST_ID
+ end
+
+ def request_id=(id) # :nodoc:
+ set_header ACTION_DISPATCH_REQUEST_ID, id
end
+ alias_method :uuid, :request_id
+
# Returns the lowercase name of the HTTP server software.
def server_software
- (@env['SERVER_SOFTWARE'] && /^([a-zA-Z]+)/ =~ @env['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 @env.include? 'RAW_POST_DATA'
+ unless has_header? 'RAW_POST_DATA'
raw_post_body = body
- @env['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
- @env['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 = @env['RAW_POST_DATA']
+ if raw_post = get_header('RAW_POST_DATA')
raw_post.force_encoding(Encoding::BINARY)
StringIO.new(raw_post)
else
- @env['rack.input']
+ body_stream
end
end
+ # Returns true if the request's content MIME type is
+ # +application/x-www-form-urlencoded+ or +multipart/form-data+.
def form_data?
FORM_DATA_MEDIA_TYPES.include?(content_mime_type.to_s)
end
def body_stream #:nodoc:
- @env['rack.input']
+ get_header('rack.input')
end
# TODO This should be broken apart into AD::Request::Session and probably
@@ -278,64 +324,75 @@ module ActionDispatch
else
self.session = {}
end
- @env['action_dispatch.request.flash_hash'] = nil
+ self.flash = nil
end
def session=(session) #:nodoc:
- Session.set @env, session
+ Session.set self, session
end
def session_options=(options)
- Session::Options.set @env, options
+ Session::Options.set self, options
end
# Override Rack's GET method to support indifferent access
def GET
- @env["action_dispatch.request.query_parameters"] ||= Utils.deep_munge(normalize_encode_params(super || {}))
- rescue TypeError => e
- raise ActionController::BadRequest.new(:query, e)
+ fetch_header("action_dispatch.request.query_parameters") do |k|
+ rack_query_params = super || {}
+ # Check for non UTF-8 parameter values, which would cause errors later
+ Request::Utils.check_param_encoding(rack_query_params)
+ set_header k, Request::Utils.normalize_encode_params(rack_query_params)
+ end
+ rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e
+ raise ActionController::BadRequest.new("Invalid query parameters: #{e.message}", e)
end
alias :query_parameters :GET
# Override Rack's POST method to support indifferent access
def POST
- @env["action_dispatch.request.request_parameters"] ||= Utils.deep_munge(normalize_encode_params(super || {}))
- rescue TypeError => e
- raise ActionController::BadRequest.new(:request, e)
+ fetch_header("action_dispatch.request.request_parameters") do
+ pr = parse_formatted_parameters(params_parsers) do |params|
+ super || {}
+ end
+ self.request_parameters = Request::Utils.normalize_encode_params(pr)
+ end
+ rescue 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}", e)
end
alias :request_parameters :POST
# Returns the authorization header regardless of whether it was specified directly or through one of the
# proxy alternatives.
def authorization
- @env['HTTP_AUTHORIZATION'] ||
- @env['X-HTTP_AUTHORIZATION'] ||
- @env['X_HTTP_AUTHORIZATION'] ||
- @env['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.
+ # True if the request came from localhost, 127.0.0.1, or ::1.
def local?
LOCALHOST =~ remote_addr && LOCALHOST =~ remote_ip
end
- # Extracted into ActionDispatch::Request::Utils.deep_munge, but kept here for backwards compatibility.
- def deep_munge(hash)
- ActiveSupport::Deprecation.warn(
- "This method has been extracted into ActionDispatch::Request::Utils.deep_munge. Please start using that instead."
- )
+ def request_parameters=(params)
+ raise if params.nil?
+ set_header("action_dispatch.request.request_parameters".freeze, params)
+ end
- Utils.deep_munge(hash)
+ def logger
+ get_header("action_dispatch.logger".freeze)
end
- protected
- def parse_query(qs)
- Utils.deep_munge(super)
- end
+ def commit_flash
+ end
private
def check_method(name)
- HTTP_METHOD_LOOKUP[name] || raise(ActionController::UnknownHttpMethod, "#{name}, accepted HTTP methods are #{HTTP_METHODS.to_sentence(:locale => :en)}")
+ HTTP_METHOD_LOOKUP[name] || raise(ActionController::UnknownHttpMethod, "#{name}, accepted HTTP methods are #{HTTP_METHODS[0...-1].join(', ')}, and #{HTTP_METHODS[-1]}")
name
end
end
diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb
index 2fab6be1a5..f0127aa276 100644
--- a/actionpack/lib/action_dispatch/http/response.rb
+++ b/actionpack/lib/action_dispatch/http/response.rb
@@ -32,46 +32,56 @@ module ActionDispatch # :nodoc:
# end
# end
class Response
+ class Header < DelegateClass(Hash) # :nodoc:
+ def initialize(response, header)
+ @response = response
+ super(header)
+ end
+
+ def []=(k,v)
+ if @response.sending? || @response.sent?
+ raise ActionDispatch::IllegalStateError, 'header already sent'
+ end
+
+ super
+ end
+
+ def merge(other)
+ self.class.new @response, __getobj__.merge(other)
+ end
+
+ def to_hash
+ __getobj__.dup
+ end
+ end
+
# The request that the response is responding to.
attr_accessor :request
# The HTTP status code.
attr_reader :status
- attr_writer :sending_file
-
- # Get and set headers for this response.
- attr_accessor :header
+ # Get headers for this response.
+ attr_reader :header
- alias_method :headers=, :header=
alias_method :headers, :header
delegate :[], :[]=, :to => :@header
delegate :each, :to => :@stream
- # Sets the HTTP response's content MIME type. For example, in the controller
- # you could write this:
- #
- # response.content_type = "text/plain"
- #
- # If a character set has been defined for this response (see charset=) then
- # the character set information will also be included in the content type
- # information.
- attr_reader :content_type
-
- # 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.
- attr_accessor :charset
-
CONTENT_TYPE = "Content-Type".freeze
SET_COOKIE = "Set-Cookie".freeze
LOCATION = "Location".freeze
- NO_CONTENT_CODES = [204, 304]
+ NO_CONTENT_CODES = [100, 101, 102, 204, 205, 304]
cattr_accessor(:default_charset) { "utf-8" }
cattr_accessor(:default_headers)
include Rack::Response::Helpers
+ # Aliasing these off because AD::Http::Cache::Response defines them
+ alias :_cache_control :cache_control
+ alias :_cache_control= :cache_control=
+
include ActionDispatch::Http::FilterRedirect
include ActionDispatch::Http::Cache::Response
include MonitorMixin
@@ -81,11 +91,21 @@ module ActionDispatch # :nodoc:
@response = response
@buf = buf
@closed = false
+ @str_body = nil
+ end
+
+ def body
+ @str_body ||= begin
+ buf = ''
+ each { |chunk| buf << chunk }
+ buf
+ end
end
def write(string)
raise IOError, "closed stream" if closed?
+ @str_body = nil
@response.commit!
@buf.push string
end
@@ -110,36 +130,40 @@ module ActionDispatch # :nodoc:
end
end
+ def self.create(status = 200, header = {}, body = [], default_headers: self.default_headers)
+ header = merge_default_headers(header, default_headers)
+ new status, header, body
+ end
+
+ def self.merge_default_headers(original, default)
+ default.respond_to?(:merge) ? default.merge(original) : original
+ end
+
# The underlying body, as a streamable object.
attr_reader :stream
def initialize(status = 200, header = {}, body = [])
super()
- header = merge_default_headers(header, self.class.default_headers)
+ @header = Header.new(self, header)
- self.body, self.header, self.status = body, header, status
+ self.body, self.status = body, status
- @sending_file = false
- @blank = false
@cv = new_cond
@committed = false
@sending = false
@sent = false
- @content_type = nil
- @charset = nil
-
- if content_type = self[CONTENT_TYPE]
- type, charset = content_type.split(/;\s*charset=/)
- @content_type = Mime::Type.lookup(type)
- @charset = charset || self.class.default_charset
- end
prepare_cache_control!
yield self if block_given?
end
+ def has_header?(key); headers.key? key; end
+ def get_header(key); headers[key]; end
+ def set_header(key, v); headers[key] = v; end
+ def delete_header(key); headers.delete key; end
+
def await_commit
synchronize do
@cv.wait_until { @committed }
@@ -184,7 +208,49 @@ module ActionDispatch # :nodoc:
# Sets the HTTP content type.
def content_type=(content_type)
- @content_type = content_type.to_s
+ 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:
+ #
+ # response.content_type = "text/plain"
+ #
+ # If a character set has been defined for this response (see charset=) then
+ # the character set information will also be included in the content type
+ # information.
+
+ def content_type
+ parse_content_type.mime_type
+ end
+
+ def sending_file=(v)
+ if true == v
+ self.charset = false
+ end
+ end
+
+ # Sets the HTTP character set. In case of nil parameter
+ # it sets the charset to utf-8.
+ #
+ # response.charset = 'utf-16' # => 'utf-16'
+ # response.charset = nil # => 'utf-8'
+ def charset=(charset)
+ header_info = parse_content_type
+ if false == charset
+ set_header CONTENT_TYPE, header_info.mime_type
+ else
+ content_type = header_info.mime_type
+ set_content_type content_type, charset || self.class.default_charset
+ end
+ end
+
+ # The charset of the response. HTML wants to know the encoding of the
+ # content you're giving them, so we need to send that along.
+ def charset
+ header_info = parse_content_type
+ header_info.charset || self.class.default_charset
end
# The response code of the request.
@@ -213,17 +279,15 @@ module ActionDispatch # :nodoc:
# Returns the content of the response as a string. This contains the contents
# of any calls to <tt>render</tt>.
def body
- strings = []
- each { |part| strings << part.to_s }
- strings.join
+ @stream.body
end
- EMPTY = " "
+ def write(string)
+ @stream.write string
+ end
# Allows you to manually set or override the response body.
def body=(body)
- @blank = true if body == EMPTY
-
if body.respond_to?(:to_path)
@stream = body
else
@@ -233,31 +297,49 @@ module ActionDispatch # :nodoc:
end
end
- def body_parts
- parts = []
- @stream.each { |x| parts << x }
- parts
- end
+ # Avoid having to pass an open file handle as the response body.
+ # Rack::Sendfile will usually intercept the response and uses
+ # the path directly, so there is no reason to open the file.
+ class FileBody #:nodoc:
+ attr_reader :to_path
+
+ def initialize(path)
+ @to_path = path
+ end
+
+ def body
+ File.binread(to_path)
+ end
- def set_cookie(key, value)
- ::Rack::Utils.set_cookie_header!(header, key, value)
+ # Stream the file's contents if Rack::Sendfile isn't present.
+ def each
+ File.open(to_path, 'rb') do |file|
+ while chunk = file.read(16384)
+ yield chunk
+ end
+ end
+ end
end
- def delete_cookie(key, value={})
- ::Rack::Utils.delete_cookie_header!(header, key, value)
+ # Send the file stored at +path+ as the response body.
+ def send_file(path)
+ commit!
+ @stream = FileBody.new(path)
end
- # The location header we'll be responding with.
- def location
- headers[LOCATION]
+ def reset_body!
+ @stream = build_buffer(self, [])
end
- alias_method :redirect_url, :location
- # Sets the location header we'll be responding with.
- def location=(url)
- headers[LOCATION] = url
+ def body_parts
+ parts = []
+ @stream.each { |x| parts << x }
+ parts
end
+ # The location header we'll be responding with.
+ alias_method :redirect_url, :location
+
def close
stream.close if stream.respond_to?(:close)
end
@@ -274,19 +356,21 @@ module ActionDispatch # :nodoc:
end
# Turns the Response into a Rack-compatible array of the status, headers,
- # and body.
+ # and body. Allows explicit splatting:
+ #
+ # status, headers, body = *response
def to_a
+ commit!
rack_response @status, @header.to_hash
end
alias prepare! to_a
- alias to_ary to_a
# Returns the response cookies, converted to a Hash of (name => value) pairs
#
# assert_equal 'AuthorOfNewPage', r.cookies['author']
def cookies
cookies = {}
- if header = self[SET_COOKIE]
+ 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
@@ -298,21 +382,36 @@ module ActionDispatch # :nodoc:
cookies
end
- def _status_code
- @status
- end
private
- def before_committed
+ 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)
+ else
+ NullContentTypeHeader
+ end
end
- def before_sending
+ def set_content_type(content_type, charset)
+ type = (content_type || '').dup
+ type << "; charset=#{charset}" if charset
+ set_header CONTENT_TYPE, type
end
- def merge_default_headers(original, default)
- return original unless default.respond_to?(:merge)
+ def before_committed
+ return if committed?
+ assign_default_content_type_and_charset!
+ handle_conditional_get!
+ handle_no_content!
+ end
- default.merge(original)
+ def before_sending
end
def build_buffer(response, body)
@@ -323,20 +422,12 @@ module ActionDispatch # :nodoc:
body.respond_to?(:each) ? body : [body]
end
- def assign_default_content_type_and_charset!(headers)
- return if headers[CONTENT_TYPE].present?
-
- @content_type ||= Mime::HTML
- @charset ||= self.class.default_charset unless @charset == false
+ def assign_default_content_type_and_charset!
+ return if content_type
- type = @content_type.to_s.dup
- type << "; charset=#{@charset}" if append_charset?
-
- headers[CONTENT_TYPE] = type
- end
-
- def append_charset?
- !@sending_file && @charset != false
+ ct = parse_content_type
+ set_content_type(ct.mime_type || Mime[:html].to_s,
+ ct.charset || self.class.default_charset)
end
class RackBody
@@ -369,16 +460,21 @@ module ActionDispatch # :nodoc:
def to_path
@response.stream.to_path
end
- end
-
- def rack_response(status, header)
- assign_default_content_type_and_charset!(header)
- handle_conditional_get!
- header[SET_COOKIE] = header[SET_COOKIE].join("\n") if header[SET_COOKIE].respond_to?(:join)
+ def to_ary
+ nil
+ end
+ end
+ def handle_no_content!
if NO_CONTENT_CODES.include?(@status)
- header.delete CONTENT_TYPE
+ @header.delete CONTENT_TYPE
+ @header.delete 'Content-Length'
+ end
+ end
+
+ def rack_response(status, header)
+ if NO_CONTENT_CODES.include?(status)
[status, header, []]
else
[status, header, RackBody.new(self)]
diff --git a/actionpack/lib/action_dispatch/http/upload.rb b/actionpack/lib/action_dispatch/http/upload.rb
index 540e11a4a0..a221f4c5af 100644
--- a/actionpack/lib/action_dispatch/http/upload.rb
+++ b/actionpack/lib/action_dispatch/http/upload.rb
@@ -28,7 +28,13 @@ module ActionDispatch
raise(ArgumentError, ':tempfile is required') unless @tempfile
@original_filename = hash[:filename]
- @original_filename &&= @original_filename.encode "UTF-8"
+ if @original_filename
+ begin
+ @original_filename.encode!(Encoding::UTF_8)
+ rescue EncodingError
+ @original_filename.force_encoding(Encoding::UTF_8)
+ end
+ end
@content_type = hash[:type]
@headers = hash[:head]
end
diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb
index 473f692b05..92b10b6d3b 100644
--- a/actionpack/lib/action_dispatch/http/url.rb
+++ b/actionpack/lib/action_dispatch/http/url.rb
@@ -1,21 +1,32 @@
require 'active_support/core_ext/module/attribute_accessors'
-require 'active_support/core_ext/hash/slice'
module ActionDispatch
module Http
module URL
IP_HOST_REGEXP = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/
- HOST_REGEXP = /(^[^:]+:\/\/)?([^:]+)(?::(\d+$))?/
+ HOST_REGEXP = /(^[^:]+:\/\/)?(\[[^\]]+\]|[^:]+)(?::(\d+$))?/
PROTOCOL_REGEXP = /^([^:]+)(:)?(\/\/)?$/
mattr_accessor :tld_length
self.tld_length = 1
class << self
+ # Returns the domain part of a host given the domain level.
+ #
+ # # Top-level domain example
+ # extract_domain('www.example.com', 1) # => "example.com"
+ # # Second-level domain example
+ # extract_domain('dev.www.example.co.uk', 2) # => "example.co.uk"
def extract_domain(host, tld_length)
extract_domain_from(host, tld_length) if named_host?(host)
end
+ # Returns the subdomains of a host as an Array given the domain level.
+ #
+ # # Top-level domain example
+ # extract_subdomains('www.example.com', 1) # => ["www"]
+ # # Second-level domain example
+ # extract_subdomains('dev.www.example.co.uk', 2) # => ["dev", "www"]
def extract_subdomains(host, tld_length)
if named_host?(host)
extract_subdomains_from(host, tld_length)
@@ -24,6 +35,12 @@ module ActionDispatch
end
end
+ # Returns the subdomains of a host as a String given the domain level.
+ #
+ # # Top-level domain example
+ # extract_subdomain('www.example.com', 1) # => "www"
+ # # Second-level domain example
+ # extract_subdomain('dev.www.example.co.uk', 2) # => "dev.www"
def extract_subdomain(host, tld_length)
extract_subdomains(host, tld_length).join('.')
end
@@ -49,31 +66,28 @@ module ActionDispatch
end
def path_for(options)
- result = options[:script_name].to_s.chomp("/")
- result << options[:path].to_s
+ path = options[:script_name].to_s.chomp("/".freeze)
+ path << options[:path] if options.key?(:path)
- result = add_trailing_slash(result) if options[:trailing_slash]
+ add_trailing_slash(path) if options[:trailing_slash]
+ add_params(path, options[:params]) if options.key?(:params)
+ add_anchor(path, options[:anchor]) if options.key?(:anchor)
- result = add_params options, result
- add_anchor options, result
+ path
end
private
- def add_params(options, result)
- if options.key? :params
- param = options[:params]
- params = param.is_a?(Hash) ? param : { params: param }
-
- params.reject! { |_,v| v.to_param.nil? }
- result << "?#{params.to_query}" unless params.empty?
- end
- result
+ def add_params(path, params)
+ params = { params: params } unless params.is_a?(Hash)
+ params.reject! { |_,v| v.to_param.nil? }
+ path << "?#{params.to_query}" unless params.empty?
end
- def add_anchor(options, result)
- result << "##{Journey::Router::Utils.escape_fragment(options[:anchor].to_param.to_s)}" if options[:anchor]
- result
+ def add_anchor(path, anchor)
+ if anchor
+ path << "##{Journey::Router::Utils.escape_fragment(anchor.to_param)}"
+ end
end
def extract_domain_from(host, tld_length)
@@ -93,19 +107,17 @@ module ActionDispatch
elsif !path.include?(".")
path.sub!(/[^\/]\z|\A\z/, '\&/')
end
-
- path
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
+ protocol ||= match[1] unless protocol == false
+ host = match[2]
+ port = match[3] unless options.key? :port
end
- protocol = normalize_protocol protocol
- host = normalize_host(host, options)
+ protocol = normalize_protocol protocol
+ host = normalize_host(host, options)
result = protocol.dup
@@ -171,43 +183,97 @@ module ActionDispatch
end
end
- def initialize(env)
+ def initialize
super
@protocol = nil
@port = nil
end
# 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.url # => "http://example.com"
def url
protocol + host_with_port + fullpath
end
# 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.protocol # => "http://"
+ #
+ # req = Request.new 'HTTP_HOST' => 'example.com', 'HTTPS' => 'on'
+ # req.protocol # => "https://"
def protocol
@protocol ||= ssl? ? 'https://' : 'http://'
end
# Returns the \host for this request, such as "example.com".
+ #
+ # class Request < Rack::Request
+ # include ActionDispatch::Http::URL
+ # end
+ #
+ # req = Request.new 'HTTP_HOST' => 'example.com'
+ # req.raw_host_with_port # => "example.com"
+ #
+ # req = Request.new 'HTTP_HOST' => 'example.com:8080'
+ # req.raw_host_with_port # => "example.com:8080"
def raw_host_with_port
- if forwarded = env["HTTP_X_FORWARDED_HOST"]
+ if forwarded = x_forwarded_host.presence
forwarded.split(/,\s?/).last
else
- env['HTTP_HOST'] || "#{env['SERVER_NAME'] || env['SERVER_ADDR']}:#{env['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
+ #
+ # req = Request.new 'HTTP_HOST' => 'example.com:8080'
+ # req.host # => "example.com"
def host
- raw_host_with_port.sub(/:\d+$/, '')
+ raw_host_with_port.sub(/:\d+$/, ''.freeze)
end
# Returns a \host:\port string for this request, such as "example.com" or
# "example.com:8080".
+ #
+ # class Request < Rack::Request
+ # include ActionDispatch::Http::URL
+ # end
+ #
+ # req = Request.new 'HTTP_HOST' => 'example.com:80'
+ # req.host_with_port # => "example.com"
+ #
+ # req = Request.new 'HTTP_HOST' => 'example.com:8080'
+ # req.host_with_port # => "example.com:8080"
def host_with_port
"#{host}#{port_string}"
end
# 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.port # => 80
+ #
+ # req = Request.new 'HTTP_HOST' => 'example.com:8080'
+ # req.port # => 8080
def port
@port ||= begin
if raw_host_with_port =~ /:(\d+)$/
@@ -219,6 +285,13 @@ module ActionDispatch
end
# 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.standard_port # => 80
def standard_port
case protocol
when 'https://' then 443
@@ -227,24 +300,54 @@ module ActionDispatch
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.standard_port? # => true
+ #
+ # req = Request.new 'HTTP_HOST' => 'example.com:8080'
+ # req.standard_port? # => false
def standard_port?
port == standard_port
end
# Returns a number \port suffix like 8080 if the \port number of this request
# is not the default HTTP \port 80 or HTTPS \port 443.
+ #
+ # class Request < Rack::Request
+ # include ActionDispatch::Http::URL
+ # end
+ #
+ # req = Request.new 'HTTP_HOST' => 'example.com:80'
+ # req.optional_port # => nil
+ #
+ # req = Request.new 'HTTP_HOST' => 'example.com:8080'
+ # req.optional_port # => 8080
def optional_port
standard_port? ? nil : port
end
# Returns a string \port suffix, including colon, like ":8080" if the \port
# number of this request is not the default HTTP \port 80 or HTTPS \port 443.
+ #
+ # class Request < Rack::Request
+ # include ActionDispatch::Http::URL
+ # end
+ #
+ # req = Request.new 'HTTP_HOST' => 'example.com:80'
+ # req.port_string # => ""
+ #
+ # req = Request.new 'HTTP_HOST' => 'example.com:8080'
+ # req.port_string # => ":8080"
def port_string
standard_port? ? '' : ":#{port}"
end
def server_port
- @env['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/formatter.rb b/actionpack/lib/action_dispatch/journey/formatter.rb
index 59b353b1b7..0323360faa 100644
--- a/actionpack/lib/action_dispatch/journey/formatter.rb
+++ b/actionpack/lib/action_dispatch/journey/formatter.rb
@@ -14,7 +14,7 @@ module ActionDispatch
def generate(name, options, path_parameters, parameterize = nil)
constraints = path_parameters.merge(options)
- missing_keys = []
+ missing_keys = nil # need for variable scope
match_route(name, constraints) do |route|
parameterized_parts = extract_parameterized_parts(route, options, path_parameters, parameterize)
@@ -25,22 +25,22 @@ module ActionDispatch
next unless name || route.dispatcher?
missing_keys = missing_keys(route, parameterized_parts)
- next unless missing_keys.empty?
+ next if missing_keys && !missing_keys.empty?
params = options.dup.delete_if do |key, _|
parameterized_parts.key?(key) || route.defaults.key?(key)
end
defaults = route.defaults
required_parts = route.required_parts
- parameterized_parts.delete_if do |key, value|
- value.to_s == defaults[key].to_s && !required_parts.include?(key)
+ parameterized_parts.keep_if do |key, value|
+ (defaults[key].nil? && value.present?) || value.to_s != defaults[key].to_s || required_parts.include?(key)
end
return [route.format(parameterized_parts), params]
end
- message = "No route matches #{Hash[constraints.sort].inspect}"
- message << " missing required keys: #{missing_keys.sort.inspect}" if name
+ 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?
raise ActionController::UrlGenerationError, message
end
@@ -54,12 +54,12 @@ module ActionDispatch
def extract_parameterized_parts(route, options, recall, parameterize = nil)
parameterized_parts = recall.merge(options)
- keys_to_keep = route.parts.reverse.drop_while { |part|
+ keys_to_keep = route.parts.reverse_each.drop_while { |part|
!options.key?(part) || (options[part] || recall[part]).nil?
} | route.required_parts
- (parameterized_parts.keys - keys_to_keep).each do |bad_key|
- parameterized_parts.delete(bad_key)
+ parameterized_parts.delete_if do |bad_key, _|
+ !keys_to_keep.include?(bad_key)
end
if parameterize
@@ -110,15 +110,36 @@ module ActionDispatch
routes
end
+ module RegexCaseComparator
+ DEFAULT_INPUT = /[-_.a-zA-Z0-9]+\/[-_.a-zA-Z0-9]+/
+ DEFAULT_REGEX = /\A#{DEFAULT_INPUT}\Z/
+
+ def self.===(regex)
+ DEFAULT_INPUT == regex
+ end
+ end
+
# Returns an array populated with missing keys if any are present.
def missing_keys(route, parts)
- missing_keys = []
+ missing_keys = nil
tests = route.path.requirements
route.required_parts.each { |key|
- if tests.key?(key)
- missing_keys << key unless /\A#{tests[key]}\Z/ === parts[key]
+ case tests[key]
+ when nil
+ unless parts[key]
+ missing_keys ||= []
+ missing_keys << key
+ end
+ when RegexCaseComparator
+ unless RegexCaseComparator::DEFAULT_REGEX === parts[key]
+ missing_keys ||= []
+ missing_keys << key
+ end
else
- missing_keys << key unless parts[key]
+ unless /\A#{tests[key]}\Z/ === parts[key]
+ missing_keys ||= []
+ missing_keys << key
+ end
end
}
missing_keys
@@ -134,7 +155,7 @@ module ActionDispatch
def build_cache
root = { ___routes: [] }
- routes.each_with_index do |route, i|
+ routes.routes.each_with_index do |route, i|
leaf = route.required_defaults.inject(root) do |h, tuple|
h[tuple] ||= {}
end
diff --git a/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb b/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb
index 990d2127ee..d7ce6042c2 100644
--- a/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb
+++ b/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb
@@ -88,13 +88,13 @@ module ActionDispatch
erb = File.read File.join(viz_dir, 'index.html.erb')
states = "function tt() { return #{to_json}; }"
- fun_routes = paths.shuffle.first(3).map do |ast|
+ fun_routes = paths.sample(3).map do |ast|
ast.map { |n|
case n
when Nodes::Symbol
case n.left
when ':id' then rand(100).to_s
- when ':format' then %w{ xml json }.shuffle.first
+ when ':format' then %w{ xml json }.sample
else
'omg'
end
@@ -109,7 +109,7 @@ module ActionDispatch
svg = to_svg
javascripts = [states, fsm_js]
- # Annoying hack for 1.9 warnings
+ # Annoying hack warnings
fun_routes = fun_routes
stylesheets = stylesheets
svg = svg
diff --git a/actionpack/lib/action_dispatch/journey/nfa/dot.rb b/actionpack/lib/action_dispatch/journey/nfa/dot.rb
index 47bf76bdbf..7063b44bb5 100644
--- a/actionpack/lib/action_dispatch/journey/nfa/dot.rb
+++ b/actionpack/lib/action_dispatch/journey/nfa/dot.rb
@@ -1,5 +1,3 @@
-# encoding: utf-8
-
module ActionDispatch
module Journey # :nodoc:
module NFA # :nodoc:
diff --git a/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb b/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb
index 66e414213a..0ccab21801 100644
--- a/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb
+++ b/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb
@@ -45,51 +45,6 @@ module ActionDispatch
(@table.keys + @table.values.flat_map(&:keys)).uniq
end
- # Returns a generalized transition graph with reduced states. The states
- # are reduced like a DFA, but the table must be simulated like an NFA.
- #
- # Edges of the GTG are regular expressions.
- def generalized_table
- gt = GTG::TransitionTable.new
- marked = {}
- state_id = Hash.new { |h,k| h[k] = h.length }
- alphabet = self.alphabet
-
- stack = [eclosure(0)]
-
- until stack.empty?
- state = stack.pop
- next if marked[state] || state.empty?
-
- marked[state] = true
-
- alphabet.each do |alpha|
- next_state = eclosure(following_states(state, alpha))
- next if next_state.empty?
-
- gt[state_id[state], state_id[next_state]] = alpha
- stack << next_state
- end
- end
-
- final_groups = state_id.keys.find_all { |s|
- s.sort.last == accepting
- }
-
- final_groups.each do |states|
- id = state_id[states]
-
- gt.add_accepting(id)
- save = states.find { |s|
- @memos.key?(s) && eclosure(s).sort.last == accepting
- }
-
- gt.add_memo(id, memo(save))
- end
-
- gt
- end
-
# Returns set of NFA states to which there is a transition on ast symbol
# +a+ from some state +s+ in +t+.
def following_states(t, a)
@@ -107,7 +62,7 @@ module ActionDispatch
end
def alphabet
- inverted.values.flat_map(&:keys).compact.uniq.sort_by { |x| x.to_s }
+ inverted.values.flat_map(&:keys).compact.uniq.sort_by(&:to_s)
end
# Returns a set of NFA states reachable from some NFA state +s+ in set
diff --git a/actionpack/lib/action_dispatch/journey/nodes/node.rb b/actionpack/lib/action_dispatch/journey/nodes/node.rb
index bb01c087bc..2793c5668d 100644
--- a/actionpack/lib/action_dispatch/journey/nodes/node.rb
+++ b/actionpack/lib/action_dispatch/journey/nodes/node.rb
@@ -14,15 +14,15 @@ module ActionDispatch
end
def each(&block)
- Visitors::Each.new(block).accept(self)
+ Visitors::Each::INSTANCE.accept(self, block)
end
def to_s
- Visitors::String.new.accept(self)
+ Visitors::String::INSTANCE.accept(self, '')
end
def to_dot
- Visitors::Dot.new.accept(self)
+ Visitors::Dot::INSTANCE.accept(self)
end
def to_sym
@@ -30,7 +30,7 @@ module ActionDispatch
end
def name
- left.tr '*:', ''
+ left.tr '*:'.freeze, ''.freeze
end
def type
@@ -39,10 +39,15 @@ module ActionDispatch
def symbol?; false; end
def literal?; false; end
+ def terminal?; false; end
+ def star?; false; end
+ def cat?; false; end
+ def group?; false; end
end
class Terminal < Node # :nodoc:
alias :symbol :left
+ def terminal?; true; end
end
class Literal < Terminal # :nodoc:
@@ -69,11 +74,13 @@ module ActionDispatch
class Symbol < Terminal # :nodoc:
attr_accessor :regexp
alias :symbol :regexp
+ attr_reader :name
DEFAULT_EXP = /[^\.\/\?]+/
def initialize(left)
super
@regexp = DEFAULT_EXP
+ @name = left.tr '*:'.freeze, ''.freeze
end
def default_regexp?
@@ -89,9 +96,11 @@ module ActionDispatch
class Group < Unary # :nodoc:
def type; :GROUP; end
+ def group?; true; end
end
class Star < Unary # :nodoc:
+ def star?; true; end
def type; :STAR; end
def name
@@ -111,6 +120,7 @@ module ActionDispatch
end
class Cat < Binary # :nodoc:
+ def cat?; true; end
def type; :CAT; end
end
diff --git a/actionpack/lib/action_dispatch/journey/parser.rb b/actionpack/lib/action_dispatch/journey/parser.rb
index d129ba7e16..9012297400 100644
--- a/actionpack/lib/action_dispatch/journey/parser.rb
+++ b/actionpack/lib/action_dispatch/journey/parser.rb
@@ -86,7 +86,7 @@ racc_token_table = {
racc_nt_base = 10
-racc_use_result_var = true
+racc_use_result_var = false
Racc_arg = [
racc_action_table,
@@ -133,14 +133,12 @@ Racc_debug_parser = false
# reduce 0 omitted
-def _reduce_1(val, _values, result)
- result = Cat.new(val.first, val.last)
- result
+def _reduce_1(val, _values)
+ Cat.new(val.first, val.last)
end
-def _reduce_2(val, _values, result)
- result = val.first
- result
+def _reduce_2(val, _values)
+ val.first
end
# reduce 3 omitted
@@ -151,24 +149,20 @@ end
# reduce 6 omitted
-def _reduce_7(val, _values, result)
- result = Group.new(val[1])
- result
+def _reduce_7(val, _values)
+ Group.new(val[1])
end
-def _reduce_8(val, _values, result)
- result = Or.new([val.first, val.last])
- result
+def _reduce_8(val, _values)
+ Or.new([val.first, val.last])
end
-def _reduce_9(val, _values, result)
- result = Or.new([val.first, val.last])
- result
+def _reduce_9(val, _values)
+ Or.new([val.first, val.last])
end
-def _reduce_10(val, _values, result)
- result = Star.new(Symbol.new(val.last))
- result
+def _reduce_10(val, _values)
+ Star.new(Symbol.new(val.last))
end
# reduce 11 omitted
@@ -179,27 +173,23 @@ end
# reduce 14 omitted
-def _reduce_15(val, _values, result)
- result = Slash.new('/')
- result
+def _reduce_15(val, _values)
+ Slash.new('/')
end
-def _reduce_16(val, _values, result)
- result = Symbol.new(val.first)
- result
+def _reduce_16(val, _values)
+ Symbol.new(val.first)
end
-def _reduce_17(val, _values, result)
- result = Literal.new(val.first)
- result
+def _reduce_17(val, _values)
+ Literal.new(val.first)
end
-def _reduce_18(val, _values, result)
- result = Dot.new(val.first)
- result
+def _reduce_18(val, _values)
+ Dot.new(val.first)
end
-def _reduce_none(val, _values, result)
+def _reduce_none(val, _values)
val[0]
end
diff --git a/actionpack/lib/action_dispatch/journey/parser.y b/actionpack/lib/action_dispatch/journey/parser.y
index 0ead222551..d3f7c4d765 100644
--- a/actionpack/lib/action_dispatch/journey/parser.y
+++ b/actionpack/lib/action_dispatch/journey/parser.y
@@ -1,11 +1,11 @@
class ActionDispatch::Journey::Parser
-
+ options no_result_var
token SLASH LITERAL SYMBOL LPAREN RPAREN DOT STAR OR
rule
expressions
- : expression expressions { result = Cat.new(val.first, val.last) }
- | expression { result = val.first }
+ : expression expressions { Cat.new(val.first, val.last) }
+ | expression { val.first }
| or
;
expression
@@ -14,14 +14,14 @@ rule
| star
;
group
- : LPAREN expressions RPAREN { result = Group.new(val[1]) }
+ : LPAREN expressions RPAREN { Group.new(val[1]) }
;
or
- : expression OR expression { result = Or.new([val.first, val.last]) }
- | expression OR or { result = Or.new([val.first, val.last]) }
+ : expression OR expression { Or.new([val.first, val.last]) }
+ | expression OR or { Or.new([val.first, val.last]) }
;
star
- : STAR { result = Star.new(Symbol.new(val.last)) }
+ : STAR { Star.new(Symbol.new(val.last)) }
;
terminal
: symbol
@@ -30,16 +30,16 @@ rule
| dot
;
slash
- : SLASH { result = Slash.new('/') }
+ : SLASH { Slash.new('/') }
;
symbol
- : SYMBOL { result = Symbol.new(val.first) }
+ : SYMBOL { Symbol.new(val.first) }
;
literal
- : LITERAL { result = Literal.new(val.first) }
+ : LITERAL { Literal.new(val.first) }
;
dot
- : DOT { result = Dot.new(val.first) }
+ : DOT { Dot.new(val.first) }
;
end
diff --git a/actionpack/lib/action_dispatch/journey/parser_extras.rb b/actionpack/lib/action_dispatch/journey/parser_extras.rb
index 14892f4321..fff0299812 100644
--- a/actionpack/lib/action_dispatch/journey/parser_extras.rb
+++ b/actionpack/lib/action_dispatch/journey/parser_extras.rb
@@ -6,6 +6,10 @@ module ActionDispatch
class Parser < Racc::Parser # :nodoc:
include Journey::Nodes
+ def self.parse(string)
+ new.parse string
+ end
+
def initialize
@scanner = Scanner.new
end
diff --git a/actionpack/lib/action_dispatch/journey/path/pattern.rb b/actionpack/lib/action_dispatch/journey/path/pattern.rb
index 3af940a02f..5ee8810066 100644
--- a/actionpack/lib/action_dispatch/journey/path/pattern.rb
+++ b/actionpack/lib/action_dispatch/journey/path/pattern.rb
@@ -1,5 +1,3 @@
-require 'action_dispatch/journey/router/strexp'
-
module ActionDispatch
module Journey # :nodoc:
module Path # :nodoc:
@@ -7,14 +5,20 @@ module ActionDispatch
attr_reader :spec, :requirements, :anchored
def self.from_string string
- new Journey::Router::Strexp.build(string, {}, ["/.?"], true)
+ build(string, {}, "/.?", true)
+ end
+
+ def self.build(path, requirements, separators, anchored)
+ parser = Journey::Parser.new
+ ast = parser.parse path
+ new ast, requirements, separators, anchored
end
- def initialize(strexp)
- @spec = strexp.ast
- @requirements = strexp.requirements
- @separators = strexp.separators.join
- @anchored = strexp.anchor
+ def initialize(ast, requirements, separators, anchored)
+ @spec = ast
+ @requirements = requirements
+ @separators = separators
+ @anchored = anchored
@names = nil
@optional_names = nil
@@ -28,12 +32,12 @@ module ActionDispatch
end
def ast
- @spec.grep(Nodes::Symbol).each do |node|
+ @spec.find_all(&:symbol?).each do |node|
re = @requirements[node.to_sym]
node.regexp = re if re
end
- @spec.grep(Nodes::Star).each do |node|
+ @spec.find_all(&:star?).each do |node|
node = node.left
node.regexp = @requirements[node.to_sym] || /(.+)/
end
@@ -42,7 +46,7 @@ module ActionDispatch
end
def names
- @names ||= spec.grep(Nodes::Symbol).map { |n| n.name }
+ @names ||= spec.find_all(&:symbol?).map(&:name)
end
def required_names
@@ -50,34 +54,9 @@ module ActionDispatch
end
def optional_names
- @optional_names ||= spec.grep(Nodes::Group).flat_map { |group|
- group.grep(Nodes::Symbol)
- }.map { |n| n.name }.uniq
- end
-
- class RegexpOffsets < Journey::Visitors::Visitor # :nodoc:
- attr_reader :offsets
-
- def initialize(matchers)
- @matchers = matchers
- @capture_count = [0]
- end
-
- def visit(node)
- super
- @capture_count
- end
-
- def visit_SYMBOL(node)
- node = node.to_sym
-
- if @matchers.key?(node)
- re = /#{@matchers[node]}|/
- @capture_count.push((re.match('').length - 1) + (@capture_count.last || 0))
- else
- @capture_count << (@capture_count.last || 0)
- end
- end
+ @optional_names ||= spec.find_all(&:group?).flat_map { |group|
+ group.find_all(&:symbol?)
+ }.map(&:name).uniq
end
class AnchoredRegexp < Journey::Visitors::Visitor # :nodoc:
@@ -122,6 +101,11 @@ module ActionDispatch
re = @matchers[node.left.to_sym] || '.+'
"(#{re})"
end
+
+ def visit_OR(node)
+ children = node.children.map { |n| visit n }
+ "(?:#{children.join(?|)})"
+ end
end
class UnanchoredRegexp < AnchoredRegexp # :nodoc:
@@ -184,8 +168,20 @@ module ActionDispatch
def offsets
return @offsets if @offsets
- viz = RegexpOffsets.new(@requirements)
- @offsets = viz.accept(spec)
+ @offsets = [0]
+
+ spec.find_all(&:symbol?).each do |node|
+ node = node.to_sym
+
+ if @requirements.key?(node)
+ re = /#{@requirements[node]}|/
+ @offsets.push((re.match('').length - 1) + @offsets.last)
+ else
+ @offsets << @offsets.last
+ end
+ end
+
+ @offsets
end
end
end
diff --git a/actionpack/lib/action_dispatch/journey/route.rb b/actionpack/lib/action_dispatch/journey/route.rb
index 9f0a3af902..35c2b1b86e 100644
--- a/actionpack/lib/action_dispatch/journey/route.rb
+++ b/actionpack/lib/action_dispatch/journey/route.rb
@@ -1,42 +1,88 @@
module ActionDispatch
module Journey # :nodoc:
class Route # :nodoc:
- attr_reader :app, :path, :defaults, :name
+ attr_reader :app, :path, :defaults, :name, :precedence
attr_reader :constraints
alias :conditions :constraints
- attr_accessor :precedence
+ 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
+ eoc
+ end
+
+ class Unknown
+ attr_reader :verb
+
+ def initialize(verb)
+ @verb = verb
+ end
+
+ def call(request); @verb === request.request_method; end
+ end
+
+ class All
+ def self.call(_); true; end
+ def self.verb; ''; end
+ end
+
+ 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)
+ VerbMatchers::VERB_TO_CLASS.fetch(verb) do
+ VerbMatchers::Unknown.new verb.to_s.dasherize.upcase
+ end
+ end
+
+ def self.build(name, app, path, constraints, required_defaults, defaults)
+ request_method_match = verb_matcher(constraints.delete(:request_method))
+ new name, app, path, constraints, required_defaults, defaults, request_method_match, 0
+ end
##
# +path+ is a path constraint.
# +constraints+ is a hash of constraints to be applied to this route.
- def initialize(name, app, path, constraints, defaults = {})
+ def initialize(name, app, path, constraints, required_defaults, defaults, request_method_match, precedence)
@name = name
@app = app
@path = path
+ @request_method_match = request_method_match
@constraints = constraints
@defaults = defaults
@required_defaults = nil
+ @_required_defaults = required_defaults
@required_parts = nil
@parts = nil
@decorated_ast = nil
- @precedence = 0
+ @precedence = precedence
@path_formatter = @path.build_formatter
end
def ast
@decorated_ast ||= begin
decorated_ast = path.ast
- decorated_ast.grep(Nodes::Terminal).each { |n| n.memo = self }
+ decorated_ast.find_all(&:terminal?).each { |n| n.memo = self }
decorated_ast
end
end
def requirements # :nodoc:
# needed for rails `rake routes`
- path.requirements.merge(@defaults).delete_if { |_,v|
+ @defaults.merge(path.requirements).delete_if { |_,v|
/.+?/ == v
}
end
@@ -60,7 +106,7 @@ module ActionDispatch
end
def parts
- @parts ||= segments.map { |n| n.to_sym }
+ @parts ||= segments.map(&:to_sym)
end
alias :segment_keys :parts
@@ -68,16 +114,12 @@ module ActionDispatch
@path_formatter.evaluate path_options
end
- def optional_parts
- path.optional_names.map { |n| n.to_sym }
- end
-
def required_parts
- @required_parts ||= path.required_names.map { |n| n.to_sym }
+ @required_parts ||= path.required_names.map(&:to_sym)
end
def required_default?(key)
- (constraints[:required_defaults] || []).include?(key)
+ @_required_defaults.include?(key)
end
def required_defaults
@@ -95,9 +137,8 @@ module ActionDispatch
end
def matches?(request)
- constraints.all? do |method, value|
- next true unless request.respond_to?(method)
-
+ match_verb(request) &&
+ constraints.all? { |method, value|
case value
when Regexp, String
value === request.send(method).to_s
@@ -110,15 +151,28 @@ module ActionDispatch
else
value === request.send(method)
end
- end
+ }
end
def ip
constraints[:ip] || //
end
+ def requires_matching_verb?
+ !@request_method_match.all? { |x| x == VerbMatchers::All }
+ end
+
def verb
- constraints[:request_method] || //
+ verbs.join('|')
+ end
+
+ private
+ def verbs
+ @request_method_match.map(&:verb)
+ end
+
+ def match_verb(request)
+ @request_method_match.any? { |m| m.call request }
end
end
end
diff --git a/actionpack/lib/action_dispatch/journey/router.rb b/actionpack/lib/action_dispatch/journey/router.rb
index 21817b374c..f649588520 100644
--- a/actionpack/lib/action_dispatch/journey/router.rb
+++ b/actionpack/lib/action_dispatch/journey/router.rb
@@ -1,5 +1,4 @@
require 'action_dispatch/journey/router/utils'
-require 'action_dispatch/journey/router/strexp'
require 'action_dispatch/journey/routes'
require 'action_dispatch/journey/formatter'
@@ -68,8 +67,8 @@ module ActionDispatch
def visualizer
tt = GTG::Builder.new(ast).transition_table
- groups = partitioned_routes.first.map(&:ast).group_by { |a| a.to_s }
- asts = groups.values.map { |v| v.first }
+ groups = partitioned_routes.first.map(&:ast).group_by(&:to_s)
+ asts = groups.values.map(&:first)
tt.visualizer(asts)
end
@@ -88,7 +87,7 @@ module ActionDispatch
end
def custom_routes
- partitioned_routes.last
+ routes.custom_routes
end
def filter_routes(path)
@@ -101,11 +100,13 @@ module ActionDispatch
r.path.match(req.path_info)
}
- if req.env["REQUEST_METHOD"] === "HEAD"
- routes.concat get_routes_as_head(routes)
- end
+ routes =
+ if req.head?
+ match_head_routes(routes, req)
+ else
+ match_routes(routes, req)
+ end
- routes.select! { |r| r.matches?(req) }
routes.sort_by!(&:precedence)
routes.map! { |r|
@@ -118,19 +119,24 @@ module ActionDispatch
}
end
- def get_routes_as_head(routes)
- precedence = (routes.map(&:precedence).max || 0) + 1
- routes.select { |r|
- r.verb === "GET" && !(r.verb === "HEAD")
- }.map! { |r|
- Route.new(r.name,
- r.app,
- r.path,
- r.conditions.merge(request_method: "HEAD"),
- r.defaults).tap do |route|
- route.precedence = r.precedence + precedence
- end
- }
+ def match_head_routes(routes, req)
+ verb_specific_routes = routes.select(&:requires_matching_verb?)
+ head_routes = match_routes(verb_specific_routes, req)
+
+ if head_routes.empty?
+ begin
+ req.request_method = "GET"
+ match_routes(routes, req)
+ ensure
+ req.request_method = "HEAD"
+ end
+ else
+ head_routes
+ end
+ end
+
+ def match_routes(routes, req)
+ routes.select { |r| r.matches?(req) }
end
end
end
diff --git a/actionpack/lib/action_dispatch/journey/router/strexp.rb b/actionpack/lib/action_dispatch/journey/router/strexp.rb
deleted file mode 100644
index 4b7738f335..0000000000
--- a/actionpack/lib/action_dispatch/journey/router/strexp.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-module ActionDispatch
- module Journey # :nodoc:
- class Router # :nodoc:
- class Strexp # :nodoc:
- class << self
- alias :compile :new
- end
-
- attr_reader :path, :requirements, :separators, :anchor, :ast
-
- def self.build(path, requirements, separators, anchor = true)
- parser = Journey::Parser.new
- ast = parser.parse path
- new ast, path, requirements, separators, anchor
- end
-
- def initialize(ast, path, requirements, separators, anchor = true)
- @ast = ast
- @path = path
- @requirements = requirements
- @separators = separators
- @anchor = anchor
- end
- end
- end
- end
-end
diff --git a/actionpack/lib/action_dispatch/journey/router/utils.rb b/actionpack/lib/action_dispatch/journey/router/utils.rb
index 2b0a6575d4..9793ca1c7a 100644
--- a/actionpack/lib/action_dispatch/journey/router/utils.rb
+++ b/actionpack/lib/action_dispatch/journey/router/utils.rb
@@ -14,10 +14,10 @@ module ActionDispatch
# normalize_path("/%ab") # => "/%AB"
def self.normalize_path(path)
path = "/#{path}"
- path.squeeze!('/')
- path.sub!(%r{/+\Z}, '')
+ path.squeeze!('/'.freeze)
+ path.sub!(%r{/+\Z}, ''.freeze)
path.gsub!(/(%[a-f0-9]{2})/) { $1.upcase }
- path = '/' if path == ''
+ path = '/' if path == ''.freeze
path
end
@@ -55,7 +55,7 @@ module ActionDispatch
def unescape_uri(uri)
encoding = uri.encoding == US_ASCII ? UTF_8 : uri.encoding
- uri.gsub(ESCAPED) { [$&[1, 2].hex].pack('C') }.force_encoding(encoding)
+ uri.gsub(ESCAPED) { |match| [match[1, 2].hex].pack('C') }.force_encoding(encoding)
end
protected
diff --git a/actionpack/lib/action_dispatch/journey/routes.rb b/actionpack/lib/action_dispatch/journey/routes.rb
index 80e3818ccd..f7b009109e 100644
--- a/actionpack/lib/action_dispatch/journey/routes.rb
+++ b/actionpack/lib/action_dispatch/journey/routes.rb
@@ -5,16 +5,20 @@ module ActionDispatch
class Routes # :nodoc:
include Enumerable
- attr_reader :routes, :named_routes
+ attr_reader :routes, :custom_routes, :anchored_routes
def initialize
@routes = []
- @named_routes = {}
@ast = nil
- @partitioned_routes = nil
+ @anchored_routes = []
+ @custom_routes = []
@simulator = nil
end
+ def empty?
+ routes.empty?
+ end
+
def length
routes.length
end
@@ -30,18 +34,21 @@ module ActionDispatch
def clear
routes.clear
- named_routes.clear
+ anchored_routes.clear
+ custom_routes.clear
end
- def partitioned_routes
- @partitioned_routes ||= routes.partition do |r|
- r.path.anchored && r.ast.grep(Nodes::Symbol).all?(&:default_regexp?)
+ def partition_route(route)
+ if route.path.anchored && route.ast.grep(Nodes::Symbol).all?(&:default_regexp?)
+ anchored_routes << route
+ else
+ custom_routes << route
end
end
def ast
@ast ||= begin
- asts = partitioned_routes.first.map(&:ast)
+ asts = anchored_routes.map(&:ast)
Nodes::Or.new(asts) unless asts.empty?
end
end
@@ -53,13 +60,10 @@ module ActionDispatch
end
end
- # Add a route to the routing table.
- def add_route(app, path, conditions, defaults, name = nil)
- route = Route.new(name, app, path, conditions, defaults)
-
- route.precedence = routes.length
+ def add_route(name, mapping)
+ route = mapping.make_route name, routes.length
routes << route
- named_routes[name] = route if name && !named_routes[name]
+ partition_route(route)
clear_cache!
route
end
@@ -68,7 +72,6 @@ module ActionDispatch
def clear_cache!
@ast = nil
- @partitioned_routes = nil
@simulator = nil
end
end
diff --git a/actionpack/lib/action_dispatch/journey/scanner.rb b/actionpack/lib/action_dispatch/journey/scanner.rb
index 633be11a2d..19e0bc03d6 100644
--- a/actionpack/lib/action_dispatch/journey/scanner.rb
+++ b/actionpack/lib/action_dispatch/journey/scanner.rb
@@ -39,18 +39,18 @@ module ActionDispatch
[:SLASH, text]
when text = @ss.scan(/\*\w+/)
[:STAR, text]
- when text = @ss.scan(/\(/)
+ when text = @ss.scan(/(?<!\\)\(/)
[:LPAREN, text]
- when text = @ss.scan(/\)/)
+ when text = @ss.scan(/(?<!\\)\)/)
[:RPAREN, text]
when text = @ss.scan(/\|/)
[:OR, text]
when text = @ss.scan(/\./)
[:DOT, text]
- when text = @ss.scan(/:\w+/)
+ when text = @ss.scan(/(?<!\\):\w+/)
[:SYMBOL, text]
- when text = @ss.scan(/[\w%\-~]+/)
- [:LITERAL, text]
+ when text = @ss.scan(/(?:[\w%\-~!$&'*+,;=@]|\\:|\\\(|\\\))+/)
+ [:LITERAL, text.tr('\\', '')]
# any char
when text = @ss.scan(/./)
[:LITERAL, text]
diff --git a/actionpack/lib/action_dispatch/journey/visitors.rb b/actionpack/lib/action_dispatch/journey/visitors.rb
index 52b4c8b489..306d2e674a 100644
--- a/actionpack/lib/action_dispatch/journey/visitors.rb
+++ b/actionpack/lib/action_dispatch/journey/visitors.rb
@@ -1,5 +1,3 @@
-# encoding: utf-8
-
module ActionDispatch
module Journey # :nodoc:
class Format
@@ -92,6 +90,45 @@ module ActionDispatch
end
end
+ class FunctionalVisitor # :nodoc:
+ DISPATCH_CACHE = {}
+
+ def accept(node, seed)
+ visit(node, seed)
+ end
+
+ def visit node, seed
+ send(DISPATCH_CACHE[node.type], node, seed)
+ end
+
+ def binary(node, seed)
+ visit(node.right, visit(node.left, seed))
+ end
+ def visit_CAT(n, seed); binary(n, seed); end
+
+ def nary(node, seed)
+ node.children.inject(seed) { |s, c| visit(c, s) }
+ end
+ def visit_OR(n, seed); nary(n, seed); end
+
+ def unary(node, seed)
+ visit(node.left, seed)
+ end
+ def visit_GROUP(n, seed); unary(n, seed); end
+ def visit_STAR(n, seed); unary(n, seed); end
+
+ def terminal(node, seed); seed; end
+ def visit_LITERAL(n, seed); terminal(n, seed); end
+ def visit_SYMBOL(n, seed); terminal(n, seed); end
+ def visit_SLASH(n, seed); terminal(n, seed); end
+ def visit_DOT(n, seed); terminal(n, seed); end
+
+ instance_methods(false).each do |pim|
+ next unless pim =~ /^visit_(.*)$/
+ DISPATCH_CACHE[$1.to_sym] = pim
+ end
+ end
+
class FormatBuilder < Visitor # :nodoc:
def accept(node); Journey::Format.new(super); end
def terminal(node); [node.left]; end
@@ -117,104 +154,110 @@ module ActionDispatch
end
# Loop through the requirements AST
- class Each < Visitor # :nodoc:
- attr_reader :block
-
- def initialize(block)
- @block = block
- end
-
- def visit(node)
+ class Each < FunctionalVisitor # :nodoc:
+ def visit(node, block)
block.call(node)
super
end
+
+ INSTANCE = new
end
- class String < Visitor # :nodoc:
+ class String < FunctionalVisitor # :nodoc:
private
- def binary(node)
- [visit(node.left), visit(node.right)].join
+ def binary(node, seed)
+ visit(node.right, visit(node.left, seed))
end
- def nary(node)
- node.children.map { |c| visit(c) }.join '|'
+ 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 terminal(node)
- node.left
+ def terminal(node, seed)
+ seed + node.left
end
- def visit_GROUP(node)
- "(#{visit(node.left)})"
+ def visit_GROUP(node, seed)
+ visit(node.left, seed << "(".freeze) << ")".freeze
end
+
+ INSTANCE = new
end
- class Dot < Visitor # :nodoc:
+ class Dot < FunctionalVisitor # :nodoc:
def initialize
@nodes = []
@edges = []
end
- def accept(node)
+ def accept(node, seed = [[], []])
super
+ nodes, edges = seed
<<-eodot
digraph parse_tree {
size="8,5"
node [shape = none];
edge [dir = none];
- #{@nodes.join "\n"}
- #{@edges.join("\n")}
+ #{nodes.join "\n"}
+ #{edges.join("\n")}
}
eodot
end
private
- def binary(node)
- node.children.each do |c|
- @edges << "#{node.object_id} -> #{c.object_id};"
- end
+ def binary(node, seed)
+ seed.last.concat node.children.map { |c|
+ "#{node.object_id} -> #{c.object_id};"
+ }
super
end
- def nary(node)
- node.children.each do |c|
- @edges << "#{node.object_id} -> #{c.object_id};"
- end
+ def nary(node, seed)
+ seed.last.concat node.children.map { |c|
+ "#{node.object_id} -> #{c.object_id};"
+ }
super
end
- def unary(node)
- @edges << "#{node.object_id} -> #{node.left.object_id};"
+ def unary(node, seed)
+ seed.last << "#{node.object_id} -> #{node.left.object_id};"
super
end
- def visit_GROUP(node)
- @nodes << "#{node.object_id} [label=\"()\"];"
+ def visit_GROUP(node, seed)
+ seed.first << "#{node.object_id} [label=\"()\"];"
super
end
- def visit_CAT(node)
- @nodes << "#{node.object_id} [label=\"â—‹\"];"
+ def visit_CAT(node, seed)
+ seed.first << "#{node.object_id} [label=\"â—‹\"];"
super
end
- def visit_STAR(node)
- @nodes << "#{node.object_id} [label=\"*\"];"
+ def visit_STAR(node, seed)
+ seed.first << "#{node.object_id} [label=\"*\"];"
super
end
- def visit_OR(node)
- @nodes << "#{node.object_id} [label=\"|\"];"
+ def visit_OR(node, seed)
+ seed.first << "#{node.object_id} [label=\"|\"];"
super
end
- def terminal(node)
+ def terminal(node, seed)
value = node.left
- @nodes << "#{node.object_id} [label=\"#{value}\"];"
+ seed.first << "#{node.object_id} [label=\"#{value}\"];"
+ seed
end
+ INSTANCE = new
end
end
end
diff --git a/actionpack/lib/action_dispatch/journey/visualizer/fsm.css b/actionpack/lib/action_dispatch/journey/visualizer/fsm.css
index 50caebaa18..403e16a7bb 100644
--- a/actionpack/lib/action_dispatch/journey/visualizer/fsm.css
+++ b/actionpack/lib/action_dispatch/journey/visualizer/fsm.css
@@ -16,10 +16,6 @@ h2 {
font-size: 0.5em;
}
-div#chart-2 {
- height: 350px;
-}
-
.clearfix {display: inline-block; }
.input { overflow: show;}
.instruction { color: #666; padding: 0 30px 20px; font-size: 0.9em}
diff --git a/actionpack/lib/action_dispatch/middleware/callbacks.rb b/actionpack/lib/action_dispatch/middleware/callbacks.rb
index baf9d5779e..f80df78582 100644
--- a/actionpack/lib/action_dispatch/middleware/callbacks.rb
+++ b/actionpack/lib/action_dispatch/middleware/callbacks.rb
@@ -1,6 +1,6 @@
module ActionDispatch
- # Provide callbacks to be executed before and after the request dispatch.
+ # Provides callbacks to be executed before and after dispatching the request.
class Callbacks
include ActiveSupport::Callbacks
diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb
index ac9e5effe2..2889acaeb8 100644
--- a/actionpack/lib/action_dispatch/middleware/cookies.rb
+++ b/actionpack/lib/action_dispatch/middleware/cookies.rb
@@ -1,14 +1,57 @@
require 'active_support/core_ext/hash/keys'
-require 'active_support/core_ext/module/attribute_accessors'
-require 'active_support/core_ext/object/blank'
require 'active_support/key_generator'
require 'active_support/message_verifier'
+require 'active_support/json'
module ActionDispatch
- class Request < Rack::Request
+ class Request
def cookie_jar
- env['action_dispatch.cookies'] ||= Cookies::CookieJar.build(self)
+ fetch_header('action_dispatch.cookies'.freeze) do
+ self.cookie_jar = Cookies::CookieJar.build(self, cookies)
+ end
+ end
+
+ # :stopdoc:
+ def have_cookie_jar?
+ has_header? 'action_dispatch.cookies'.freeze
+ end
+
+ def cookie_jar=(jar)
+ set_header 'action_dispatch.cookies'.freeze, jar
+ end
+
+ def key_generator
+ get_header Cookies::GENERATOR_KEY
+ end
+
+ def signed_cookie_salt
+ get_header Cookies::SIGNED_COOKIE_SALT
+ end
+
+ def encrypted_cookie_salt
+ get_header Cookies::ENCRYPTED_COOKIE_SALT
+ end
+
+ def encrypted_signed_cookie_salt
+ get_header Cookies::ENCRYPTED_SIGNED_COOKIE_SALT
+ end
+
+ def secret_token
+ get_header Cookies::SECRET_TOKEN
end
+
+ def secret_key_base
+ get_header Cookies::SECRET_KEY_BASE
+ end
+
+ def cookies_serializer
+ get_header Cookies::COOKIES_SERIALIZER
+ end
+
+ def cookies_digest
+ get_header Cookies::COOKIES_DIGEST
+ end
+ # :startdoc:
end
# \Cookies are read and written through ActionController#cookies.
@@ -70,12 +113,17 @@ module ActionDispatch
# restrict to the domain level. If you use a schema like www.example.com
# and want to share session with user.example.com set <tt>:domain</tt>
# to <tt>:all</tt>. Make sure to specify the <tt>:domain</tt> option with
- # <tt>:all</tt> again when deleting cookies.
+ # <tt>:all</tt> or <tt>Array</tt> again when deleting cookies.
#
- # domain: nil # Does not sets cookie domain. (default)
+ # domain: nil # Does not set cookie domain. (default)
# domain: :all # Allow the cookie for the top most level
# # domain and subdomains.
+ # domain: %w(.example.com .example.org) # Allow the cookie
+ # # for concrete domain names.
#
+ # * <tt>:tld_length</tt> - When using <tt>:domain => :all</tt>, this option can be used to explicitly
+ # set the TLD length when using a short (<= 3 character) domain that is being interpreted as part of a TLD.
+ # For example, to share cookies between user1.lvh.me and user2.lvh.me, set <tt>:tld_length</tt> to 1.
# * <tt>:expires</tt> - The time at which this cookie expires, as a \Time object.
# * <tt>:secure</tt> - Whether this cookie is only transmitted to HTTPS servers.
# Default is +false+.
@@ -90,6 +138,7 @@ module ActionDispatch
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
# Cookies can typically store 4096 bytes.
MAX_COOKIE_SIZE = 4096
@@ -111,14 +160,14 @@ module ActionDispatch
# cookies.permanent.signed[:remember_me] = current_user.id
# # => Set-Cookie: remember_me=BAhU--848956038e692d7046deab32b7131856ab20e14e; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT
def permanent
- @permanent ||= PermanentCookieJar.new(self, @key_generator, @options)
+ @permanent ||= PermanentCookieJar.new(self)
end
# Returns a jar that'll automatically generate a signed representation of cookie value and verify it when reading from
# the cookie again. This is useful for creating cookies with values that the user is not supposed to change. If a signed
# cookie was tampered with by the user (or a 3rd party), nil will be returned.
#
- # If +secrets.secret_key_base+ and +config.secret_token+ (deprecated) are both set,
+ # 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.
#
# This jar requires that you set a suitable secret for the verification on your app's +secrets.secret_key_base+.
@@ -131,17 +180,17 @@ module ActionDispatch
# cookies.signed[:discount] # => 45
def signed
@signed ||=
- if @options[:upgrade_legacy_signed_cookies]
- UpgradeLegacySignedCookieJar.new(self, @key_generator, @options)
+ if upgrade_legacy_signed_cookies?
+ UpgradeLegacySignedCookieJar.new(self)
else
- SignedCookieJar.new(self, @key_generator, @options)
+ SignedCookieJar.new(self)
end
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 +secrets.secret_key_base+ and +config.secret_token+ (deprecated) are both set,
+ # 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.
#
# This jar requires that you set a suitable secret for the verification on your app's +secrets.secret_key_base+.
@@ -154,10 +203,10 @@ module ActionDispatch
# cookies.encrypted[:discount] # => 45
def encrypted
@encrypted ||=
- if @options[:upgrade_legacy_signed_cookies]
- UpgradeLegacyEncryptedCookieJar.new(self, @key_generator, @options)
+ if upgrade_legacy_signed_cookies?
+ UpgradeLegacyEncryptedCookieJar.new(self)
else
- EncryptedCookieJar.new(self, @key_generator, @options)
+ EncryptedCookieJar.new(self)
end
end
@@ -165,18 +214,28 @@ module ActionDispatch
# Used by ActionDispatch::Session::CookieStore to avoid the need to introduce new cookie stores.
def signed_or_encrypted
@signed_or_encrypted ||=
- if @options[:secret_key_base].present?
+ if request.secret_key_base.present?
encrypted
else
signed
end
end
+
+ private
+
+ def upgrade_legacy_signed_cookies?
+ request.secret_token.present? && request.secret_key_base.present?
+ end
end
- module VerifyAndUpgradeLegacySignedMessage
+ # 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(@options[:secret_token], serializer: NullSerializer)
+ @legacy_verifier = ActiveSupport::MessageVerifier.new(request.secret_token, serializer: ActiveSupport::MessageEncryptor::NullSerializer)
end
def verify_and_upgrade_legacy_signed_message(name, signed_message)
@@ -186,6 +245,11 @@ module ActionDispatch
rescue ActiveSupport::MessageVerifier::InvalidSignature
nil
end
+
+ private
+ def parse(name, signed_message)
+ super || verify_and_upgrade_legacy_signed_message(name, signed_message)
+ end
end
class CookieJar #:nodoc:
@@ -205,37 +269,18 @@ module ActionDispatch
# $& => example.local
DOMAIN_REGEXP = /[^.]*\.([^.]*|..\...|...\...)$/
- def self.options_for_env(env) #:nodoc:
- { signed_cookie_salt: env[SIGNED_COOKIE_SALT] || '',
- encrypted_cookie_salt: env[ENCRYPTED_COOKIE_SALT] || '',
- encrypted_signed_cookie_salt: env[ENCRYPTED_SIGNED_COOKIE_SALT] || '',
- secret_token: env[SECRET_TOKEN],
- secret_key_base: env[SECRET_KEY_BASE],
- upgrade_legacy_signed_cookies: env[SECRET_TOKEN].present? && env[SECRET_KEY_BASE].present?,
- serializer: env[COOKIES_SERIALIZER]
- }
- end
-
- def self.build(request)
- env = request.env
- key_generator = env[GENERATOR_KEY]
- options = options_for_env env
-
- host = request.host
- secure = request.ssl?
-
- new(key_generator, host, secure, options).tap do |hash|
- hash.update(request.cookies)
+ def self.build(req, cookies)
+ new(req).tap do |hash|
+ hash.update(cookies)
end
end
- def initialize(key_generator, host = nil, secure = false, options = {})
- @key_generator = key_generator
+ attr_reader :request
+
+ def initialize(request)
@set_cookies = {}
@delete_cookies = {}
- @host = host
- @secure = secure
- @options = options
+ @request = request
@cookies = {}
@committed = false
end
@@ -271,21 +316,32 @@ module ActionDispatch
self
end
+ def update_cookies_from_jar
+ request_jar = @request.cookie_jar.instance_variable_get(:@cookies)
+ set_cookies = request_jar.reject { |k,_| @delete_cookies.key?(k) }
+
+ @cookies.update set_cookies if set_cookies
+ end
+
+ def to_header
+ @cookies.map { |k,v| "#{k}=#{v}" }.join ';'
+ end
+
def handle_options(options) #:nodoc:
options[:path] ||= "/"
- if options[:domain] == :all
+ if options[:domain] == :all || options[:domain] == 'all'
# if there is a provided tld length then we use it otherwise default domain regexp
domain_regexp = options[:tld_length] ? /([^.]+\.?){#{options[:tld_length]}}$/ : DOMAIN_REGEXP
# if host is not ip and matches domain regexp
# (ip confirms to domain regexp so we explicitly check for ip)
- options[:domain] = if (@host !~ /^[\d.]+$/) && (@host =~ domain_regexp)
+ 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| @host.include? domain.sub(/^\./, '') }
+ options[:domain] = options[:domain].find {|domain| request.host.include? domain.sub(/^\./, '') }
end
end
@@ -340,81 +396,92 @@ module ActionDispatch
end
def write(headers)
- @set_cookies.each { |k, v| ::Rack::Utils.set_cookie_header!(headers, k, v) if write_cookie?(v) }
- @delete_cookies.each { |k, v| ::Rack::Utils.delete_cookie_header!(headers, k, v) }
- end
-
- def recycle! #:nodoc:
- @set_cookies = {}
- @delete_cookies = {}
+ if header = make_set_cookie_header(headers[HTTP_HEADER])
+ headers[HTTP_HEADER] = header
+ end
end
mattr_accessor :always_write_cookie
self.always_write_cookie = false
private
- def write_cookie?(cookie)
- @secure || !cookie[:secure] || always_write_cookie
- 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
end
- class PermanentCookieJar #:nodoc:
+ class AbstractCookieJar # :nodoc:
include ChainedCookieJars
- def initialize(parent_jar, key_generator, options = {})
+ def initialize(parent_jar)
@parent_jar = parent_jar
- @key_generator = key_generator
- @options = options
end
def [](name)
- @parent_jar[name.to_s]
+ if data = @parent_jar[name.to_s]
+ parse name, data
+ end
end
def []=(name, options)
if options.is_a?(Hash)
options.symbolize_keys!
else
- options = { :value => options }
+ options = { value: options }
end
- options[:expires] = 20.years.from_now
+ commit(options)
@parent_jar[name] = options
end
- end
- class JsonSerializer
- def self.load(value)
- JSON.parse(value, quirks_mode: true)
- end
+ protected
+ def request; @parent_jar.request; end
- def self.dump(value)
- JSON.generate(value, quirks_mode: true)
- end
+ private
+ def parse(name, data); data; end
+ def commit(options); end
+ end
+
+ class PermanentCookieJar < AbstractCookieJar # :nodoc:
+ private
+ def commit(options)
+ options[:expires] = 20.years.from_now
+ end
end
- # Passing the 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.
- class NullSerializer
+ class JsonSerializer # :nodoc:
def self.load(value)
- value
+ ActiveSupport::JSON.decode(value)
end
def self.dump(value)
- value
+ ActiveSupport::JSON.encode(value)
end
end
- module SerializedCookieJars
+ module SerializedCookieJars # :nodoc:
MARSHAL_SIGNATURE = "\x04\x08".freeze
protected
def needs_migration?(value)
- @options[:serializer] == :hybrid && value.start_with?(MARSHAL_SIGNATURE)
+ request.cookies_serializer == :hybrid && value.start_with?(MARSHAL_SIGNATURE)
end
- def serialize(name, value)
+ def serialize(value)
serializer.dump(value)
end
@@ -431,7 +498,7 @@ module ActionDispatch
end
def serializer
- serializer = @options[:serializer] || :marshal
+ serializer = request.cookies_serializer || :marshal
case serializer
when :marshal
Marshal
@@ -441,115 +508,81 @@ module ActionDispatch
serializer
end
end
+
+ def digest
+ request.cookies_digest || 'SHA1'
+ end
+
+ def key_generator
+ request.key_generator
+ end
end
- class SignedCookieJar #:nodoc:
- include ChainedCookieJars
+ class SignedCookieJar < AbstractCookieJar # :nodoc:
include SerializedCookieJars
- def initialize(parent_jar, key_generator, options = {})
- @parent_jar = parent_jar
- @options = options
- secret = key_generator.generate_key(@options[:signed_cookie_salt])
- @verifier = ActiveSupport::MessageVerifier.new(secret, serializer: NullSerializer)
- end
-
- def [](name)
- if signed_message = @parent_jar[name]
- deserialize name, verify(signed_message)
- end
+ 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)
end
- def []=(name, options)
- if options.is_a?(Hash)
- options.symbolize_keys!
- options[:value] = @verifier.generate(serialize(name, options[:value]))
- else
- options = { :value => @verifier.generate(serialize(name, options)) }
+ private
+ def parse(name, signed_message)
+ deserialize name, @verifier.verified(signed_message)
end
- raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE
- @parent_jar[name] = options
- end
+ def commit(options)
+ options[:value] = @verifier.generate(serialize(options[:value]))
- private
- def verify(signed_message)
- @verifier.verify(signed_message)
- rescue ActiveSupport::MessageVerifier::InvalidSignature
- nil
+ raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE
end
end
# UpgradeLegacySignedCookieJar is used instead of SignedCookieJar if
- # config.secret_token and secrets.secret_key_base are both set. It reads
- # legacy cookies signed with the old dummy key generator and re-saves
- # them using the new key generator to provide a smooth upgrade path.
+ # 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
-
- def [](name)
- if signed_message = @parent_jar[name]
- deserialize(name, verify(signed_message)) || verify_and_upgrade_legacy_signed_message(name, signed_message)
- end
- end
end
- class EncryptedCookieJar #:nodoc:
- include ChainedCookieJars
+ class EncryptedCookieJar < AbstractCookieJar # :nodoc:
include SerializedCookieJars
- def initialize(parent_jar, key_generator, options = {})
+ 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."
end
- @parent_jar = parent_jar
- @options = options
- secret = key_generator.generate_key(@options[:encrypted_cookie_salt])
- sign_secret = key_generator.generate_key(@options[:encrypted_signed_cookie_salt])
- @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, serializer: NullSerializer)
- end
-
- def [](name)
- if encrypted_message = @parent_jar[name]
- deserialize name, decrypt_and_verify(encrypted_message)
- end
- end
-
- def []=(name, options)
- if options.is_a?(Hash)
- options.symbolize_keys!
- else
- options = { :value => options }
- end
-
- options[:value] = @encryptor.encrypt_and_sign(serialize(name, options[:value]))
-
- raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE
- @parent_jar[name] = options
+ 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)
end
private
- def decrypt_and_verify(encrypted_message)
- @encryptor.decrypt_and_verify(encrypted_message)
+ def parse(name, encrypted_message)
+ deserialize name, @encryptor.decrypt_and_verify(encrypted_message)
rescue ActiveSupport::MessageVerifier::InvalidSignature, ActiveSupport::MessageEncryptor::InvalidMessage
nil
end
+
+ def commit(options)
+ options[:value] = @encryptor.encrypt_and_sign(serialize(options[:value]))
+
+ raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE
+ end
end
# UpgradeLegacyEncryptedCookieJar is used by ActionDispatch::Session::CookieStore
- # instead of EncryptedCookieJar if config.secret_token and secrets.secret_key_base
+ # 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
-
- def [](name)
- if encrypted_or_signed_message = @parent_jar[name]
- deserialize(name, decrypt_and_verify(encrypted_or_signed_message)) || verify_and_upgrade_legacy_signed_message(name, encrypted_or_signed_message)
- end
- end
end
def initialize(app)
@@ -557,9 +590,12 @@ module ActionDispatch
end
def call(env)
+ request = ActionDispatch::Request.new env
+
status, headers, body = @app.call(env)
- if cookie_jar = env['action_dispatch.cookies']
+ if request.have_cookie_jar?
+ cookie_jar = request.cookie_jar
unless cookie_jar.committed?
cookie_jar.write(headers)
if headers[HTTP_HEADER].respond_to?(:join)
diff --git a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
index 0ca1a87645..66bb74b9c5 100644
--- a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
+++ b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
@@ -1,6 +1,10 @@
require 'action_dispatch/http/request'
require 'action_dispatch/middleware/exception_wrapper'
require 'action_dispatch/routing/inspector'
+require 'action_view'
+require 'action_view/base'
+
+require 'pp'
module ActionDispatch
# This middleware is responsible for logging exceptions and
@@ -8,12 +12,39 @@ module ActionDispatch
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
+
+ def debug_hash(object)
+ object.to_hash.sort_by { |k, _| k.to_s }.map { |k, v| "#{k}: #{v.inspect rescue $!.message}" }.join("\n")
+ end
+ end
+
def initialize(app, routes_app = nil)
@app = app
@routes_app = routes_app
end
def call(env)
+ request = ActionDispatch::Request.new env
_, headers, body = response = @app.call(env)
if headers['X-Cascade'] == 'pass'
@@ -23,26 +54,37 @@ module ActionDispatch
response
rescue Exception => exception
- raise exception if env['action_dispatch.show_exceptions'] == false
- render_exception(env, exception)
+ raise exception unless request.show_exceptions?
+ render_exception(request, exception)
end
private
- def render_exception(env, exception)
- wrapper = ExceptionWrapper.new(env, exception)
- log_error(env, wrapper)
+ 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')
+ traces = wrapper.traces
+
+ trace_to_show = 'Application Trace'
+ if traces[trace_to_show].empty? && wrapper.rescue_template != 'routing_error'
+ trace_to_show = 'Full Trace'
+ end
+
+ if source_to_show = traces[trace_to_show].first
+ source_to_show_id = source_to_show[:id]
+ end
- if env['action_dispatch.show_detailed_exceptions']
- request = Request.new(env)
- template = ActionView::Base.new([RESCUES_TEMPLATE_PATH],
+ template = DebugView.new([RESCUES_TEMPLATE_PATH],
request: request,
exception: wrapper.exception,
- application_trace: wrapper.application_trace,
- framework_trace: wrapper.framework_trace,
- full_trace: wrapper.full_trace,
+ traces: traces,
+ show_source_idx: source_to_show_id,
+ trace_to_show: trace_to_show,
routes_inspector: routes_inspector(exception),
- source_extract: wrapper.source_extract,
+ source_extracts: wrapper.source_extracts,
line_number: wrapper.line_number,
file: wrapper.file
)
@@ -65,8 +107,8 @@ module ActionDispatch
[status, {'Content-Type' => "#{format}; charset=#{Response.default_charset}", 'Content-Length' => body.bytesize.to_s}, [body]]
end
- def log_error(env, wrapper)
- logger = logger(env)
+ def log_error(request, wrapper)
+ logger = logger(request)
return unless logger
exception = wrapper.exception
@@ -82,8 +124,8 @@ module ActionDispatch
end
end
- def logger(env)
- env['action_dispatch.logger'] || stderr_logger
+ def logger(request)
+ request.logger || stderr_logger
end
def stderr_logger
diff --git a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
index 2326bb043a..5fd984cd07 100644
--- a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
+++ b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
@@ -1,21 +1,25 @@
require 'action_controller/metal/exceptions'
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,
- 'ActionDispatch::ParamsParser::ParseError' => :bad_request,
- 'ActionController::BadRequest' => :bad_request,
- 'ActionController::ParameterMissing' => :bad_request
+ '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_templates
@@ -27,10 +31,10 @@ module ActionDispatch
'ActionView::Template::Error' => 'template_error'
)
- attr_reader :env, :exception, :line_number, :file
+ attr_reader :backtrace_cleaner, :exception, :line_number, :file
- def initialize(env, exception)
- @env = env
+ def initialize(backtrace_cleaner, exception)
+ @backtrace_cleaner = backtrace_cleaner
@exception = original_exception(exception)
expand_backtrace if exception.is_a?(SyntaxError) || exception.try(:original_exception).try(:is_a?, SyntaxError)
@@ -56,21 +60,51 @@ module ActionDispatch
clean_backtrace(:all)
end
+ def traces
+ application_trace_with_ids = []
+ framework_trace_with_ids = []
+ full_trace_with_ids = []
+
+ full_trace.each_with_index do |trace, idx|
+ trace_with_id = { id: idx, trace: trace }
+
+ if application_trace.include?(trace)
+ application_trace_with_ids << trace_with_id
+ else
+ framework_trace_with_ids << trace_with_id
+ end
+
+ full_trace_with_ids << trace_with_id
+ end
+
+ {
+ "Application Trace" => application_trace_with_ids,
+ "Framework Trace" => framework_trace_with_ids,
+ "Full Trace" => full_trace_with_ids
+ }
+ end
+
def self.status_code_for_exception(class_name)
Rack::Utils.status_code(@@rescue_responses[class_name])
end
- def source_extract
- if application_trace && trace = application_trace.first
- file, line, _ = trace.split(":")
- @file = file
- @line_number = line.to_i
- source_fragment(@file, @line_number)
+ def source_extracts
+ backtrace.map do |trace|
+ file, line_number = extract_file_and_line_number(trace)
+
+ {
+ code: source_fragment(file, line_number),
+ line_number: line_number
+ }
end
end
private
+ def backtrace
+ Array(@exception.backtrace)
+ end
+
def original_exception(exception)
if registered_original_exception?(exception)
exception.original_exception
@@ -85,16 +119,12 @@ module ActionDispatch
def clean_backtrace(*args)
if backtrace_cleaner
- backtrace_cleaner.clean(@exception.backtrace, *args)
+ backtrace_cleaner.clean(backtrace, *args)
else
- @exception.backtrace
+ backtrace
end
end
- def backtrace_cleaner
- @backtrace_cleaner ||= @env['action_dispatch.backtrace_cleaner']
- end
-
def source_fragment(path, line)
return unless Rails.respond_to?(:root) && Rails.root
full_path = Rails.root.join(path)
@@ -107,10 +137,17 @@ module ActionDispatch
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 expand_backtrace
@exception.backtrace.unshift(
@exception.to_s.split("\n")
- ).flatten!
+ ).flatten!
end
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb
index e90f8b9ce6..c51dcd542a 100644
--- a/actionpack/lib/action_dispatch/middleware/flash.rb
+++ b/actionpack/lib/action_dispatch/middleware/flash.rb
@@ -1,15 +1,6 @@
require 'active_support/core_ext/hash/keys'
module ActionDispatch
- class Request < Rack::Request
- # Access the contents of the flash. Use <tt>flash["notice"]</tt> to
- # read a notice you put there or <tt>flash["notice"] = "hello"</tt>
- # to put a new one.
- def flash
- @env[Flash::KEY] ||= Flash::FlashHash.from_session_value(session["flash"])
- end
- end
-
# The flash provides a way to pass temporary primitive-types (String, Array, Hash) between actions. Anything you place in the flash will be exposed
# to the very next action and then cleared out. This is a great way of doing notices and alerts, such as a create
# action that sets <tt>flash[:notice] = "Post successfully created"</tt> before redirecting to a display action that can
@@ -47,6 +38,40 @@ module ActionDispatch
class Flash
KEY = 'action_dispatch.request.flash_hash'.freeze
+ module RequestMethods
+ # Access the contents of the flash. Use <tt>flash["notice"]</tt> to
+ # read a notice you put there or <tt>flash["notice"] = "hello"</tt>
+ # to put a new one.
+ def flash
+ flash = flash_hash
+ return flash if flash
+ self.flash = Flash::FlashHash.from_session_value(session["flash"])
+ end
+
+ def flash=(flash)
+ set_header Flash::KEY, flash
+ end
+
+ def flash_hash # :nodoc:
+ get_header Flash::KEY
+ end
+
+ def commit_flash # :nodoc:
+ session = self.session || {}
+ flash_hash = self.flash_hash
+
+ if flash_hash && (flash_hash.present? || session.key?('flash'))
+ session["flash"] = flash_hash.to_session_value
+ self.flash = flash_hash.dup
+ end
+
+ if (!session.respond_to?(:loaded?) || session.loaded?) && # (reset_session uses {}, which doesn't implement #loaded?)
+ session.key?('flash') && session['flash'].nil?
+ session.delete('flash')
+ end
+ end
+ end
+
class FlashNow #:nodoc:
attr_accessor :flash
@@ -79,22 +104,31 @@ module ActionDispatch
class FlashHash
include Enumerable
- def self.from_session_value(value)
- flash = case value
- when FlashHash # Rails 3.1, 3.2
- new(value.instance_variable_get(:@flashes), value.instance_variable_get(:@used))
- when Hash # Rails 4.0
- new(value['flashes'], value['discard'])
- else
- new
- end
-
- flash.tap(&:sweep)
+ def self.from_session_value(value) #:nodoc:
+ case value
+ when FlashHash # Rails 3.1, 3.2
+ flashes = value.instance_variable_get(:@flashes)
+ if discard = value.instance_variable_get(:@used)
+ flashes.except!(*discard)
+ end
+ new(flashes, flashes.keys)
+ when Hash # Rails 4.0
+ flashes = value['flashes']
+ if discard = value['discard']
+ flashes.except!(*discard)
+ end
+ new(flashes, flashes.keys)
+ else
+ new
+ end
end
- def to_session_value
- return nil if empty?
- {'discard' => @discard.to_a, 'flashes' => @flashes}
+ # Builds a hash containing the flashes to keep for the next request.
+ # 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}
end
def initialize(flashes = {}, discard = []) #:nodoc:
@@ -132,7 +166,7 @@ module ActionDispatch
end
def key?(name)
- @flashes.key? name
+ @flashes.key? name.to_s
end
def delete(key)
@@ -249,25 +283,10 @@ module ActionDispatch
end
end
- def initialize(app)
- @app = app
- end
-
- def call(env)
- @app.call(env)
- ensure
- session = Request::Session.find(env) || {}
- flash_hash = env[KEY]
-
- if flash_hash && (flash_hash.present? || session.key?('flash'))
- session["flash"] = flash_hash.to_session_value
- env[KEY] = flash_hash.dup
- end
+ def self.new(app) app; end
+ end
- if (!session.respond_to?(:loaded?) || session.loaded?) && # (reset_session uses {}, which doesn't implement #loaded?)
- session.key?('flash') && session['flash'].nil?
- session.delete('flash')
- end
- end
+ class Request
+ prepend Flash::RequestMethods
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/load_interlock.rb b/actionpack/lib/action_dispatch/middleware/load_interlock.rb
new file mode 100644
index 0000000000..07f498319c
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/load_interlock.rb
@@ -0,0 +1,21 @@
+require 'active_support/dependencies'
+require 'rack/body_proxy'
+
+module ActionDispatch
+ class LoadInterlock
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ interlock = ActiveSupport::Dependencies.interlock
+ interlock.start_running
+ response = @app.call(env)
+ body = Rack::BodyProxy.new(response[2]) { interlock.done_running }
+ response[2] = body
+ response
+ ensure
+ interlock.done_running unless body
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/middleware/params_parser.rb b/actionpack/lib/action_dispatch/middleware/params_parser.rb
index b426183488..18af0a583a 100644
--- a/actionpack/lib/action_dispatch/middleware/params_parser.rb
+++ b/actionpack/lib/action_dispatch/middleware/params_parser.rb
@@ -1,9 +1,14 @@
-require 'active_support/core_ext/hash/conversions'
require 'action_dispatch/http/request'
-require 'active_support/core_ext/hash/indifferent_access'
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
attr_reader :original_exception
@@ -13,48 +18,13 @@ module ActionDispatch
end
end
- DEFAULT_PARSERS = { Mime::JSON => :json }
-
- def initialize(app, parsers = {})
- @app, @parsers = app, DEFAULT_PARSERS.merge(parsers)
+ # 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 = {})
+ ActionDispatch::Request.parameter_parsers = ActionDispatch::Request::DEFAULT_PARSERS.merge(parsers)
+ app
end
-
- def call(env)
- if params = parse_formatted_parameters(env)
- env["action_dispatch.request.request_parameters"] = params
- end
-
- @app.call(env)
- end
-
- private
- def parse_formatted_parameters(env)
- request = Request.new(env)
-
- return false if request.content_length.zero?
-
- strategy = @parsers[request.content_mime_type]
-
- return false unless strategy
-
- case strategy
- when Proc
- strategy.call(request.raw_post)
- when :json
- data = ActiveSupport::JSON.decode(request.raw_post)
- data = {:_json => data} unless data.is_a?(Hash)
- Request::Utils.deep_munge(data).with_indifferent_access
- else
- false
- end
- rescue Exception => e # JSON or Ruby code block errors
- logger(env).debug "Error occurred while parsing request parameters.\nContents:\n\n#{request.raw_post}"
-
- raise ParseError.new(e.message, e)
- end
-
- def logger(env)
- env['action_dispatch.logger'] || ActiveSupport::Logger.new($stderr)
- end
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/public_exceptions.rb b/actionpack/lib/action_dispatch/middleware/public_exceptions.rb
index 6c8944e067..0f27984550 100644
--- a/actionpack/lib/action_dispatch/middleware/public_exceptions.rb
+++ b/actionpack/lib/action_dispatch/middleware/public_exceptions.rb
@@ -1,4 +1,14 @@
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`
+ # directory. For example when this middleware receives a 500 response it will
+ # render the template found in `/public/500.html`.
+ # 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`.
+ #
+ # 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.
class PublicExceptions
attr_accessor :public_path
@@ -7,10 +17,10 @@ module ActionDispatch
end
def call(env)
- status = env["PATH_INFO"][1..-1]
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.to_i, Rack::Utils::HTTP_STATUS_CODES[500]) }
+ body = { :status => status, :error => Rack::Utils::HTTP_STATUS_CODES.fetch(status, Rack::Utils::HTTP_STATUS_CODES[500]) }
render(status, content_type, body)
end
diff --git a/actionpack/lib/action_dispatch/middleware/reloader.rb b/actionpack/lib/action_dispatch/middleware/reloader.rb
index 15b5a48535..af9a29eb07 100644
--- a/actionpack/lib/action_dispatch/middleware/reloader.rb
+++ b/actionpack/lib/action_dispatch/middleware/reloader.rb
@@ -1,5 +1,3 @@
-require 'active_support/deprecation/reporting'
-
module ActionDispatch
# ActionDispatch::Reloader provides prepare and cleanup callbacks,
# intended to assist with code reloading during development.
@@ -11,9 +9,9 @@ module ActionDispatch
# the response body. This is important for streaming responses such as the
# following:
#
- # self.response_body = lambda { |response, output|
+ # 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
diff --git a/actionpack/lib/action_dispatch/middleware/remote_ip.rb b/actionpack/lib/action_dispatch/middleware/remote_ip.rb
index 6a79b4e859..aee2334da9 100644
--- a/actionpack/lib/action_dispatch/middleware/remote_ip.rb
+++ b/actionpack/lib/action_dispatch/middleware/remote_ip.rb
@@ -1,3 +1,5 @@
+require 'ipaddr'
+
module ActionDispatch
# This middleware calculates the IP address of the remote client that is
# making the request. It does this by checking various headers that could
@@ -28,14 +30,14 @@ module ActionDispatch
# 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.
- TRUSTED_PROXIES = %r{
- ^127\.0\.0\.1$ | # localhost IPv4
- ^::1$ | # localhost IPv6
- ^[fF][cCdD] | # private IPv6 range fc00::/7
- ^10\. | # private IPv4 range 10.x.x.x
- ^172\.(1[6-9]|2[0-9]|3[0-1])\.| # private IPv4 range 172.16.0.0 .. 172.31.255.255
- ^192\.168\. # private IPv4 range 192.168.x.x
- }x
+ TRUSTED_PROXIES = [
+ "127.0.0.1", # localhost IPv4
+ "::1", # localhost IPv6
+ "fc00::/7", # private IPv6 range fc00::/7
+ "10.0.0.0/8", # private IPv4 range 10.x.x.x
+ "172.16.0.0/12", # private IPv4 range 172.16.0.0 .. 172.31.255.255
+ "192.168.0.0/16", # private IPv4 range 192.168.x.x
+ ].map { |proxy| IPAddr.new(proxy) }
attr_reader :check_ip, :proxies
@@ -47,24 +49,24 @@ module ActionDispatch
# clients (like WAP devices), or behind proxies that set headers in an
# incorrect or confusing way (like AWS ELB).
#
- # The +custom_proxies+ argument can take a regex, which will be used
- # instead of +TRUSTED_PROXIES+, or a string, which will be used in addition
- # to +TRUSTED_PROXIES+. Any proxy setup will put the value you want in the
- # middle (or at the beginning) of the X-Forwarded-For list, with your proxy
- # servers after it. If your proxies aren't removed, pass them in via the
- # +custom_proxies+ parameter. That way, the middleware will ignore those
- # IP addresses, and return the one that you want.
+ # The +custom_proxies+ argument can take an Array of string, IPAddr, or
+ # Regexp objects which will be used instead of +TRUSTED_PROXIES+. If a
+ # single string, IPAddr, or Regexp object is provided, it will be used in
+ # addition to +TRUSTED_PROXIES+. Any proxy setup will put the value you
+ # want in the middle (or at the beginning) of the X-Forwarded-For list,
+ # with your proxy servers after it. If your proxies aren't removed, pass
+ # them in via the +custom_proxies+ parameter. That way, the middleware will
+ # ignore those IP addresses, and return the one that you want.
def initialize(app, check_ip_spoofing = true, custom_proxies = nil)
@app = app
@check_ip = check_ip_spoofing
- @proxies = case custom_proxies
- when Regexp
- custom_proxies
- when nil
- TRUSTED_PROXIES
- else
- Regexp.union(TRUSTED_PROXIES, custom_proxies)
- end
+ @proxies = if custom_proxies.blank?
+ TRUSTED_PROXIES
+ elsif custom_proxies.respond_to?(:any?)
+ custom_proxies
+ else
+ Array(custom_proxies) + TRUSTED_PROXIES
+ end
end
# Since the IP address may not be needed, we store the object here
@@ -72,44 +74,19 @@ module ActionDispatch
# requests. For those requests that do need to know the IP, the
# GetIp#calculate_ip method will calculate the memoized client IP address.
def call(env)
- env["action_dispatch.remote_ip"] = GetIp.new(env, self)
- @app.call(env)
+ req = ActionDispatch::Request.new env
+ req.remote_ip = GetIp.new(req, check_ip, proxies)
+ @app.call(req.env)
end
# The GetIp class exists as a way to defer processing of the request data
# into an actual IP address. If the ActionDispatch::Request#remote_ip method
# is called, this class will calculate the value and then memoize it.
class GetIp
-
- # This constant contains a regular expression that validates every known
- # form of IP v4 and v6 address, with or without abbreviations, adapted
- # from {this gist}[https://gist.github.com/gazay/1289635].
- VALID_IP = %r{
- (^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[0-9]{1,2})(\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[0-9]{1,2})){3}$) | # ip v4
- (^(
- (([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}) | # ip v6 not abbreviated
- (([0-9A-Fa-f]{1,4}:){6}:[0-9A-Fa-f]{1,4}) | # ip v6 with double colon in the end
- (([0-9A-Fa-f]{1,4}:){5}:([0-9A-Fa-f]{1,4}:)?[0-9A-Fa-f]{1,4}) | # - ip addresses v6
- (([0-9A-Fa-f]{1,4}:){4}:([0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4}) | # - with
- (([0-9A-Fa-f]{1,4}:){3}:([0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4}) | # - double colon
- (([0-9A-Fa-f]{1,4}:){2}:([0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4}) | # - in the middle
- (([0-9A-Fa-f]{1,4}:){6} ((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3} (\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4
- (([0-9A-Fa-f]{1,4}:){1,5}:((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4
- (([0-9A-Fa-f]{1,4}:){1}:([0-9A-Fa-f]{1,4}:){0,4}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4
- (([0-9A-Fa-f]{1,4}:){0,2}:([0-9A-Fa-f]{1,4}:){0,3}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4
- (([0-9A-Fa-f]{1,4}:){0,3}:([0-9A-Fa-f]{1,4}:){0,2}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4
- (([0-9A-Fa-f]{1,4}:){0,4}:([0-9A-Fa-f]{1,4}:){1}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4
- (::([0-9A-Fa-f]{1,4}:){0,5}((\b((25[0-5])|(1\d{2})|(2[0-4]\d) |(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4
- ([0-9A-Fa-f]{1,4}::([0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4}) | # ip v6 with compatible to v4
- (::([0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4}) | # ip v6 with double colon at the beginning
- (([0-9A-Fa-f]{1,4}:){1,7}:) # ip v6 without ending
- )$)
- }x
-
- def initialize(env, middleware)
- @env = env
- @check_ip = middleware.check_ip
- @proxies = middleware.proxies
+ def initialize(req, check_ip, proxies)
+ @req = req
+ @check_ip = check_ip
+ @proxies = proxies
end
# Sort through the various IP address headers, looking for the IP most
@@ -132,11 +109,11 @@ module ActionDispatch
# the last address left, which was presumably set by one of those proxies.
def calculate_ip
# Set by the Rack web server, this is a single value.
- remote_addr = ips_from('REMOTE_ADDR').last
+ remote_addr = ips_from(@req.remote_addr).last
# Could be a CSV list and/or repeated headers that were concatenated.
- client_ips = ips_from('HTTP_CLIENT_IP').reverse
- forwarded_ips = ips_from('HTTP_X_FORWARDED_FOR').reverse
+ client_ips = ips_from(@req.client_ip).reverse
+ forwarded_ips = ips_from(@req.x_forwarded_for).reverse
# +Client-Ip+ and +X-Forwarded-For+ should not, generally, both be set.
# If they are both set, it means that this request passed through two
@@ -147,8 +124,8 @@ module ActionDispatch
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=#{@env['HTTP_CLIENT_IP'].inspect} " +
- "HTTP_X_FORWARDED_FOR=#{@env['HTTP_X_FORWARDED_FOR'].inspect}"
+ "HTTP_CLIENT_IP=#{@req.client_ip.inspect} " +
+ "HTTP_X_FORWARDED_FOR=#{@req.x_forwarded_for.inspect}"
end
# We assume these things about the IP headers:
@@ -171,14 +148,25 @@ module ActionDispatch
protected
def ips_from(header)
+ return [] unless header
# Split the comma-separated list into an array of strings
- ips = @env[header] ? @env[header].strip.split(/[,\s]+/) : []
- # Only return IPs that are valid according to the regex
- ips.select{ |ip| ip =~ VALID_IP }
+ 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
+ end
end
def filter_proxies(ips)
- ips.reject { |ip| ip =~ @proxies }
+ ips.reject do |ip|
+ @proxies.any? { |proxy| proxy === ip }
+ end
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/request_id.rb b/actionpack/lib/action_dispatch/middleware/request_id.rb
index 5d1740d0d4..1555ff72af 100644
--- a/actionpack/lib/action_dispatch/middleware/request_id.rb
+++ b/actionpack/lib/action_dispatch/middleware/request_id.rb
@@ -3,28 +3,33 @@ 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) and sends the same id to the client via the X-Request-Id header.
+ # ActionDispatch::Request#uuid or the alias ActionDispatch::Request#request_id) and sends the same id to the client via the X-Request-Id header.
#
- # The unique request id is either based off the X-Request-Id header in the request, which would typically be generated
+ # The unique request id is either based on the X-Request-Id header in the request, which would typically be generated
# by a firewall, load balancer, or the web server, or, if this header is not available, a random uuid. If the
# header is accepted from the outside world, we sanitize it to a max of 255 chars and alphanumeric and dashes only.
#
# The unique request id can be used to trace a request end-to-end and would typically end up being part of log files
# from multiple pieces of the stack.
class RequestId
+ X_REQUEST_ID = "X-Request-Id".freeze # :nodoc:
+
def initialize(app)
@app = app
end
def call(env)
- env["action_dispatch.request_id"] = external_request_id(env) || internal_request_id
- @app.call(env).tap { |_status, headers, _body| headers["X-Request-Id"] = env["action_dispatch.request_id"] }
+ req = ActionDispatch::Request.new env
+ req.request_id = make_request_id(req.x_request_id)
+ @app.call(env).tap { |_status, headers, _body| headers[X_REQUEST_ID] = req.request_id }
end
private
- def external_request_id(env)
- if request_id = env["HTTP_X_REQUEST_ID"].presence
- request_id.gsub(/[^\w\-]/, "").first(255)
+ def make_request_id(request_id)
+ if request_id.presence
+ request_id.gsub(/[^\w\-]/, "".freeze).first(255)
+ else
+ internal_request_id
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb
index 84df55fd5a..9e50fea3fc 100644
--- a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb
+++ b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb
@@ -36,6 +36,11 @@ module ActionDispatch
@default_options.delete(:sidbits)
@default_options.delete(:secure_random)
end
+
+ private
+ def make_request(env)
+ ActionDispatch::Request.new env
+ end
end
module StaleSessionCheck
@@ -65,8 +70,8 @@ module ActionDispatch
end
module SessionObject # :nodoc:
- def prepare_session(env)
- Request::Session.create(self, env, @default_options)
+ def prepare_session(req)
+ Request::Session.create(self, req, @default_options)
end
def loaded_session?(session)
@@ -74,15 +79,14 @@ module ActionDispatch
end
end
- class AbstractStore < Rack::Session::Abstract::ID
+ class AbstractStore < Rack::Session::Abstract::Persisted
include Compatibility
include StaleSessionCheck
include SessionObject
private
- def set_cookie(env, session_id, cookie)
- request = ActionDispatch::Request.new(env)
+ def set_cookie(request, session_id, cookie)
request.cookie_jar[key] = cookie
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 1db6194271..589ae46e38 100644
--- a/actionpack/lib/action_dispatch/middleware/session/cache_store.rb
+++ b/actionpack/lib/action_dispatch/middleware/session/cache_store.rb
@@ -2,12 +2,15 @@ require 'action_dispatch/middleware/session/abstract_store'
module ActionDispatch
module Session
- # Session store that uses an ActiveSupport::Cache::Store to store the sessions. This store is most useful
+ # A session store that uses an ActiveSupport::Cache::Store to store the sessions. This store is most useful
# if you don't store critical data in your sessions and you don't need them to live for extended periods
# of time.
+ #
+ # ==== Options
+ # * <tt>cache</tt> - The cache to use. If it is not specified, <tt>Rails.cache</tt> will be used.
+ # * <tt>expire_after</tt> - The length of time a session will be stored before automatically expiring.
+ # By default, the <tt>:expires_in</tt> option of the cache is used.
class CacheStore < AbstractStore
- # Create a new store. The cache to use can be passed in the <tt>:cache</tt> option. If it is
- # not specified, <tt>Rails.cache</tt> will be used.
def initialize(app, options = {})
@cache = options[:cache] || Rails.cache
options[:expire_after] ||= @cache.options[:expires_in]
@@ -15,15 +18,15 @@ module ActionDispatch
end
# Get a session from the cache.
- def get_session(env, sid)
- sid ||= generate_sid
- session = @cache.read(cache_key(sid))
- session ||= {}
+ def find_session(env, sid)
+ unless sid and session = @cache.read(cache_key(sid))
+ sid, session = generate_sid, {}
+ end
[sid, session]
end
# Set a session in the cache.
- def set_session(env, sid, session, options)
+ def write_session(env, sid, session, options)
key = cache_key(sid)
if session
@cache.write(key, session, :expires_in => options[:expire_after])
@@ -34,7 +37,7 @@ module ActionDispatch
end
# Remove a session from the cache.
- def destroy_session(env, sid, options)
+ def delete_session(env, sid, options)
@cache.delete(cache_key(sid))
generate_sid
end
diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
index ed25c67ae5..0e636b8257 100644
--- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
+++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
@@ -52,25 +52,31 @@ module ActionDispatch
# JavaScript before upgrading.
#
# Note that changing the secret key will invalidate all existing sessions!
- class CookieStore < Rack::Session::Abstract::ID
- include Compatibility
- include StaleSessionCheck
- include SessionObject
-
+ #
+ # Because CookieStore extends Rack::Session::Abstract::Persisted, many of the
+ # options described there can be used to customize the session cookie that
+ # is generated. For example:
+ #
+ # Rails.application.config.session_store :cookie_store, expire_after: 14.days
+ #
+ # would set the session cookie to expire automatically 14 days after creation.
+ # Other useful options include <tt>:key</tt>, <tt>:secure</tt> and
+ # <tt>:httponly</tt>.
+ class CookieStore < AbstractStore
def initialize(app, options={})
super(app, options.merge!(:cookie_only => true))
end
- def destroy_session(env, session_id, options)
+ def delete_session(req, session_id, options)
new_sid = generate_sid unless options[:drop]
# Reset hash and Assign the new session id
- env["action_dispatch.request.unsigned_session_cookie"] = new_sid ? { "session_id" => new_sid } : {}
+ req.set_header("action_dispatch.request.unsigned_session_cookie", new_sid ? { "session_id" => new_sid } : {})
new_sid
end
- def load_session(env)
+ def load_session(req)
stale_session_check! do
- data = unpacked_cookie_data(env)
+ data = unpacked_cookie_data(req)
data = persistent_session_id!(data)
[data["session_id"], data]
end
@@ -78,20 +84,21 @@ module ActionDispatch
private
- def extract_session_id(env)
+ def extract_session_id(req)
stale_session_check! do
- unpacked_cookie_data(env)["session_id"]
+ unpacked_cookie_data(req)["session_id"]
end
end
- def unpacked_cookie_data(env)
- env["action_dispatch.request.unsigned_session_cookie"] ||= begin
- stale_session_check! do
- if data = get_cookie(env)
+ def unpacked_cookie_data(req)
+ req.fetch_header("action_dispatch.request.unsigned_session_cookie") do |k|
+ v = stale_session_check! do
+ if data = get_cookie(req)
data.stringify_keys!
end
data || {}
end
+ req.set_header k, v
end
end
@@ -101,21 +108,20 @@ module ActionDispatch
data
end
- def set_session(env, sid, session_data, options)
+ def write_session(req, sid, session_data, options)
session_data["session_id"] = sid
session_data
end
- def set_cookie(env, session_id, cookie)
- cookie_jar(env)[@key] = cookie
+ def set_cookie(request, session_id, cookie)
+ cookie_jar(request)[@key] = cookie
end
- def get_cookie(env)
- cookie_jar(env)[@key]
+ def get_cookie(req)
+ cookie_jar(req)[@key]
end
- def cookie_jar(env)
- request = ActionDispatch::Request.new(env)
+ def cookie_jar(request)
request.cookie_jar.signed_or_encrypted
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 b4d6629c35..cb19786f0b 100644
--- a/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb
+++ b/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb
@@ -8,6 +8,10 @@ end
module ActionDispatch
module Session
+ # A session store that uses MemCache to implement storage.
+ #
+ # ==== Options
+ # * <tt>expire_after</tt> - The length of time a session will be stored before automatically expiring.
class MemCacheStore < Rack::Session::Dalli
include Compatibility
include StaleSessionCheck
diff --git a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb
index f0779279c1..64695f9738 100644
--- a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb
+++ b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb
@@ -27,24 +27,26 @@ module ActionDispatch
end
def call(env)
+ request = ActionDispatch::Request.new env
@app.call(env)
rescue Exception => exception
- if env['action_dispatch.show_exceptions'] == false
- raise exception
+ if request.show_exceptions?
+ render_exception(request, exception)
else
- render_exception(env, exception)
+ raise exception
end
end
private
- def render_exception(env, exception)
- wrapper = ExceptionWrapper.new(env, exception)
+ def render_exception(request, exception)
+ backtrace_cleaner = request.get_header 'action_dispatch.backtrace_cleaner'
+ wrapper = ExceptionWrapper.new(backtrace_cleaner, exception)
status = wrapper.status_code
- env["action_dispatch.exception"] = wrapper.exception
- env["action_dispatch.original_path"] = env["PATH_INFO"]
- env["PATH_INFO"] = "/#{status}"
- response = @exceptions_app.call(env)
+ 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 "}"
diff --git a/actionpack/lib/action_dispatch/middleware/ssl.rb b/actionpack/lib/action_dispatch/middleware/ssl.rb
index 0c7caef25d..47f475559a 100644
--- a/actionpack/lib/action_dispatch/middleware/ssl.rb
+++ b/actionpack/lib/action_dispatch/middleware/ssl.rb
@@ -1,72 +1,129 @@
module ActionDispatch
+ # This middleware is added to the stack when `config.force_ssl = true`.
+ # It does three jobs to enforce secure HTTP requests:
+ #
+ # 1. TLS redirect. http:// requests are permanently redirected to https://
+ # with the same URL host, path, etc. Pass `:host` and/or `:port` to
+ # modify the destination URL. This is always enabled.
+ #
+ # 2. Secure cookies. Sets the `secure` flag on cookies to tell browsers they
+ # mustn't be sent along with http:// requests. This is always enabled.
+ #
+ # 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. Pass `hsts: false` to disable.
+ #
+ # Configure HSTS with `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 `false`.
+ # * `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.
+ #
+ # Disabling 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 }`.
class SSL
- YEAR = 31536000
+ # 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
def self.default_hsts_options
- { :expires => YEAR, :subdomains => false }
+ { expires: HSTS_EXPIRES_IN, subdomains: false, preload: false }
end
- def initialize(app, options = {})
+ def initialize(app, redirect: {}, hsts: {}, **options)
@app = app
- @hsts = options.fetch(:hsts, {})
- @hsts = {} if @hsts == true
- @hsts = self.class.default_hsts_options.merge(@hsts) if @hsts
+ 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
- @host = options[:host]
- @port = options[:port]
+ @hsts_header = build_hsts_header(normalize_hsts_options(hsts))
end
def call(env)
- request = Request.new(env)
+ request = Request.new env
if request.ssl?
- status, headers, body = @app.call(env)
- headers = hsts_headers.merge(headers)
- flag_cookies_as_secure!(headers)
- [status, headers, body]
+ @app.call(env).tap do |status, headers, body|
+ set_hsts_header! headers
+ flag_cookies_as_secure! headers
+ end
else
- redirect_to_https(request)
+ redirect_to_https request
end
end
private
- def redirect_to_https(request)
- host = @host || request.host
- port = @port || request.port
-
- location = "https://#{host}"
- location << ":#{port}" if port != 80
- location << request.fullpath
-
- headers = { 'Content-Type' => 'text/html', 'Location' => location }
-
- [301, headers, []]
+ def set_hsts_header!(headers)
+ headers['Strict-Transport-Security'.freeze] ||= @hsts_header
end
- # http://tools.ietf.org/html/draft-hodges-strict-transport-sec-02
- def hsts_headers
- if @hsts
- value = "max-age=#{@hsts[:expires].to_i}"
- value += "; includeSubDomains" if @hsts[:subdomains]
- { 'Strict-Transport-Security' => value }
+ def normalize_hsts_options(options)
+ case options
+ # Explicitly disabling HSTS clears the existing setting from browsers
+ # by setting expiry to 0.
+ when false
+ self.class.default_hsts_options.merge(expires: 0)
+ # Default to enabled, with default options.
+ when nil, true
+ self.class.default_hsts_options
else
- {}
+ self.class.default_hsts_options.merge(options)
end
end
+ # http://tools.ietf.org/html/rfc6797#section-6.1
+ def build_hsts_header(hsts)
+ value = "max-age=#{hsts[:expires].to_i}"
+ value << "; includeSubDomains" if hsts[:subdomains]
+ value << "; preload" if hsts[:preload]
+ value
+ end
+
def flag_cookies_as_secure!(headers)
- if cookies = headers['Set-Cookie']
- cookies = cookies.split("\n")
+ if cookies = headers['Set-Cookie'.freeze]
+ cookies = cookies.split("\n".freeze)
- headers['Set-Cookie'] = cookies.map { |cookie|
+ headers['Set-Cookie'.freeze] = cookies.map { |cookie|
if cookie !~ /;\s*secure\s*(;|$)/i
"#{cookie}; secure"
else
cookie
end
- }.join("\n")
+ }.join("\n".freeze)
end
end
+
+ def redirect_to_https(request)
+ [ @redirect.fetch(:status, 301),
+ { 'Content-Type' => 'text/html',
+ 'Location' => https_location_for(request) },
+ @redirect.fetch(:body, []) ]
+ end
+
+ def https_location_for(request)
+ host = @redirect[:host] || request.host
+ port = @redirect[:port] || request.port
+
+ location = "https://#{host}"
+ location << ":#{port}" if port != 80 && port != 443
+ location << request.fullpath
+ location
+ end
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/stack.rb b/actionpack/lib/action_dispatch/middleware/stack.rb
index bbf734f103..90e2ae6802 100644
--- a/actionpack/lib/action_dispatch/middleware/stack.rb
+++ b/actionpack/lib/action_dispatch/middleware/stack.rb
@@ -4,36 +4,15 @@ require "active_support/dependencies"
module ActionDispatch
class MiddlewareStack
class Middleware
- attr_reader :args, :block, :name, :classcache
+ attr_reader :args, :block, :klass
- def initialize(klass_or_name, *args, &block)
- @klass = nil
-
- if klass_or_name.respond_to?(:name)
- @klass = klass_or_name
- @name = @klass.name
- else
- @name = klass_or_name.to_s
- end
-
- @classcache = ActiveSupport::Dependencies::Reference
- @args, @block = args, block
+ def initialize(klass, args, block)
+ @klass = klass
+ @args = args
+ @block = block
end
- def klass
- @klass || classcache[@name]
- end
-
- def ==(middleware)
- case middleware
- when Middleware
- klass == middleware.klass
- when Class
- klass == middleware
- else
- normalize(@name) == normalize(middleware)
- end
- end
+ def name; klass.name; end
def inspect
klass.to_s
@@ -42,12 +21,6 @@ module ActionDispatch
def build(app)
klass.new(app, *args, &block)
end
-
- private
-
- def normalize(object)
- object.to_s.strip.sub(/^::/, '')
- end
end
include Enumerable
@@ -75,19 +48,17 @@ module ActionDispatch
middlewares[i]
end
- def unshift(*args, &block)
- middleware = self.class::Middleware.new(*args, &block)
- middlewares.unshift(middleware)
+ def unshift(klass, *args, &block)
+ middlewares.unshift(build_middleware(klass, args, block))
end
def initialize_copy(other)
self.middlewares = other.middlewares.dup
end
- def insert(index, *args, &block)
+ def insert(index, klass, *args, &block)
index = assert_index(index, :before)
- middleware = self.class::Middleware.new(*args, &block)
- middlewares.insert(index, middleware)
+ middlewares.insert(index, build_middleware(klass, args, block))
end
alias_method :insert_before, :insert
@@ -104,26 +75,46 @@ module ActionDispatch
end
def delete(target)
- middlewares.delete target
+ target = get_class target
+ middlewares.delete_if { |m| m.klass == target }
end
- def use(*args, &block)
- middleware = self.class::Middleware.new(*args, &block)
- middlewares.push(middleware)
+ def use(klass, *args, &block)
+ middlewares.push(build_middleware(klass, args, block))
end
- def build(app = nil, &block)
- app ||= block
- raise "MiddlewareStack#build requires an app" unless app
+ def build(app = Proc.new)
middlewares.freeze.reverse.inject(app) { |a, e| e.build(a) }
end
- protected
+ private
def assert_index(index, where)
- i = index.is_a?(Integer) ? index : middlewares.index(index)
+ 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
+ end
+ end
+
+ def build_middleware(klass, args, block)
+ Middleware.new(get_class(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 2764584fe9..75f8e05a3f 100644
--- a/actionpack/lib/action_dispatch/middleware/static.rb
+++ b/actionpack/lib/action_dispatch/middleware/static.rb
@@ -2,66 +2,135 @@ 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"]`
+ # 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
+ # does not exist, a 404 "File not Found" response will be returned.
class FileHandler
- def initialize(root, cache_control)
+ def initialize(root, index: 'index', headers: {})
@root = root.chomp('/')
@compiled_root = /^#{Regexp.escape(root)}/
- headers = cache_control && { 'Cache-Control' => cache_control }
@file_server = ::Rack::File.new(@root, headers)
+ @index = index
end
+ # Takes a path to a file. If the file is found, has valid encoding, and has
+ # 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).
def match?(path)
- path = unescape_path(path)
+ path = ::Rack::Utils.unescape_path path
return false unless path.valid_encoding?
+ path = Rack::Utils.clean_path_info path
- full_path = path.empty? ? @root : File.join(@root, escape_glob_chars(path))
- paths = "#{full_path}#{ext}"
+ paths = [path, "#{path}#{ext}", "#{path}/#{@index}#{ext}"]
- matches = Dir[paths]
- match = matches.detect { |m| File.file?(m) }
- if match
- match.sub!(@compiled_root, '')
- ::Rack::Utils.escape(match)
+ if match = paths.detect { |p|
+ path = File.join(@root, p.force_encoding('UTF-8'.freeze))
+ begin
+ File.file?(path) && File.readable?(path)
+ rescue SystemCallError
+ false
+ end
+
+ }
+ return ::Rack::Utils.escape_path(match)
end
end
def call(env)
- @file_server.call(env)
+ serve ActionDispatch::Request.new env
end
- def ext
- @ext ||= begin
- ext = ::ActionController::Base.default_static_extension
- "{,#{ext},/index#{ext}}"
+ def serve(request)
+ path = request.path_info
+ gzip_path = gzip_file_path(path)
+
+ if gzip_path && gzip_encoding_accepted?(request)
+ request.path_info = gzip_path
+ status, headers, body = @file_server.call(request.env)
+ if status == 304
+ return [status, headers, body]
+ end
+ headers['Content-Encoding'] = 'gzip'
+ headers['Content-Type'] = content_type(path)
+ else
+ status, headers, body = @file_server.call(request.env)
end
- end
- def unescape_path(path)
- URI.parser.unescape(path)
- end
+ headers['Vary'] = 'Accept-Encoding' if gzip_path
- def escape_glob_chars(path)
- path.gsub(/[*?{}\[\]]/, "\\\\\\&")
+ return [status, headers, body]
+ ensure
+ request.path_info = path
end
+
+ private
+ def ext
+ ::ActionController::Base.default_static_extension
+ end
+
+ def content_type(path)
+ ::Rack::Mime.mime_type(::File.extname(path), 'text/plain'.freeze)
+ end
+
+ def gzip_encoding_accepted?(request)
+ request.accept_encoding =~ /\bgzip\b/i
+ end
+
+ def gzip_file_path(path)
+ can_gzip_mime = content_type(path) =~ /\A(?:text\/|application\/javascript)/
+ gzip_path = "#{path}.gz"
+ if can_gzip_mime && File.exist?(File.join(@root, ::Rack::Utils.unescape_path(gzip_path)))
+ gzip_path
+ else
+ false
+ end
+ 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.
+ #
+ # 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, cache_control=nil)
+ 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
+
@app = app
- @file_handler = FileHandler.new(path, cache_control)
+ @file_handler = FileHandler.new(path, index: index, headers: headers)
end
def call(env)
- case env['REQUEST_METHOD']
- when 'GET', 'HEAD'
- path = env['PATH_INFO'].chomp('/')
+ req = ActionDispatch::Request.new env
+
+ if req.get? || req.head?
+ path = req.path_info.chomp('/'.freeze)
if match = @file_handler.match?(path)
- env["PATH_INFO"] = match
- return @file_handler.call(env)
+ req.path_info = match
+ return @file_handler.serve(req)
end
end
- @app.call(env)
+ @app.call(req.env)
end
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb
index db219c8fa9..49b1e83551 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb
@@ -5,20 +5,8 @@
<pre id="blame_trace" <%='style="display:none"' if hide %>><code><%= @exception.describe_blame %></code></pre>
<% end %>
-<%
- clean_params = @request.filtered_parameters.clone
- clean_params.delete("action")
- clean_params.delete("controller")
-
- request_dump = clean_params.empty? ? 'None' : clean_params.inspect.gsub(',', ",\n")
-
- def debug_hash(object)
- object.to_hash.sort_by { |k, _| k.to_s }.map { |k, v| "#{k}: #{v.inspect rescue $!.message}" }.join("\n")
- end unless self.class.method_defined?(:debug_hash)
-%>
-
<h2 style="margin-top: 30px">Request</h2>
-<p><b>Parameters</b>:</p> <pre><%= request_dump %></pre>
+<p><b>Parameters</b>:</p> <pre><%= debug_params(@request.filtered_parameters) %></pre>
<div class="details">
<div class="summary"><a href="#" onclick="return toggleSessionDump()">Toggle session dump</a></div>
@@ -31,4 +19,4 @@
</div>
<h2 style="margin-top: 30px">Response</h2>
-<p><b>Headers</b>:</p> <pre><%= defined?(@response) ? @response.headers.inspect.gsub(',', ",\n") : 'None' %></pre>
+<p><b>Headers</b>:</p> <pre><%= debug_headers(defined?(@response) ? @response.headers : {}) %></pre>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.erb
index 38429cb78e..e7b913bbe4 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.erb
@@ -1,25 +1,27 @@
-<% if @source_extract %>
-<div class="source">
-<div class="info">
- Extracted source (around line <strong>#<%= @line_number %></strong>):
-</div>
-<div class="data">
- <table cellpadding="0" cellspacing="0" class="lines">
- <tr>
- <td>
- <pre class="line_numbers">
- <% @source_extract.keys.each do |line_number| %>
+<% @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="info">
+ Extracted source (around line <strong>#<%= source_extract[:line_number] %></strong>):
+ </div>
+ <div class="data">
+ <table cellpadding="0" cellspacing="0" class="lines">
+ <tr>
+ <td>
+ <pre class="line_numbers">
+ <% source_extract[:code].each_key do |line_number| %>
<span><%= line_number -%></span>
- <% end %>
- </pre>
- </td>
+ <% end %>
+ </pre>
+ </td>
<td width="100%">
<pre>
-<% @source_extract.each do |line, source| -%><div class="line<%= " active" if line == @line_number -%>"><%= source -%></div><% end -%>
+<% source_extract[:code].each do |line, source| -%><div class="line<%= " active" if line == source_extract[:line_number] -%>"><%= source -%></div><% end -%>
</pre>
</td>
- </tr>
- </table>
-</div>
-</div>
+ </tr>
+ </table>
+ </div>
+ </div>
+ <% end %>
<% end %>
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 b181909bff..ab57b11c7d 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb
@@ -1,9 +1,4 @@
-<%
- traces = { "Application Trace" => @application_trace,
- "Framework Trace" => @framework_trace,
- "Full Trace" => @full_trace }
- names = traces.keys
-%>
+<% names = @traces.keys %>
<p><code>Rails.root: <%= defined?(Rails) && Rails.respond_to?(:root) ? Rails.root : "unset" %></code></p>
@@ -16,9 +11,42 @@
<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 == "Application Trace") ? 'block' : 'none' %>;">
- <pre><code><%= trace.join "\n" %></code></pre>
+ <% @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>
</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;
+ }
+ }
+ }
+ </script>
</div>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb
index d4af5c9b06..c0b53068f7 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb
@@ -1,15 +1,9 @@
-<%
- traces = { "Application Trace" => @application_trace,
- "Framework Trace" => @framework_trace,
- "Full Trace" => @full_trace }
-%>
-
Rails.root: <%= defined?(Rails) && Rails.respond_to?(:root) ? Rails.root : "unset" %>
-<% traces.each do |name, trace| %>
+<% @traces.each do |name, trace| %>
<% if trace.any? %>
<%= name %>
-<%= trace.join("\n") %>
+<%= trace.map { |t| t[:trace] }.join("\n") %>
<% end %>
<% end %>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb
index bc5d03dc10..e0509f56f4 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb
@@ -116,9 +116,15 @@
background-color: #FFCCCC;
}
+ .hidden {
+ display: none;
+ }
+
a { color: #980905; }
a:visited { color: #666; }
+ a.trace-frames { color: #666; }
a:hover { color: #C52F24; }
+ a.trace-frames.selected { color: #C52F24 }
<%= yield :style %>
</style>
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 5c016e544e..2a65fd06ad 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
@@ -4,4 +4,8 @@
<div id="container">
<h2><%= h @exception.message %></h2>
+
+ <%= render template: "rescues/_source" %>
+ <%= render template: "rescues/_trace" %>
+ <%= 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 7e9cedb95e..55dd5ddc7b 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
@@ -27,4 +27,6 @@
<%= @routes_inspector.format(ActionDispatch::Routing::HtmlTableFormatter.new(self)) %>
<% end %>
+
+ <%= render template: "rescues/_request_and_response" %>
</div>
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 027a0f5b3e..c1e8b6cae3 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
@@ -1,4 +1,3 @@
-<% @source_extract = @exception.source_extract(0, :html) %>
<header>
<h1>
<%= @exception.original_exception.class.to_s %> in
@@ -12,29 +11,7 @@
</p>
<pre><code><%= h @exception.message %></code></pre>
- <div class="source">
- <div class="info">
- <p>Extracted source (around line <strong>#<%= @exception.line_number %></strong>):</p>
- </div>
- <div class="data">
- <table cellpadding="0" cellspacing="0" class="lines">
- <tr>
- <td>
- <pre class="line_numbers">
- <% @source_extract.keys.each do |line_number| %>
-<span><%= line_number -%></span>
- <% end %>
- </pre>
- </td>
-<td width="100%">
-<pre>
-<% @source_extract.each do |line, source| -%><div class="line<%= " active" if line == @exception.line_number -%>"><%= source -%></div><% end -%>
-</pre>
-</td>
- </tr>
- </table>
-</div>
-</div>
+ <%= render template: "rescues/_source" %>
<p><%= @exception.sub_template_message %></p>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb
index 5da21d9784..77bcd26726 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb
@@ -1,4 +1,3 @@
-<% @source_extract = @exception.source_extract(0, :html) %>
<%= @exception.original_exception.class.to_s %> in <%= @request.parameters["controller"].camelize if @request.parameters["controller"] %>#<%= @request.parameters["action"] %>
Showing <%= @exception.file_name %> where line #<%= @exception.line_number %> raised:
diff --git a/actionpack/lib/action_dispatch/middleware/templates/routes/_route.html.erb b/actionpack/lib/action_dispatch/middleware/templates/routes/_route.html.erb
index 24e44f31ac..6e995c85c1 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/routes/_route.html.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/routes/_route.html.erb
@@ -4,13 +4,13 @@
<%= route[:name] %><span class='helper'>_path</span>
<% end %>
</td>
- <td data-route-verb='<%= route[:verb] %>'>
+ <td>
<%= route[:verb] %>
</td>
- <td data-route-path='<%= route[:path] %>' data-regexp='<%= route[:regexp] %>'>
+ <td data-route-path='<%= route[:path] %>'>
<%= route[:path] %>
</td>
- <td data-route-reqs='<%= route[:reqs] %>'>
- <%= route[:reqs] %>
+ <td>
+ <%=simple_format route[:reqs] %>
</td>
</tr>
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 6ffa242da4..429ea7057c 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb
@@ -1,6 +1,6 @@
<% content_for :style do %>
#route_table {
- margin: 0 auto 0;
+ margin: 0;
border-collapse: collapse;
}
@@ -81,92 +81,87 @@
</table>
<script type='text/javascript'>
- // Iterates each element through a function
- function each(elems, func) {
- if (!elems instanceof Array) { elems = [elems]; }
- for (var i = 0, len = elems.length; i < len; i++) {
- func(elems[i]);
- }
- }
-
- // Sets innerHTML for an element
- function setContent(elem, text) {
- elem.innerHTML = text;
- }
+ // support forEarch iterator on NodeList
+ NodeList.prototype.forEach = Array.prototype.forEach;
// Enables path search functionality
function setupMatchPaths() {
- // Check if the user input (sanitized as a path) matches the regexp data attribute
- function checkExactMatch(section, elem, value) {
- var string = sanitizePath(value),
- regexp = elem.getAttribute("data-regexp");
-
- showMatch(string, regexp, section, elem);
+ // Check if there are any matched results in a section
+ function checkNoMatch(section, noMatchText) {
+ if (section.children.length <= 1) {
+ section.innerHTML += noMatchText;
+ }
}
- // Check if the route path data attribute contains the user input
- function checkFuzzyMatch(section, elem, value) {
- var string = elem.getAttribute("data-route-path"),
- regexp = value;
-
- showMatch(string, regexp, section, elem);
+ // get JSON from url and invoke callback with result
+ function getJSON(url, success) {
+ var xhr = new XMLHttpRequest();
+ xhr.open('GET', url);
+ xhr.onload = function() {
+ if (this.status == 200)
+ success(JSON.parse(this.response));
+ };
+ xhr.send();
}
- // Display the parent <tr> element in the appropriate section when there's a match
- function showMatch(string, regexp, section, elem) {
- if(string.match(RegExp(regexp))) {
- section.appendChild(elem.parentNode.cloneNode(true));
+ function delayedKeyup(input, callback) {
+ var timeout;
+ input.onkeyup = function(){
+ if (timeout) clearTimeout(timeout);
+ timeout = setTimeout(callback, 300);
}
}
- // Check if there are any matched results in a section
- function checkNoMatch(section, defaultText, noMatchText) {
- if (section.innerHTML === defaultText) {
- setContent(section, defaultText + noMatchText);
- }
- }
-
- // Ensure path always starts with a slash "/" and remove params or fragments
+ // remove params or fragments
function sanitizePath(path) {
- var path = path.charAt(0) == '/' ? path : "/" + path;
- return path.replace(/\#.*|\?.*/, '');
+ return path.replace(/[#?].*/, '');
}
- var regexpElems = document.querySelectorAll('#route_table [data-regexp]'),
- searchElem = document.querySelector('#search'),
- exactMatches = document.querySelector('#exact_matches'),
- fuzzyMatches = document.querySelector('#fuzzy_matches');
+ var pathElements = document.querySelectorAll('#route_table [data-route-path]'),
+ searchElem = document.querySelector('#search'),
+ exactSection = document.querySelector('#exact_matches'),
+ fuzzySection = document.querySelector('#fuzzy_matches');
// Remove matches when no search value is present
searchElem.onblur = function(e) {
if (searchElem.value === "") {
- setContent(exactMatches, "");
- setContent(fuzzyMatches, "");
+ exactSection.innerHTML = "";
+ fuzzySection.innerHTML = "";
}
}
// On key press perform a search for matching paths
- searchElem.onkeyup = function(e){
- var userInput = searchElem.value,
- defaultExactMatch = '<tr><th colspan="4">Paths Matching (' + escape(sanitizePath(userInput)) +'):</th></tr>',
- defaultFuzzyMatch = '<tr><th colspan="4">Paths Containing (' + escape(userInput) +'):</th></tr>',
+ delayedKeyup(searchElem, function() {
+ var path = sanitizePath(searchElem.value),
+ defaultExactMatch = '<tr><th colspan="4">Paths Matching (' + path +'):</th></tr>',
+ defaultFuzzyMatch = '<tr><th colspan="4">Paths Containing (' + path +'):</th></tr>',
noExactMatch = '<tr><th colspan="4">No Exact Matches Found</th></tr>',
noFuzzyMatch = '<tr><th colspan="4">No Fuzzy Matches Found</th></tr>';
- // Clear out results section
- setContent(exactMatches, defaultExactMatch);
- setContent(fuzzyMatches, defaultFuzzyMatch);
+ if (!path)
+ return searchElem.onblur();
- // Display exact matches and fuzzy matches
- each(regexpElems, function(elem) {
- checkExactMatch(exactMatches, elem, userInput);
- checkFuzzyMatch(fuzzyMatches, elem, userInput);
- })
+ getJSON('/rails/info/routes?path=' + path, function(matches){
+ // Clear out results section
+ exactSection.innerHTML = defaultExactMatch;
+ fuzzySection.innerHTML = defaultFuzzyMatch;
- // Display 'No Matches' message when no matches are found
- checkNoMatch(exactMatches, defaultExactMatch, noExactMatch);
- checkNoMatch(fuzzyMatches, defaultFuzzyMatch, noFuzzyMatch);
- }
+ // Display exact matches and fuzzy matches
+ pathElements.forEach(function(elem) {
+ var elemPath = elem.getAttribute('data-route-path');
+
+ if (matches['exact'].indexOf(elemPath) != -1)
+ exactSection.appendChild(elem.parentNode.cloneNode(true));
+
+ if (matches['fuzzy'].indexOf(elemPath) != -1)
+ fuzzySection.appendChild(elem.parentNode.cloneNode(true));
+ })
+
+ // Display 'No Matches' message when no matches are found
+ checkNoMatch(exactSection, noExactMatch);
+ checkNoMatch(fuzzySection, noFuzzyMatch);
+ })
+ })
}
// Enables functionality to toggle between `_path` and `_url` helper suffixes
@@ -174,19 +169,20 @@
// Sets content for each element
function setValOn(elems, val) {
- each(elems, function(elem) {
- setContent(elem, val);
+ elems.forEach(function(elem) {
+ elem.innerHTML = val;
});
}
// Sets onClick event for each element
function onClick(elems, func) {
- each(elems, function(elem) {
+ elems.forEach(function(elem) {
elem.onclick = func;
});
}
var toggleLinks = document.querySelectorAll('#route_table [data-route-helper]');
+
onClick(toggleLinks, function(){
var helperTxt = this.getAttribute("data-route-helper"),
helperElems = document.querySelectorAll('[data-route-name] span.helper');
diff --git a/actionpack/lib/action_dispatch/request/session.rb b/actionpack/lib/action_dispatch/request/session.rb
index 973627f106..9e7fcbd849 100644
--- a/actionpack/lib/action_dispatch/request/session.rb
+++ b/actionpack/lib/action_dispatch/request/session.rb
@@ -1,56 +1,56 @@
require 'rack/session/abstract/id'
module ActionDispatch
- class Request < Rack::Request
+ class Request
# Session is responsible for lazily loading the session from store.
class Session # :nodoc:
- ENV_SESSION_KEY = Rack::Session::Abstract::ENV_SESSION_KEY # :nodoc:
- ENV_SESSION_OPTIONS_KEY = Rack::Session::Abstract::ENV_SESSION_OPTIONS_KEY # :nodoc:
+ 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
Unspecified = Object.new
-
- def self.create(store, env, default_options)
- session_was = find env
- session = Request::Session.new(store, env)
+
+ # 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)
session.merge! session_was if session_was
- set(env, session)
- Options.set(env, Request::Session::Options.new(store, env, default_options))
+ set(req, session)
+ Options.set(req, Request::Session::Options.new(store, default_options))
session
end
- def self.find(env)
- env[ENV_SESSION_KEY]
+ def self.find(req)
+ req.get_header ENV_SESSION_KEY
end
- def self.set(env, session)
- env[ENV_SESSION_KEY] = session
+ def self.set(req, session)
+ req.set_header ENV_SESSION_KEY, session
end
class Options #:nodoc:
- def self.set(env, options)
- env[ENV_SESSION_OPTIONS_KEY] = options
+ def self.set(req, options)
+ req.set_header ENV_SESSION_OPTIONS_KEY, options
end
- def self.find(env)
- env[ENV_SESSION_OPTIONS_KEY]
+ def self.find(req)
+ req.get_header ENV_SESSION_OPTIONS_KEY
end
- def initialize(by, env, default_options)
+ def initialize(by, default_options)
@by = by
- @env = env
@delegate = default_options.dup
end
def [](key)
- if key == :id
- @delegate.fetch(key) {
- @delegate[:id] = @by.send(:extract_session_id, @env)
- }
- else
- @delegate[key]
- end
+ @delegate[key]
+ end
+
+ def id(req)
+ @delegate.fetch(:id) {
+ @by.send(:extract_session_id, req)
+ }
end
def []=(k,v); @delegate[k] = v; end
@@ -58,38 +58,40 @@ module ActionDispatch
def values_at(*args); @delegate.values_at(*args); end
end
- def initialize(by, env)
+ def initialize(by, req)
@by = by
- @env = env
+ @req = req
@delegate = {}
@loaded = false
@exists = nil # we haven't checked yet
end
def id
- options[:id]
+ options.id(@req)
end
def options
- Options.find @env
+ Options.find @req
end
def destroy
clear
options = self.options || {}
- new_sid = @by.send(:destroy_session, @env, options[:id], options)
- options[:id] = new_sid # Reset session id with a new value or nil
+ @by.send(:delete_session, @req, options.id(@req), options)
# Load the new sid to be written with the response
@loaded = false
load_for_write!
end
+ # Returns value of the key stored in the session or
+ # nil if the given key is not found in the session.
def [](key)
load_for_read!
@delegate[key.to_s]
end
+ # Returns true if the session has the given key or false.
def has_key?(key)
load_for_read!
@delegate.key?(key.to_s)
@@ -97,39 +99,69 @@ module ActionDispatch
alias :key? :has_key?
alias :include? :has_key?
+ # Returns keys of the session as Array.
def keys
@delegate.keys
end
+ # Returns values of the session as Array.
def values
@delegate.values
end
+ # Writes given value to given key of the session.
def []=(key, value)
load_for_write!
@delegate[key.to_s] = value
end
+ # Clears the session.
def clear
load_for_write!
@delegate.clear
end
+ # Returns the session as Hash.
def to_hash
load_for_read!
@delegate.dup.delete_if { |_,v| v.nil? }
end
+ # Updates the session with given Hash.
+ #
+ # session.to_hash
+ # # => {"session_id"=>"e29b9ea315edf98aad94cc78c34cc9b2"}
+ #
+ # session.update({ "foo" => "bar" })
+ # # => {"session_id"=>"e29b9ea315edf98aad94cc78c34cc9b2", "foo" => "bar"}
+ #
+ # session.to_hash
+ # # => {"session_id"=>"e29b9ea315edf98aad94cc78c34cc9b2", "foo" => "bar"}
def update(hash)
load_for_write!
@delegate.update stringify_keys(hash)
end
+ # Deletes given key from the session.
def delete(key)
load_for_write!
@delegate.delete key.to_s
end
+ # Returns value of given key from the session, or raises +KeyError+
+ # if can't find given key in case of not setted dafault value.
+ # Returns default value if specified.
+ #
+ # session.fetch(:foo)
+ # # => KeyError: key not found: "foo"
+ #
+ # session.fetch(:foo, :bar)
+ # # => :bar
+ #
+ # session.fetch(:foo) do
+ # :bar
+ # end
+ # # => :bar
def fetch(key, default=Unspecified, &block)
load_for_read!
if default == Unspecified
@@ -149,7 +181,7 @@ module ActionDispatch
def exists?
return @exists unless @exists.nil?
- @exists = @by.send(:session_exists?, @env)
+ @exists = @by.send(:session_exists?, @req)
end
def loaded?
@@ -177,7 +209,7 @@ module ActionDispatch
end
def load!
- id, session = @by.load_session @env
+ id, session = @by.load_session @req
options[:id] = id
@delegate.replace(stringify_keys(session))
@loaded = true
diff --git a/actionpack/lib/action_dispatch/request/utils.rb b/actionpack/lib/action_dispatch/request/utils.rb
index 9d4f1aa3c5..bb3df3c311 100644
--- a/actionpack/lib/action_dispatch/request/utils.rb
+++ b/actionpack/lib/action_dispatch/request/utils.rb
@@ -1,32 +1,64 @@
module ActionDispatch
- class Request < Rack::Request
+ class Request
class Utils # :nodoc:
mattr_accessor :perform_deep_munge
self.perform_deep_munge = true
- class << self
- # Remove nils from the params hash
- def deep_munge(hash, keys = [])
- return hash unless perform_deep_munge
+ def self.normalize_encode_params(params)
+ if perform_deep_munge
+ NoNilParamEncoder.normalize_encode_params params
+ else
+ ParamEncoder.normalize_encode_params params
+ end
+ end
+
+ def self.check_param_encoding(params)
+ case params
+ when Array
+ params.each { |element| check_param_encoding(element) }
+ when Hash
+ params.each_value { |value| check_param_encoding(value) }
+ when String
+ unless params.valid_encoding?
+ # Raise Rack::Utils::InvalidParameterError for consistency with Rack.
+ # ActionDispatch::Request#GET will re-raise as a BadRequest error.
+ raise Rack::Utils::InvalidParameterError, "Non UTF-8 value: #{params}"
+ end
+ end
+ end
- hash.each do |k, v|
- keys << k
- case v
- when Array
- v.grep(Hash) { |x| deep_munge(x, keys) }
- v.compact!
- if v.empty?
- hash[k] = nil
- ActiveSupport::Notifications.instrument("deep_munge.action_controller", keys: keys)
- end
- when Hash
- deep_munge(v, keys)
+ class ParamEncoder # :nodoc:
+ # Convert nested Hash to HashWithIndifferentAccess.
+ #
+ def self.normalize_encode_params(params)
+ case params
+ when Array
+ handle_array params
+ when Hash
+ if params.has_key?(:tempfile)
+ ActionDispatch::Http::UploadedFile.new(params)
+ else
+ params.each_with_object({}) do |(key, val), new_hash|
+ new_hash[key] = normalize_encode_params(val)
+ end.with_indifferent_access
end
- keys.pop
+ else
+ params
end
+ end
+
+ def self.handle_array(params)
+ params.map! { |el| normalize_encode_params(el) }
+ end
+ end
- hash
+ # Remove nils from the params hash
+ class NoNilParamEncoder < ParamEncoder # :nodoc:
+ def self.handle_array(params)
+ list = super
+ list.compact!
+ list
end
end
end
diff --git a/actionpack/lib/action_dispatch/routing.rb b/actionpack/lib/action_dispatch/routing.rb
index ce03164ca9..59c3f9248f 100644
--- a/actionpack/lib/action_dispatch/routing.rb
+++ b/actionpack/lib/action_dispatch/routing.rb
@@ -1,8 +1,3 @@
-# encoding: UTF-8
-require 'active_support/core_ext/object/to_param'
-require 'active_support/core_ext/regexp'
-require 'active_support/dependencies/autoload'
-
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
@@ -58,7 +53,7 @@ module ActionDispatch
# resources :posts, :comments
# end
#
- # Alternately, you can add prefixes to your path without using a separate
+ # Alternatively, you can add prefixes to your path without using a separate
# directory by using +scope+. +scope+ takes additional options which
# apply to all enclosed routes.
#
@@ -151,6 +146,7 @@ module ActionDispatch
# get 'geocode/:postalcode' => :show, constraints: {
# postalcode: /\d{5}(-\d{4})?/
# }
+ # end
#
# Constraints can include the 'ignorecase' and 'extended syntax' regular
# expression modifiers:
@@ -232,7 +228,6 @@ module ActionDispatch
# def send_to_jail
# get '/jail'
# assert_response :success
- # assert_template "jail/front"
# end
#
# def goes_to_login
diff --git a/actionpack/lib/action_dispatch/routing/inspector.rb b/actionpack/lib/action_dispatch/routing/inspector.rb
index ea3b2f419d..f3a5268d2e 100644
--- a/actionpack/lib/action_dispatch/routing/inspector.rb
+++ b/actionpack/lib/action_dispatch/routing/inspector.rb
@@ -16,10 +16,6 @@ module ActionDispatch
app.app
end
- def verb
- super.source.gsub(/[$^]/, '')
- end
-
def path
super.spec.to_s
end
@@ -28,27 +24,10 @@ module ActionDispatch
super.to_s
end
- def regexp
- __getobj__.path.to_regexp
- end
-
- def json_regexp
- str = regexp.inspect.
- sub('\\A' , '^').
- sub('\\Z' , '$').
- sub('\\z' , '$').
- sub(/^\// , '').
- sub(/\/[a-z]*$/ , '').
- gsub(/\(\?#.+\)/ , '').
- gsub(/\(\?-\w+:/ , '(').
- gsub(/\s/ , '')
- Regexp.new(str).source
- end
-
def reqs
@reqs ||= begin
reqs = endpoint
- reqs += " #{constraints.to_s}" unless constraints.empty?
+ reqs += " #{constraints}" unless constraints.empty?
reqs
end
end
@@ -62,7 +41,7 @@ module ActionDispatch
end
def internal?
- controller.to_s =~ %r{\Arails/(info|mailers|welcome)} || path =~ %r{\A#{Rails.application.config.assets.prefix}\z}
+ controller.to_s =~ %r{\Arails/(info|mailers|welcome)}
end
def engine?
@@ -114,16 +93,13 @@ module ActionDispatch
def collect_routes(routes)
routes.collect do |route|
RouteWrapper.new(route)
- end.reject do |route|
- route.internal?
- end.collect do |route|
+ end.reject(&:internal?).collect do |route|
collect_engine_routes(route)
- { name: route.name,
- verb: route.verb,
- path: route.path,
- reqs: route.reqs,
- regexp: route.json_regexp }
+ { name: route.name,
+ verb: route.verb,
+ path: route.path,
+ reqs: route.reqs }
end
end
diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb
index 6f7e6f3128..7c0404ca62 100644
--- a/actionpack/lib/action_dispatch/routing/mapper.rb
+++ b/actionpack/lib/action_dispatch/routing/mapper.rb
@@ -1,13 +1,10 @@
-require 'active_support/core_ext/hash/except'
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/module/remove_method'
-require 'active_support/inflector'
+require 'active_support/core_ext/regexp'
require 'action_dispatch/routing/redirection'
require 'action_dispatch/routing/endpoint'
-require 'active_support/deprecation'
module ActionDispatch
module Routing
@@ -17,7 +14,10 @@ module ActionDispatch
class Constraints < Endpoint #:nodoc:
attr_reader :app, :constraints
- def initialize(app, constraints, dispatcher_p)
+ SERVE = ->(app, req) { app.serve req }
+ CALL = ->(app, req) { app.call req.env }
+
+ def initialize(app, constraints, strategy)
# Unwrap Constraints objects. I don't actually think it's possible
# to pass a Constraints object to this constructor, but there were
# multiple places that kept testing children of this object. I
@@ -27,12 +27,12 @@ module ActionDispatch
app = app.app
end
- @dispatcher = dispatcher_p
+ @strategy = strategy
@app, @constraints, = app, constraints
end
- def dispatcher?; @dispatcher; end
+ def dispatcher?; @strategy == SERVE; end
def matches?(req)
@constraints.all? do |constraint|
@@ -44,11 +44,7 @@ module ActionDispatch
def serve(req)
return [ 404, {'X-Cascade' => 'pass'}, [] ] unless matches?(req)
- if dispatcher?
- @app.serve req
- else
- @app.call req.env
- end
+ @strategy.call @app, req
end
private
@@ -60,101 +56,168 @@ module ActionDispatch
class Mapping #:nodoc:
ANCHOR_CHARACTERS_REGEX = %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z}
- attr_reader :requirements, :conditions, :defaults
- attr_reader :to, :default_controller, :default_action, :as, :anchor
+ attr_reader :requirements, :defaults
+ attr_reader :to, :default_controller, :default_action
+ attr_reader :required_defaults, :ast
- def self.build(scope, set, path, options)
+ def self.build(scope, set, ast, controller, default_action, to, via, formatted, options_constraints, anchor, options)
options = scope[:options].merge(options) if scope[:options]
- options.delete :only
- options.delete :except
- options.delete :shallow_path
- options.delete :shallow_prefix
- options.delete :shallow
+ defaults = (scope[:defaults] || {}).dup
+ scope_constraints = scope[:constraints] || {}
- defaults = (scope[:defaults] || {}).merge options.delete(:defaults) || {}
+ new set, ast, defaults, controller, default_action, scope[:module], to, formatted, scope_constraints, scope[:blocks] || [], via, options_constraints, anchor, options
+ end
- new scope, set, path, defaults, options
+ def self.check_via(via)
+ if via.empty?
+ msg = "You should not use the `match` method in your router without specifying an HTTP method.\n" \
+ "If you want to expose your action to both GET and POST, add `via: [:get, :post]` option.\n" \
+ "If you want to expose your action to GET, use `get` in the router:\n" \
+ " Instead of: match \"controller#action\"\n" \
+ " Do: get \"controller#action\""
+ raise ArgumentError, msg
+ end
+ via
end
- def initialize(scope, set, path, defaults, options)
- @requirements, @conditions = {}, {}
- @defaults = defaults
- @set = set
+ def self.normalize_path(path, format)
+ path = Mapper.normalize_path(path)
- @to = options.delete :to
- @default_controller = options.delete(:controller) || scope[:controller]
- @default_action = options.delete(:action) || scope[:action]
- @as = options.delete :as
- @anchor = options.delete :anchor
+ if format == true
+ "#{path}.:format"
+ elsif optional_format?(path, format)
+ "#{path}(.:format)"
+ else
+ path
+ end
+ end
- formatted = options.delete :format
- via = Array(options.delete(:via) { [] })
- options_constraints = options.delete :constraints
+ def self.optional_format?(path, format)
+ format != false && !path.include?(':format') && !path.end_with?('/')
+ end
- path = normalize_path! path, formatted
- ast = path_ast path
- path_params = path_params ast
+ def initialize(set, ast, defaults, controller, default_action, modyoule, to, formatted, scope_constraints, blocks, via, options_constraints, anchor, options)
+ @defaults = defaults
+ @set = set
- options = normalize_options!(options, formatted, path_params, ast, scope[:module])
+ @to = to
+ @default_controller = controller
+ @default_action = default_action
+ @ast = ast
+ @anchor = anchor
+ @via = via
+ path_params = ast.find_all(&:symbol?).map(&:to_sym)
- split_constraints(path_params, scope[:constraints]) if scope[:constraints]
- constraints = constraints(options, path_params)
+ options = add_wildcard_options(options, formatted, ast)
- split_constraints path_params, constraints
+ options = normalize_options!(options, path_params, modyoule)
- @blocks = blocks(options_constraints, scope[:blocks])
+ split_options = constraints(options, path_params)
+
+ constraints = scope_constraints.merge Hash[split_options[:constraints] || []]
if options_constraints.is_a?(Hash)
- split_constraints path_params, options_constraints
- options_constraints.each do |key, default|
- if URL_OPTIONS.include?(key) && (String === default || Fixnum === default)
- @defaults[key] ||= default
- end
- end
+ @defaults = Hash[options_constraints.find_all { |key, default|
+ URL_OPTIONS.include?(key) && (String === default || Fixnum === default)
+ }].merge @defaults
+ @blocks = blocks
+ constraints.merge! options_constraints
+ else
+ @blocks = blocks(options_constraints)
end
- normalize_format!(formatted)
+ requirements, conditions = split_constraints path_params, constraints
+ verify_regexp_requirements requirements.map(&:last).grep(Regexp)
+
+ formats = normalize_format(formatted)
- @conditions[:path_info] = path
- @conditions[:parsed_path_info] = ast
+ @requirements = formats[:requirements].merge Hash[requirements]
+ @conditions = Hash[conditions]
+ @defaults = formats[:defaults].merge(@defaults).merge(normalize_defaults(options))
- add_request_method(via, @conditions)
- normalize_defaults!(options)
+ @required_defaults = (split_options[:required_defaults] || []).map(&:first)
end
- def to_route
- [ app(@blocks), conditions, requirements, defaults, as, anchor ]
+ def make_route(name, precedence)
+ route = Journey::Route.new(name,
+ application,
+ path,
+ conditions,
+ required_defaults,
+ defaults,
+ request_method,
+ precedence)
+
+ route
end
- private
+ def application
+ app(@blocks)
+ end
- def normalize_path!(path, format)
- path = Mapper.normalize_path(path)
+ def path
+ build_path @ast, requirements, @anchor
+ end
- if format == true
- "#{path}.:format"
- elsif optional_format?(path, format)
- "#{path}(.:format)"
- else
- path
- end
- end
+ def conditions
+ build_conditions @conditions, @set.request_class
+ end
- def optional_format?(path, format)
- format != false && !path.include?(':format') && !path.end_with?('/')
+ def build_conditions(current_conditions, request_class)
+ conditions = current_conditions.dup
+
+ conditions.keep_if do |k, _|
+ request_class.public_method_defined?(k)
end
+ end
+ private :build_conditions
+
+ def request_method
+ @via.map { |x| Journey::Route.verb_matcher(x) }
+ end
+ private :request_method
+
+ JOINED_SEPARATORS = SEPARATORS.join # :nodoc:
+
+ def build_path(ast, requirements, anchor)
+ pattern = Journey::Path::Pattern.new(ast, requirements, JOINED_SEPARATORS, anchor)
+
+ # Get all the symbol nodes followed by literals that are not the
+ # dummy node.
+ symbols = ast.find_all { |n|
+ n.cat? && n.left.symbol? && n.right.cat? && n.right.left.literal?
+ }.map(&:left)
+
+ # Get all the symbol nodes preceded by literals.
+ symbols.concat ast.find_all { |n|
+ n.cat? && n.left.literal? && n.right.cat? && n.right.left.symbol?
+ }.map { |n| n.right.left }
+
+ symbols.each { |x|
+ x.regexp = /(?:#{Regexp.union(x.regexp, '-')})+/
+ }
+
+ pattern
+ end
+ private :build_path
- def normalize_options!(options, formatted, path_params, path_ast, modyoule)
+
+ 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
if formatted != false
- path_ast.grep(Journey::Nodes::Star) do |node|
- options[node.name.to_sym] ||= /.+?/
- end
+ path_ast.grep(Journey::Nodes::Star).each_with_object({}) { |node, hash|
+ hash[node.name.to_sym] ||= /.+?/
+ }.merge options
+ else
+ options
end
+ end
+ def normalize_options!(options, path_params, modyoule)
if path_params.include?(:controller)
raise ArgumentError, ":controller segment is not allowed within a namespace block" if modyoule
@@ -179,77 +242,53 @@ module ActionDispatch
end
def split_constraints(path_params, constraints)
- constraints.each_pair do |key, requirement|
- if path_params.include?(key) || key == :controller
- verify_regexp_requirement(requirement) if requirement.is_a?(Regexp)
- @requirements[key] = requirement
- else
- @conditions[key] = requirement
- end
- end
- end
-
- def normalize_format!(formatted)
- if formatted == true
- @requirements[:format] ||= /.+/
- elsif Regexp === formatted
- @requirements[:format] = formatted
- @defaults[:format] = nil
- elsif String === formatted
- @requirements[:format] = Regexp.compile(formatted)
- @defaults[:format] = formatted
+ constraints.partition do |key, requirement|
+ path_params.include?(key) || key == :controller
end
end
- def verify_regexp_requirement(requirement)
- if requirement.source =~ ANCHOR_CHARACTERS_REGEX
- raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}"
- end
-
- if requirement.multiline?
- raise ArgumentError, "Regexp multiline option is not allowed in routing requirements: #{requirement.inspect}"
+ def normalize_format(formatted)
+ case formatted
+ when true
+ { requirements: { format: /.+/ },
+ defaults: {} }
+ when Regexp
+ { requirements: { format: formatted },
+ defaults: { format: nil } }
+ when String
+ { requirements: { format: Regexp.compile(formatted) },
+ defaults: { format: formatted } }
+ else
+ { requirements: { }, defaults: { } }
end
end
- def normalize_defaults!(options)
- options.each_pair do |key, default|
- unless Regexp === default
- @defaults[key] = default
+ def verify_regexp_requirements(requirements)
+ requirements.each do |requirement|
+ if requirement.source =~ ANCHOR_CHARACTERS_REGEX
+ raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}"
end
- end
- end
- def verify_callable_constraint(callable_constraint)
- unless callable_constraint.respond_to?(:call) || callable_constraint.respond_to?(:matches?)
- raise ArgumentError, "Invalid constraint: #{callable_constraint.inspect} must respond to :call or :matches?"
+ if requirement.multiline?
+ raise ArgumentError, "Regexp multiline option is not allowed in routing requirements: #{requirement.inspect}"
+ end
end
end
- def add_request_method(via, conditions)
- return if via == [:all]
-
- if via.empty?
- msg = "You should not use the `match` method in your router without specifying an HTTP method.\n" \
- "If you want to expose your action to both GET and POST, add `via: [:get, :post]` option.\n" \
- "If you want to expose your action to GET, use `get` in the router:\n" \
- " Instead of: match \"controller#action\"\n" \
- " Do: get \"controller#action\""
- raise ArgumentError, msg
- end
-
- conditions[:request_method] = via.map { |m| m.to_s.dasherize.upcase }
+ def normalize_defaults(options)
+ Hash[options.reject { |_, default| Regexp === default }]
end
def app(blocks)
- return to if Redirect === to
-
- if to.respond_to?(:call)
- Constraints.new(to, blocks, false)
+ if to.is_a?(Class) && to < ActionController::Metal
+ Routing::RouteSet::StaticDispatcher.new to
else
- if blocks.any?
- Constraints.new(dispatcher(defaults), blocks, true)
+ 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)
+ dispatcher(defaults.key?(:controller))
end
end
end
@@ -282,14 +321,8 @@ module ActionDispatch
end
def split_to(to)
- case to
- when Symbol
- ActiveSupport::Deprecation.warn "defining a route where `to` is a symbol is deprecated. Please change \"to: :#{to}\" to \"action: :#{to}\""
- [nil, to.to_s]
- when /#/ then to.split('#')
- when String
- ActiveSupport::Deprecation.warn "defining a route where `to` is a controller without an action is deprecated. Please change \"to: :#{to}\" to \"controller: :#{to}\""
- [to, nil]
+ if to =~ /#/
+ to.split('#')
else
[]
end
@@ -314,40 +347,29 @@ module ActionDispatch
yield
end
- def blocks(options_constraints, scope_blocks)
- if options_constraints && !options_constraints.is_a?(Hash)
- verify_callable_constraint(options_constraints)
- [options_constraints]
- else
- scope_blocks || []
+ def blocks(callable_constraint)
+ unless callable_constraint.respond_to?(:call) || callable_constraint.respond_to?(:matches?)
+ raise ArgumentError, "Invalid constraint: #{callable_constraint.inspect} must respond to :call or :matches?"
end
+ [callable_constraint]
end
def constraints(options, path_params)
- constraints = {}
- required_defaults = []
- options.each_pair do |key, option|
+ options.group_by do |key, option|
if Regexp === option
- constraints[key] = option
+ :constraints
else
- required_defaults << key unless path_params.include?(key)
+ if path_params.include?(key)
+ :path_params
+ else
+ :required_defaults
+ end
end
end
- @conditions[:required_defaults] = required_defaults
- constraints
end
- def path_params(ast)
- ast.grep(Journey::Nodes::Symbol).map { |n| n.name.to_sym }
- end
-
- def path_ast(path)
- parser = Journey::Parser.new
- parser.parse path
- end
-
- def dispatcher(defaults)
- @set.dispatcher defaults
+ def dispatcher(raise_on_name_error)
+ Routing::RouteSet::Dispatcher.new raise_on_name_error
end
end
@@ -379,12 +401,13 @@ module ActionDispatch
# because this means it will be matched first. As this is the most popular route
# of most Rails applications, this is beneficial.
def root(options = {})
- match '/', { :as => :root, :via => :get }.merge!(options)
+ name = has_named_route?(:root) ? nil : :root
+ match '/', { as: name, via: :get }.merge!(options)
end
# Matches a url pattern to one or more routes.
#
- # You should not use the `match` method in your router
+ # You should not use the +match+ method in your router
# without specifying an HTTP method.
#
# If you want to expose your action to both GET and POST, use:
@@ -395,7 +418,7 @@ module ActionDispatch
# 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:
+ # If you want to expose your action to GET, use +get+ in the router:
#
# Instead of:
#
@@ -429,14 +452,14 @@ module ActionDispatch
# A pattern can also point to a +Rack+ endpoint i.e. anything that
# responds to +call+:
#
- # match 'photos/:id', to: lambda {|hash| [200, {}, ["Coming soon"]] }, via: :get
+ # match 'photos/:id', to: -> (hash) { [200, {}, ["Coming soon"]] }, via: :get
# match 'photos/:id', to: PhotoRackApp, via: :get
# # Yes, controller actions are just rack endpoints
# match 'photos/:id', to: PhotosController.action(:show), via: :get
#
# Because requesting various HTTP verbs with a single action has security
# implications, you must either specify the actions in
- # the via options or use one of the HtttpHelpers[rdoc-ref:HttpHelpers]
+ # the via options or use one of the HttpHelpers[rdoc-ref:HttpHelpers]
# instead +match+
#
# === Options
@@ -450,10 +473,25 @@ module ActionDispatch
# The route's action.
#
# [:param]
- # Overrides the default resource identifier `:id` (name of the
+ # Overrides the default resource identifier +:id+ (name of the
# dynamic segment used to generate the routes).
# You can access that segment from your controller using
# <tt>params[<:param>]</tt>.
+ # In your router:
+ #
+ # resources :user, param: :name
+ #
+ # You can override <tt>ActiveRecord::Base#to_param</tt> of a related
+ # model to construct a URL:
+ #
+ # class User < ActiveRecord::Base
+ # def to_param
+ # name
+ # end
+ # end
+ #
+ # user = User.find_by(name: 'Phusion')
+ # user_path(user) # => "/users/Phusion"
#
# [:path]
# The path prefix for the routes.
@@ -481,7 +519,7 @@ module ActionDispatch
# +call+ or a string representing a controller's action.
#
# match 'path', to: 'controller#action', via: :get
- # match 'path', to: lambda { |env| [200, {}, ["Success!"]] }, via: :get
+ # match 'path', to: -> (env) { [200, {}, ["Success!"]] }, via: :get
# match 'path', to: RackApp, via: :get
#
# [:on]
@@ -575,13 +613,7 @@ module ActionDispatch
raise "A rack application must be specified" unless path
rails_app = rails_app? app
-
- if rails_app
- options[:as] ||= app.railtie_name
- else
- # non rails apps can't have an :as
- options[:as] = nil
- end
+ options[:as] ||= app_name(app, rails_app)
target_as = name_for_action(options[:as], path)
options[:via] ||= :all
@@ -605,7 +637,7 @@ module ActionDispatch
# Query if the following named route was already defined.
def has_named_route?(name)
- @set.named_routes.routes[name.to_sym]
+ @set.named_routes.key? name
end
private
@@ -613,18 +645,30 @@ module ActionDispatch
app.is_a?(Class) && app < Rails::Railtie
end
+ def app_name(app, rails_app)
+ if rails_app
+ app.railtie_name
+ elsif app.is_a?(Class)
+ class_name = app.name
+ ActiveSupport::Inflector.underscore(class_name).tr("/", "_")
+ end
+ end
+
def define_generate_prefix(app, name)
- _route = @set.named_routes.routes[name.to_sym]
+ _route = @set.named_routes.get name
_routes = @set
app.routes.define_mounted_helper(name)
app.routes.extend Module.new {
def optimize_routes_generation?; false; end
define_method :find_script_name do |options|
- super(options) || begin
- prefix_options = options.slice(*_route.segment_keys)
- # 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)
+ 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)
end
end
}
@@ -676,7 +720,11 @@ module ActionDispatch
def map_method(method, args, &block)
options = args.extract_options!
options[:via] = method
- match(*args, options, &block)
+ if options.key?(:defaults)
+ defaults(options.delete(:defaults)) { match(*args, options, &block) }
+ else
+ match(*args, options, &block)
+ end
self
end
end
@@ -779,8 +827,8 @@ module ActionDispatch
end
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))
+ defaults = options[:constraints].select do |k, v|
+ URL_OPTIONS.include?(k) && (v.is_a?(String) || v.is_a?(Fixnum))
end
(options[:defaults] ||= {}).reverse_merge!(defaults)
@@ -788,16 +836,25 @@ module ActionDispatch
block, options[:constraints] = options[:constraints], {}
end
+ if options.key?(:only) || options.key?(:except)
+ scope[:action_options] = { only: options.delete(:only),
+ except: options.delete(:except) }
+ end
+
+ if options.key? :anchor
+ raise ArgumentError, 'anchor is ignored unless passed to `match`'
+ end
+
@scope.options.each do |option|
if option == :blocks
value = block
elsif option == :options
value = options
else
- value = options.delete(option)
+ value = options.delete(option) { POISON }
end
- if value
+ unless POISON == value
scope[option] = send("merge_#{option}_scope", @scope[option], value)
end
end
@@ -809,14 +866,18 @@ module ActionDispatch
@scope = @scope.parent
end
+ POISON = Object.new # :nodoc:
+
# Scopes routes to a specific controller
#
# controller "food" do
- # match "bacon", action: "bacon"
+ # match "bacon", action: :bacon, via: :get
# end
- def controller(controller, options={})
- options[:controller] = controller
- scope(options) { yield }
+ def controller(controller)
+ @scope = @scope.new(controller: controller)
+ yield
+ ensure
+ @scope = @scope.parent
end
# Scopes routes to a specific namespace. For example:
@@ -862,13 +923,14 @@ module ActionDispatch
defaults = {
module: path,
- path: options.fetch(:path, path),
as: options.fetch(:as, path),
shallow_path: options.fetch(:path, path),
shallow_prefix: options.fetch(:as, path)
}
- scope(defaults.merge!(options)) { yield }
+ path_scope(options.delete(:path) { path }) do
+ scope(defaults.merge!(options)) { yield }
+ end
end
# === Parameter Restriction
@@ -905,7 +967,7 @@ module ActionDispatch
#
# Requests to routes can be constrained based on specific criteria:
#
- # constraints(lambda { |req| req.env["HTTP_USER_AGENT"] =~ /iPhone/ }) do
+ # constraints(-> (req) { req.env["HTTP_USER_AGENT"] =~ /iPhone/ }) do
# resources :iphones
# end
#
@@ -936,7 +998,10 @@ module ActionDispatch
# end
# Using this, the +:id+ parameter here will default to 'home'.
def defaults(defaults = {})
- scope(:defaults => defaults) { yield }
+ @scope = @scope.new(defaults: merge_defaults_scope(@scope[:defaults], defaults))
+ yield
+ ensure
+ @scope = @scope.parent
end
private
@@ -968,6 +1033,14 @@ module ActionDispatch
child
end
+ def merge_via_scope(parent, child) #:nodoc:
+ child
+ end
+
+ def merge_format_scope(parent, child) #:nodoc:
+ child
+ end
+
def merge_path_names_scope(parent, child) #:nodoc:
merge_options_scope(parent, child)
end
@@ -987,16 +1060,12 @@ module ActionDispatch
end
def merge_options_scope(parent, child) #:nodoc:
- (parent || {}).except(*override_keys(child)).merge!(child)
+ (parent || {}).merge(child)
end
def merge_shallow_scope(parent, child) #:nodoc:
child ? true : false
end
-
- def override_keys(child) #:nodoc:
- child.key?(:only) || child.key?(:except) ? [:only, :except] : []
- end
end
# Resource routing allows you to quickly declare all of the common routes
@@ -1044,31 +1113,36 @@ module ActionDispatch
VALID_ON_OPTIONS = [:new, :collection, :member]
RESOURCE_OPTIONS = [:as, :controller, :path, :only, :except, :param, :concerns]
CANONICAL_ACTIONS = %w(index create new show update destroy)
- RESOURCE_METHOD_SCOPES = [:collection, :member, :new]
- RESOURCE_SCOPES = [:resource, :resources]
class Resource #:nodoc:
- attr_reader :controller, :path, :options, :param
+ attr_reader :controller, :path, :param
- def initialize(entities, options = {})
+ def initialize(entities, api_only, shallow, options = {})
@name = entities.to_s
@path = (options[:path] || @name).to_s
@controller = (options[:controller] || @name).to_s
@as = options[:as]
@param = (options[:param] || :id).to_sym
@options = options
- @shallow = false
+ @shallow = shallow
+ @api_only = api_only
+ @only = options.delete :only
+ @except = options.delete :except
end
def default_actions
- [:index, :create, :new, :show, :update, :destroy, :edit]
+ if @api_only
+ [:index, :create, :show, :update, :destroy]
+ else
+ [:index, :create, :new, :show, :update, :destroy, :edit]
+ end
end
def actions
- if only = @options[:only]
- Array(only).map(&:to_sym)
- elsif except = @options[:except]
- default_actions - Array(except).map(&:to_sym)
+ if @only
+ Array(@only).map(&:to_sym)
+ elsif @except
+ default_actions - Array(@except).map(&:to_sym)
else
default_actions
end
@@ -1095,7 +1169,7 @@ module ActionDispatch
end
def resource_scope
- { :controller => controller }
+ controller
end
alias :collection_scope :path
@@ -1118,17 +1192,15 @@ module ActionDispatch
"#{path}/:#{nested_param}"
end
- def shallow=(value)
- @shallow = value
- end
-
def shallow?
@shallow
end
+
+ def singleton?; false; end
end
class SingletonResource < Resource #:nodoc:
- def initialize(entities, options)
+ def initialize(entities, api_only, shallow, options)
super
@as = nil
@controller = (options[:controller] || plural).to_s
@@ -1136,7 +1208,11 @@ module ActionDispatch
end
def default_actions
- [:show, :create, :update, :destroy, :new, :edit]
+ if @api_only
+ [:show, :create, :update, :destroy]
+ else
+ [:show, :create, :update, :destroy, :new, :edit]
+ end
end
def plural
@@ -1152,6 +1228,8 @@ module ActionDispatch
alias :member_scope :path
alias :nested_scope :path
+
+ def singleton?; true; end
end
def resources_path_names(options)
@@ -1186,20 +1264,23 @@ module ActionDispatch
return self
end
- resource_scope(:resource, SingletonResource.new(resources.pop, options)) do
- yield if block_given?
+ with_scope_level(:resource) do
+ options = apply_action_options options
+ resource_scope(SingletonResource.new(resources.pop, api_only?, @scope[:shallow], options)) do
+ yield if block_given?
- concerns(options[:concerns]) if options[:concerns]
+ concerns(options[:concerns]) if options[:concerns]
- collection do
- post :create
- end if parent_resource.actions.include?(:create)
+ collection do
+ post :create
+ end if parent_resource.actions.include?(:create)
- new do
- get :new
- end if parent_resource.actions.include?(:new)
+ new do
+ get :new
+ end if parent_resource.actions.include?(:new)
- set_member_mappings_for_resource
+ set_member_mappings_for_resource
+ end
end
self
@@ -1344,21 +1425,24 @@ module ActionDispatch
return self
end
- resource_scope(:resources, Resource.new(resources.pop, options)) do
- yield if block_given?
+ with_scope_level(:resources) do
+ options = apply_action_options options
+ resource_scope(Resource.new(resources.pop, api_only?, @scope[:shallow], options)) do
+ yield if block_given?
- concerns(options[:concerns]) if options[:concerns]
+ concerns(options[:concerns]) if options[:concerns]
- collection do
- get :index if parent_resource.actions.include?(:index)
- post :create if parent_resource.actions.include?(:create)
- end
+ collection do
+ get :index if parent_resource.actions.include?(:index)
+ post :create if parent_resource.actions.include?(:create)
+ end
- new do
- get :new
- end if parent_resource.actions.include?(:new)
+ new do
+ get :new
+ end if parent_resource.actions.include?(:new)
- set_member_mappings_for_resource
+ set_member_mappings_for_resource
+ end
end
self
@@ -1382,7 +1466,7 @@ module ActionDispatch
end
with_scope_level(:collection) do
- scope(parent_resource.collection_scope) do
+ path_scope(parent_resource.collection_scope) do
yield
end
end
@@ -1406,9 +1490,11 @@ module ActionDispatch
with_scope_level(:member) do
if shallow?
- shallow_scope(parent_resource.member_scope) { yield }
+ shallow_scope {
+ path_scope(parent_resource.member_scope) { yield }
+ }
else
- scope(parent_resource.member_scope) { yield }
+ path_scope(parent_resource.member_scope) { yield }
end
end
end
@@ -1419,7 +1505,7 @@ module ActionDispatch
end
with_scope_level(:new) do
- scope(parent_resource.new_scope(action_path(:new))) do
+ path_scope(parent_resource.new_scope(action_path(:new))) do
yield
end
end
@@ -1432,9 +1518,15 @@ module ActionDispatch
with_scope_level(:nested) do
if shallow? && shallow_nesting_depth >= 1
- shallow_scope(parent_resource.nested_scope, nested_options) { yield }
+ shallow_scope do
+ path_scope(parent_resource.nested_scope) do
+ scope(nested_options) { yield }
+ end
+ end
else
- scope(parent_resource.nested_scope, nested_options) { yield }
+ path_scope(parent_resource.nested_scope) do
+ scope(nested_options) { yield }
+ end
end
end
end
@@ -1449,18 +1541,22 @@ module ActionDispatch
end
def shallow
- scope(:shallow => true) do
- yield
- end
+ @scope = @scope.new(shallow: true)
+ yield
+ ensure
+ @scope = @scope.parent
end
def shallow?
- parent_resource.instance_of?(Resource) && @scope[:shallow]
+ !parent_resource.singleton? && @scope[:shallow]
end
- # match 'path' => 'controller#action'
- # match 'path', to: 'controller#action'
- # match 'path', 'otherpath', on: :member, via: :get
+ # Matches a url pattern to one or more routes.
+ # For more information, see match[rdoc-ref:Base#match].
+ #
+ # match 'path' => 'controller#action', via: patch
+ # match 'path', to: 'controller#action', via: :post
+ # match 'path', 'otherpath', on: :member, via: :get
def match(path, *rest)
if rest.empty? && Hash === path
options = path
@@ -1486,8 +1582,6 @@ module ActionDispatch
paths = [path] + rest
end
- options[:anchor] = true unless options.key?(:anchor)
-
if options[:on] && !VALID_ON_OPTIONS.include?(options[:on])
raise ArgumentError, "Unknown scope #{on.inspect} given to :on"
end
@@ -1496,61 +1590,100 @@ module ActionDispatch
options[:to] ||= "#{@scope[:controller]}##{@scope[:action]}"
end
- paths.each do |_path|
+ 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
- route_options[:path] ||= _path if _path.is_a?(String)
+ 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:
- path_without_format = _path.to_s.sub(/\(\.:format\)$/, '')
- if using_match_shorthand?(path_without_format, route_options)
- route_options[:to] ||= path_without_format.gsub(%r{^/}, "").sub(%r{/([^/]*)$}, '#\1')
- route_options[:to].tr!("-", "_")
+ 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
- decomposed_match(_path, route_options)
+ 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 using_match_shorthand?(path, options)
- path && (options[:to] || options[:action]).nil? && path =~ %r{/[\w/]+$}
+ 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, options) # :nodoc:
+ def decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) # :nodoc:
if on = options.delete(:on)
- send(on) { decomposed_match(path, options) }
+ send(on) { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) }
else
- case @scope[:scope_level]
+ case @scope.scope_level
when :resources
- nested { decomposed_match(path, options) }
+ nested { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) }
when :resource
- member { decomposed_match(path, options) }
+ member { decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints) }
else
- add_route(path, options)
+ add_route(path, controller, options, _path, to, via, formatted, anchor, options_constraints)
end
end
end
- def add_route(action, options) # :nodoc:
- path = path_for_action(action, options.delete(:path))
+ 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.dup
+ action = action.to_s
+
+ default_action = options.delete(:action) || @scope[:action]
if action =~ /^[\w\-\/]+$/
- options[:action] ||= action.tr('-', '_') unless action.include?("/")
+ default_action ||= action.tr('-', '_') unless action.include?("/")
else
action = nil
end
- if !options.fetch(:as, true) # if it's set to nil or false
- options.delete(:as)
- else
- options[:as] = name_for_action(options[:as], action)
- 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, URI.parser.escape(path), options)
- app, conditions, requirements, defaults, as, anchor = mapping.to_route
- @set.add_route(app, conditions, requirements, defaults, as, anchor)
+ mapping = Mapping.build(@scope, @set, ast, controller, default_action, to, via, formatted, options_constraints, anchor, options)
+ @set.add_route(mapping, ast, as, anchor)
end
def root(path, options={})
@@ -1562,9 +1695,9 @@ module ActionDispatch
raise ArgumentError, "must be called with a path and/or options"
end
- if @scope[:scope_level] == :resources
+ if @scope.resources?
with_scope_level(:root) do
- scope(parent_resource.path) do
+ path_scope(parent_resource.path) do
super(options)
end
end
@@ -1609,66 +1742,46 @@ module ActionDispatch
return true
end
- unless action_options?(options)
- options.merge!(scope_action_options) if scope_action_options?
- end
-
false
end
- def action_options?(options) #:nodoc:
- options[:only] || options[:except]
+ def apply_action_options(options) # :nodoc:
+ return options if action_options? options
+ options.merge scope_action_options
end
- def scope_action_options? #:nodoc:
- @scope[:options] && (@scope[:options][:only] || @scope[:options][:except])
+ def action_options?(options) #:nodoc:
+ options[:only] || options[:except]
end
def scope_action_options #:nodoc:
- @scope[:options].slice(:only, :except)
+ @scope[:action_options] || {}
end
def resource_scope? #:nodoc:
- RESOURCE_SCOPES.include? @scope[:scope_level]
+ @scope.resource_scope?
end
def resource_method_scope? #:nodoc:
- RESOURCE_METHOD_SCOPES.include? @scope[:scope_level]
+ @scope.resource_method_scope?
end
def nested_scope? #:nodoc:
- @scope[:scope_level] == :nested
- end
-
- def with_exclusive_scope
- begin
- @scope = @scope.new(:as => nil, :path => nil)
-
- with_scope_level(:exclusive) do
- yield
- end
- ensure
- @scope = @scope.parent
- end
+ @scope.nested?
end
def with_scope_level(kind)
- @scope = @scope.new(:scope_level => kind)
+ @scope = @scope.new_level(kind)
yield
ensure
@scope = @scope.parent
end
- def resource_scope(kind, resource) #:nodoc:
- resource.shallow = @scope[:shallow]
+ def resource_scope(resource) #:nodoc:
@scope = @scope.new(:scope_level_resource => resource)
- @nesting.push(resource)
- with_scope_level(kind) do
- scope(parent_resource.resource_scope) { yield }
- end
+ controller(resource.resource_scope) { yield }
ensure
- @nesting.pop
@scope = @scope.parent
end
@@ -1681,12 +1794,10 @@ module ActionDispatch
options
end
- def nesting_depth #:nodoc:
- @nesting.size
- end
-
def shallow_nesting_depth #:nodoc:
- @nesting.select(&:shallow?).size
+ @scope.find_all { |node|
+ node.frame[:scope_level_resource]
+ }.count { |node| node.frame[:scope_level_resource].shallow? }
end
def param_constraint? #:nodoc:
@@ -1697,45 +1808,48 @@ module ActionDispatch
@scope[:constraints][parent_resource.param]
end
- def canonical_action?(action, flag) #:nodoc:
- flag && resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s)
+ def canonical_action?(action) #:nodoc:
+ resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s)
end
- def shallow_scope(path, options = {}) #:nodoc:
+ def shallow_scope #:nodoc:
scope = { :as => @scope[:shallow_prefix],
:path => @scope[:shallow_path] }
@scope = @scope.new scope
- scope(path, options) { yield }
+ yield
ensure
@scope = @scope.parent
end
def path_for_action(action, path) #:nodoc:
- if canonical_action?(action, path.blank?)
+ return "#{@scope[:path]}/#{path}" if path
+
+ if canonical_action?(action)
@scope[:path].to_s
else
- "#{@scope[:path]}/#{action_path(action, path)}"
+ "#{@scope[:path]}/#{action_path(action)}"
end
end
- def action_path(name, path = nil) #:nodoc:
- name = name.to_sym if name.is_a?(String)
- path || @scope[:path_names][name] || name.to_s
+ def action_path(name) #:nodoc:
+ @scope[:path_names][name.to_sym] || name
end
def prefix_name_for_action(as, action) #:nodoc:
if as
prefix = as
- elsif !canonical_action?(action, @scope[:scope_level])
+ elsif !canonical_action?(action)
prefix = action
end
- prefix.to_s.tr('-', '_') if prefix
+
+ if prefix && prefix != '/' && !prefix.empty?
+ Mapper.normalize_name prefix.to_s.tr('-', '_')
+ end
end
def name_for_action(as, action) #:nodoc:
prefix = prefix_name_for_action(as, action)
- prefix = Mapper.normalize_name(prefix) if prefix
name_prefix = @scope[:as]
if parent_resource
@@ -1745,27 +1859,15 @@ module ActionDispatch
member_name = parent_resource.member_name
end
- name = case @scope[:scope_level]
- when :nested
- [name_prefix, prefix]
- when :collection
- [prefix, name_prefix, collection_name]
- when :new
- [prefix, :new, name_prefix, member_name]
- when :member
- [prefix, name_prefix, member_name]
- when :root
- [name_prefix, collection_name, prefix]
- else
- [name_prefix, member_name, prefix]
- end
+ action_name = @scope.action_name(name_prefix, prefix, collection_name, member_name)
+ candidate = action_name.select(&:present?).join('_')
- if candidate = name.select(&:present?).join("_").presence
+ unless candidate.empty?
# If a name was not explicitly given, we check if it is valid
# and return nil in case it isn't. Otherwise, we pass the invalid name
# forward so the underlying router engine treats it and raises an exception.
if as.nil?
- candidate unless @set.routes.find { |r| r.name == candidate } || candidate !~ /\A[_a-z]/i
+ candidate unless candidate !~ /\A[_a-z]/i || has_named_route?(candidate)
else
candidate
end
@@ -1783,6 +1885,18 @@ module ActionDispatch
delete :destroy if parent_resource.actions.include?(:destroy)
end
end
+
+ def api_only?
+ @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
end
# Routing Concerns allow you to declare common routes that can be reused
@@ -1893,13 +2007,50 @@ module ActionDispatch
class Scope # :nodoc:
OPTIONS = [:path, :shallow_path, :as, :shallow_prefix, :module,
:controller, :action, :path_names, :constraints,
- :shallow, :blocks, :defaults, :options]
+ :shallow, :blocks, :defaults, :via, :format, :options]
+
+ RESOURCE_SCOPES = [:resource, :resources]
+ RESOURCE_METHOD_SCOPES = [:collection, :member, :new]
- attr_reader :parent
+ attr_reader :parent, :scope_level
- def initialize(hash, parent = {})
+ def initialize(hash, parent = NULL, scope_level = nil)
@hash = hash
@parent = parent
+ @scope_level = scope_level
+ end
+
+ def nested?
+ scope_level == :nested
+ end
+
+ def resources?
+ scope_level == :resources
+ end
+
+ def resource_method_scope?
+ RESOURCE_METHOD_SCOPES.include? scope_level
+ end
+
+ def action_name(name_prefix, prefix, collection_name, member_name)
+ case scope_level
+ when :nested
+ [name_prefix, prefix]
+ when :collection
+ [prefix, name_prefix, collection_name]
+ when :new
+ [prefix, :new, name_prefix, member_name]
+ when :member
+ [prefix, name_prefix, member_name]
+ when :root
+ [name_prefix, collection_name, prefix]
+ else
+ [name_prefix, member_name, prefix]
+ end
+ end
+
+ def resource_scope?
+ RESOURCE_SCOPES.include? scope_level
end
def options
@@ -1907,23 +2058,38 @@ module ActionDispatch
end
def new(hash)
- self.class.new hash, self
+ self.class.new hash, self, scope_level
+ end
+
+ def new_level(level)
+ self.class.new(frame, self, level)
end
def [](key)
- @hash.fetch(key) { @parent[key] }
+ scope = find { |node| node.frame.key? key }
+ scope && scope.frame[key]
end
- def []=(k,v)
- @hash[k] = v
+ include Enumerable
+
+ def each
+ node = self
+ loop do
+ break if node.equal? NULL
+ yield node
+ node = node.parent
+ end
end
+
+ def frame; @hash; end
+
+ NULL = Scope.new(nil, nil)
end
def initialize(set) #:nodoc:
@set = set
@scope = Scope.new({ :path_names => @set.resources_path_names })
@concerns = {}
- @nesting = []
end
include Base
diff --git a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb
index bd3696cda1..9934f5547a 100644
--- a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb
+++ b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb
@@ -1,5 +1,3 @@
-require 'action_controller/model_naming'
-
module ActionDispatch
module Routing
# Polymorphic URL helpers are methods for smart resolution to a named route call when
@@ -55,8 +53,6 @@ module ActionDispatch
# form_for([blog, @post]) # => "/blog/posts/1"
#
module PolymorphicRoutes
- include ActionController::ModelNaming
-
# Constructs a call to a named RESTful route for the given record and returns the
# resulting URL string. For example:
#
@@ -116,7 +112,6 @@ module ActionDispatch
action,
type,
opts
-
end
# Returns the path component of a URL for the given record. It uses
@@ -142,22 +137,26 @@ module ActionDispatch
%w(edit new).each do |action|
module_eval <<-EOT, __FILE__, __LINE__ + 1
- def #{action}_polymorphic_url(record_or_hash, options = {}) # def edit_polymorphic_url(record_or_hash, options = {})
- polymorphic_url( # polymorphic_url(
- record_or_hash, # record_or_hash,
- options.merge(:action => "#{action}")) # options.merge(:action => "edit"))
- end # end
- #
- def #{action}_polymorphic_path(record_or_hash, options = {}) # def edit_polymorphic_path(record_or_hash, options = {})
- polymorphic_url( # polymorphic_url(
- record_or_hash, # record_or_hash,
- options.merge(:action => "#{action}", :routing_type => :path)) # options.merge(:action => "edit", :routing_type => :path))
- end # end
+ def #{action}_polymorphic_url(record_or_hash, options = {})
+ polymorphic_url_for_action("#{action}", record_or_hash, options)
+ end
+
+ def #{action}_polymorphic_path(record_or_hash, options = {})
+ polymorphic_path_for_action("#{action}", record_or_hash, options)
+ end
EOT
end
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' => {} }
@@ -192,7 +191,8 @@ module ActionDispatch
case record_or_hash_or_array
when Array
- if record_or_hash_or_array.empty? || record_or_hash_or_array.include?(nil)
+ 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)
@@ -247,14 +247,12 @@ module ActionDispatch
args = []
model = record.to_model
- name = if record.persisted?
- args << model
- model.class.model_name.singular_route_key
- else
- @key_strategy.call model.class.model_name
- end
-
- named_route = prefix + "#{name}_#{suffix}"
+ named_route = if model.persisted?
+ args << model
+ get_method_for_string model.model_name.singular_route_key
+ else
+ get_method_for_class model
+ end
[named_route, args]
end
@@ -279,7 +277,7 @@ module ActionDispatch
parent.model_name.singular_route_key
else
args << parent.to_model
- parent.to_model.class.model_name.singular_route_key
+ parent.to_model.model_name.singular_route_key
end
}
@@ -290,11 +288,12 @@ module ActionDispatch
when Class
@key_strategy.call record.model_name
else
- if record.persisted?
- args << record.to_model
- record.to_model.class.model_name.singular_route_key
+ model = record.to_model
+ if model.persisted?
+ args << model
+ model.model_name.singular_route_key
else
- @key_strategy.call record.to_model.class.model_name
+ @key_strategy.call model.model_name
end
end
@@ -308,11 +307,11 @@ module ActionDispatch
def get_method_for_class(klass)
name = @key_strategy.call klass.model_name
- prefix + "#{name}_#{suffix}"
+ get_method_for_string name
end
def get_method_for_string(str)
- prefix + "#{str}_#{suffix}"
+ "#{prefix}#{str}_#{suffix}"
end
[nil, 'new', 'edit'].each do |action|
diff --git a/actionpack/lib/action_dispatch/routing/redirection.rb b/actionpack/lib/action_dispatch/routing/redirection.rb
index 3c1c4fadf6..d6987f4d09 100644
--- a/actionpack/lib/action_dispatch/routing/redirection.rb
+++ b/actionpack/lib/action_dispatch/routing/redirection.rb
@@ -24,7 +24,7 @@ module ActionDispatch
def serve(req)
req.check_path_parameters!
uri = URI.parse(path(req.path_parameters, req))
-
+
unless uri.host
if relative_path?(uri.path)
uri.path = "#{req.script_name}/#{uri.path}"
@@ -32,7 +32,7 @@ module ActionDispatch
uri.path = req.script_name.empty? ? "/" : req.script_name
end
end
-
+
uri.scheme ||= req.scheme
uri.host ||= req.host
uri.port ||= req.port unless req.standard_port?
@@ -124,7 +124,7 @@ module ActionDispatch
url_options[:script_name] = request.script_name
end
end
-
+
ActionDispatch::Http::URL.url_for url_options
end
diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb
index 96e23c2464..5f54ea130b 100644
--- a/actionpack/lib/action_dispatch/routing/route_set.rb
+++ b/actionpack/lib/action_dispatch/routing/route_set.rb
@@ -1,6 +1,4 @@
require 'action_dispatch/journey'
-require 'forwardable'
-require 'thread_safe'
require 'active_support/concern'
require 'active_support/core_ext/object/to_query'
require 'active_support/core_ext/hash/slice'
@@ -12,81 +10,63 @@ require 'action_dispatch/routing/endpoint'
module ActionDispatch
module Routing
- class RouteSet #:nodoc:
+ # :stopdoc:
+ class RouteSet
# Since the router holds references to many parts of the system
# like engines, controllers and the application itself, inspecting
# the route set can actually be really slow, therefore we default
# alias inspect to to_s.
alias inspect to_s
- class Dispatcher < Routing::Endpoint #:nodoc:
- def initialize(defaults)
- @defaults = defaults
- @controller_class_names = ThreadSafe::Cache.new
+ class Dispatcher < Routing::Endpoint
+ def initialize(raise_on_name_error)
+ @raise_on_name_error = raise_on_name_error
end
def dispatcher?; true; end
def serve(req)
- req.check_path_parameters!
- params = req.path_parameters
-
- prepare_params!(params)
-
- # Just raise undefined constant errors if a controller was specified as default.
- unless controller = controller(params, @defaults.key?(:controller))
+ params = req.path_parameters
+ controller = controller req
+ res = controller.make_response! req
+ dispatch(controller, params[:action], req, res)
+ rescue NameError => e
+ if @raise_on_name_error
+ raise ActionController::RoutingError, e.message, e.backtrace
+ else
return [404, {'X-Cascade' => 'pass'}, []]
end
-
- dispatch(controller, params[:action], req.env)
- end
-
- def prepare_params!(params)
- normalize_controller!(params)
- merge_default_action!(params)
- end
-
- # If this is a default_controller (i.e. a controller specified by the user)
- # we should raise an error in case it's not found, because it usually means
- # a user error. However, if the controller was retrieved through a dynamic
- # segment, as in :controller(/:action), we should simply return nil and
- # delegate the control back to Rack cascade. Besides, if this is not a default
- # controller, it means we should respect the @scope[:module] parameter.
- def controller(params, default_controller=true)
- if params && params.key?(:controller)
- controller_param = params[:controller]
- controller_reference(controller_param)
- end
- rescue NameError => e
- raise ActionController::RoutingError, e.message, e.backtrace if default_controller
end
private
- def controller_reference(controller_param)
- const_name = @controller_class_names[controller_param] ||= "#{controller_param.camelize}Controller"
- ActiveSupport::Dependencies.constantize(const_name)
+ def controller(req)
+ req.controller_class
end
- def dispatch(controller, action, env)
- controller.action(action).call(env)
+ def dispatch(controller, action, req, res)
+ controller.dispatch(action, req, res)
end
+ end
- def normalize_controller!(params)
- params[:controller] = params[:controller].underscore if params.key?(:controller)
+ class StaticDispatcher < Dispatcher
+ def initialize(controller_class)
+ super(false)
+ @controller_class = controller_class
end
- def merge_default_action!(params)
- params[:action] ||= 'index'
- end
+ private
+
+ def controller(_); @controller_class; end
end
# A NamedRouteCollection instance is a collection of named routes, and also
# maintains an anonymous module that can be used to install helpers for the
# named routes.
- class NamedRouteCollection #:nodoc:
+ class NamedRouteCollection
include Enumerable
- attr_reader :routes, :url_helpers_module
+ attr_reader :routes, :url_helpers_module, :path_helpers_module
+ private :routes
def initialize
@routes = {}
@@ -130,7 +110,7 @@ module ActionDispatch
end
routes[key] = route
define_url_helper @path_helpers_module, route, path_name, route.defaults, name, PATH
- define_url_helper @url_helpers_module, route, url_name, route.defaults, name, FULL
+ define_url_helper @url_helpers_module, route, url_name, route.defaults, name, UNKNOWN
@path_helpers << path_name
@url_helpers << url_name
@@ -140,6 +120,11 @@ module ActionDispatch
routes[name.to_sym]
end
+ def key?(name)
+ return unless name
+ routes.key? name.to_sym
+ end
+
alias []= add
alias [] get
alias clear clear!
@@ -157,26 +142,7 @@ module ActionDispatch
routes.length
end
- def path_helpers_module(warn = false)
- if warn
- mod = @path_helpers_module
- helpers = @path_helpers
- Module.new do
- include mod
-
- helpers.each do |meth|
- define_method(meth) do |*args, &block|
- ActiveSupport::Deprecation.warn("The method `#{meth}` cannot be used here as a full URL is required. Use `#{meth.to_s.sub(/_path$/, '_url')}` instead")
- super(*args, &block)
- end
- end
- end
- else
- @path_helpers_module
- end
- end
-
- class UrlHelper # :nodoc:
+ class UrlHelper
def self.create(route, options, route_name, url_strategy)
if optimize_helper?(route)
OptimizedUrlHelper.new(route, options, route_name, url_strategy)
@@ -191,7 +157,7 @@ module ActionDispatch
attr_reader :url_strategy, :route_name
- class OptimizedUrlHelper < UrlHelper # :nodoc:
+ class OptimizedUrlHelper < UrlHelper
attr_reader :arg_size
def initialize(route, options, route_name, url_strategy)
@@ -213,11 +179,8 @@ module ActionDispatch
private
def optimized_helper(args)
- params = parameterize_args(args)
- missing_keys = missing_keys(params)
-
- unless missing_keys.empty?
- raise_generation_error(params, missing_keys)
+ params = parameterize_args(args) do
+ raise_generation_error(args)
end
@route.format params
@@ -229,16 +192,21 @@ module ActionDispatch
def parameterize_args(args)
params = {}
- @required_parts.zip(args.map(&:to_param)) { |k,v| params[k] = v }
+ @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 missing_keys(args)
- args.select{ |part, arg| arg.nil? || arg.empty? }.keys
- end
-
- def raise_generation_error(args, missing_keys)
- constraints = Hash[@route.requirements.merge(args).sort]
+ 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}"
@@ -267,15 +235,26 @@ module ActionDispatch
end
def handle_positional_args(controller_options, inner_options, args, result, path_params)
-
if args.size > 0
- if args.size < path_params.size - 1 # take format into account
+ # take format into account
+ if path_params.include?(:format)
+ path_params_size = path_params.size - 1
+ else
+ path_params_size = path_params.size
+ end
+
+ if args.size < path_params_size
path_params -= controller_options.keys
path_params -= result.keys
end
- path_params.each { |param|
- result[param] = inner_options[param] || args.shift
- }
+ inner_options.each_key do |key|
+ path_params.delete(key)
+ end
+
+ args.each_with_index do |arg, index|
+ param = path_params[index]
+ result[param] = arg if param
+ end
end
result.merge!(inner_options)
@@ -308,16 +287,14 @@ module ActionDispatch
end
end
- # :stopdoc:
# strategy for building urls to send to the client
PATH = ->(options) { ActionDispatch::Http::URL.path_for(options) }
- FULL = ->(options) { ActionDispatch::Http::URL.full_url_for(options) }
UNKNOWN = ->(options) { ActionDispatch::Http::URL.url_for(options) }
- # :startdoc:
attr_accessor :formatter, :set, :named_routes, :default_scope, :router
attr_accessor :disable_clear_and_finalize, :resources_path_names
- attr_accessor :default_url_options, :request_class
+ attr_accessor :default_url_options
+ attr_reader :env_key
alias :routes :set
@@ -325,21 +302,58 @@ module ActionDispatch
{ :new => 'new', :edit => 'edit' }
end
- def initialize(request_class = ActionDispatch::Request)
+ def self.new_with_config(config)
+ route_set_config = DEFAULT_CONFIG
+
+ # engines apparently don't have this set
+ if config.respond_to? :relative_url_root
+ route_set_config.relative_url_root = config.relative_url_root
+ end
+
+ if config.respond_to? :api_only
+ route_set_config.api_only = config.api_only
+ end
+
+ new route_set_config
+ end
+
+ Config = Struct.new :relative_url_root, :api_only
+
+ DEFAULT_CONFIG = Config.new(nil, false)
+
+ def initialize(config = DEFAULT_CONFIG)
self.named_routes = NamedRouteCollection.new
self.resources_path_names = self.class.default_resources_path_names
self.default_url_options = {}
- self.request_class = request_class
+ @config = config
@append = []
@prepend = []
@disable_clear_and_finalize = false
@finalized = false
+ @env_key = "ROUTES_#{object_id}_SCRIPT_NAME".freeze
@set = Journey::Routes.new
@router = Journey::Router.new @set
- @formatter = Journey::Formatter.new @set
+ @formatter = Journey::Formatter.new self
+ end
+
+ def relative_url_root
+ @config.relative_url_root
+ end
+
+ def api_only?
+ @config.api_only
+ end
+
+ def request_class
+ ActionDispatch::Request
+ end
+
+ def make_request(env)
+ request_class.new env
end
+ private :make_request
def draw(&block)
clear! unless @disable_clear_and_finalize
@@ -384,11 +398,7 @@ module ActionDispatch
@prepend.each { |blk| eval_block(blk) }
end
- def dispatcher(defaults)
- Routing::RouteSet::Dispatcher.new(defaults)
- end
-
- module MountedHelpers #:nodoc:
+ module MountedHelpers
extend ActiveSupport::Concern
include UrlFor
end
@@ -405,9 +415,11 @@ module ActionDispatch
return if MountedHelpers.method_defined?(name)
routes = self
+ helpers = routes.url_helpers
+
MountedHelpers.class_eval do
define_method "_#{name}" do
- RoutesProxy.new(routes, _routes_context)
+ RoutesProxy.new(routes, _routes_context, helpers)
end
end
@@ -418,7 +430,7 @@ module ActionDispatch
RUBY
end
- def url_helpers(include_path_helpers = true)
+ def url_helpers(supports_path = true)
routes = self
Module.new do
@@ -429,7 +441,14 @@ module ActionDispatch
# Rails.application.routes.url_helpers.url_for(args)
@_routes = routes
class << self
- delegate :url_for, :optimize_routes_generation?, to: '@_routes'
+ def url_for(options)
+ @_routes.url_for(options)
+ end
+
+ def optimize_routes_generation?
+ @_routes.optimize_routes_generation?
+ end
+
attr_reader :_routes
def url_options; {}; end
end
@@ -445,14 +464,12 @@ module ActionDispatch
# named routes...
include url_helpers
- if include_path_helpers
+ if supports_path
path_helpers = routes.named_routes.path_helpers_module
- else
- path_helpers = routes.named_routes.path_helpers_module(true)
- end
- include path_helpers
- extend path_helpers
+ include path_helpers
+ extend path_helpers
+ end
# plus a singleton class method called _routes ...
included do
@@ -463,6 +480,12 @@ module ActionDispatch
# UrlFor (included in this module) add extra
# conveniences for working with @_routes.
define_method(:_routes) { @_routes || routes }
+
+ define_method(:_generate_paths_by_default) do
+ supports_path
+ end
+
+ private :_generate_paths_by_default
end
end
@@ -470,7 +493,7 @@ module ActionDispatch
routes.empty?
end
- def add_route(app, conditions = {}, requirements = {}, defaults = {}, name = nil, anchor = true)
+ def add_route(mapping, path_ast, name, anchor)
raise ArgumentError, "Invalid route name: '#{name}'" unless name.blank? || name.to_s.match(/^[_a-z]\w*$/i)
if name && named_routes[name]
@@ -481,74 +504,17 @@ module ActionDispatch
"http://guides.rubyonrails.org/routing.html#restricting-the-routes-created"
end
- path = conditions.delete :path_info
- ast = conditions.delete :parsed_path_info
- path = build_path(path, ast, requirements, anchor)
- conditions = build_conditions(conditions, path.names.map { |x| x.to_sym })
-
- route = @set.add_route(app, path, conditions, defaults, name)
+ route = @set.add_route(name, mapping)
named_routes[name] = route if name
route
end
- def build_path(path, ast, requirements, anchor)
- strexp = Journey::Router::Strexp.new(
- ast,
- path,
- requirements,
- SEPARATORS,
- anchor)
-
- pattern = Journey::Path::Pattern.new(strexp)
-
- builder = Journey::GTG::Builder.new pattern.spec
-
- # Get all the symbol nodes followed by literals that are not the
- # dummy node.
- symbols = pattern.spec.grep(Journey::Nodes::Symbol).find_all { |n|
- builder.followpos(n).first.literal?
- }
-
- # Get all the symbol nodes preceded by literals.
- symbols.concat pattern.spec.find_all(&:literal?).map { |n|
- builder.followpos(n).first
- }.find_all(&:symbol?)
-
- symbols.each { |x|
- x.regexp = /(?:#{Regexp.union(x.regexp, '-')})+/
- }
-
- pattern
- end
- private :build_path
-
- def build_conditions(current_conditions, path_values)
- conditions = current_conditions.dup
-
- # Rack-Mount requires that :request_method be a regular expression.
- # :request_method represents the HTTP verb that matches this route.
- #
- # Here we munge values before they get sent on to rack-mount.
- verbs = conditions[:request_method] || []
- unless verbs.empty?
- conditions[:request_method] = %r[^#{verbs.join('|')}$]
- end
-
- conditions.keep_if do |k, _|
- k == :action || k == :controller || k == :required_defaults ||
- @request_class.public_method_defined?(k) || path_values.include?(k)
- end
- end
- private :build_conditions
-
- class Generator #:nodoc:
+ class Generator
PARAMETERIZE = lambda do |name, value|
if name == :controller
value
- elsif value.is_a?(Array)
- value.map { |v| v.to_param }.join('/')
- elsif param = value.to_param
- param
+ else
+ value.to_param
end
end
@@ -556,8 +522,8 @@ module ActionDispatch
def initialize(named_route, options, recall, set)
@named_route = named_route
- @options = options.dup
- @recall = recall.dup
+ @options = options
+ @recall = recall
@set = set
normalize_recall!
@@ -579,7 +545,7 @@ module ActionDispatch
def use_recall_for(key)
if @recall[key] && (!@options.key?(key) || @options[key] == @recall[key])
if !named_route_exists? || segment_keys.include?(key)
- @options[key] = @recall.delete(key)
+ @options[key] = @recall[key]
end
end
end
@@ -633,12 +599,18 @@ module ActionDispatch
# Remove leading slashes from controllers
def normalize_controller!
- @options[:controller] = controller.sub(%r{^/}, '') if controller
+ if controller
+ if controller.start_with?("/".freeze)
+ @options[:controller] = controller[1..-1]
+ else
+ @options[:controller] = controller
+ end
+ end
end
# Move 'index' action from options to recall
def normalize_action!
- if @options[:action] == 'index'
+ if @options[:action] == 'index'.freeze
@recall[:action] = @options.delete(:action)
end
end
@@ -683,14 +655,22 @@ module ActionDispatch
RESERVED_OPTIONS = [:host, :protocol, :port, :subdomain, :domain, :tld_length,
:trailing_slash, :anchor, :params, :only_path, :script_name,
- :original_script_name]
+ :original_script_name, :relative_url_root]
def optimize_routes_generation?
default_url_options.empty?
end
def find_script_name(options)
- options.delete :script_name
+ options.delete(:script_name) || find_relative_url_root(options) || ''
+ end
+
+ def find_relative_url_root(options)
+ options.delete(:relative_url_root) || relative_url_root
+ end
+
+ def path_for(options, route_name = nil)
+ url_for(options, route_name, PATH)
end
# The +options+ argument must be a hash whose keys are *symbols*.
@@ -709,7 +689,7 @@ module ActionDispatch
original_script_name = options.delete(:original_script_name)
script_name = find_script_name options
- if script_name && original_script_name
+ if original_script_name
script_name = original_script_name + script_name
end
@@ -732,7 +712,7 @@ module ActionDispatch
end
def call(env)
- req = request_class.new(env)
+ req = make_request(env)
req.path_info = Journey::Router::Utils.normalize_path(req.path_info)
@router.serve(req)
end
@@ -748,7 +728,7 @@ module ActionDispatch
raise ActionController::RoutingError, e.message
end
- req = request_class.new(env)
+ req = make_request(env)
@router.recognize(req) do |route, params|
params.merge!(extras)
params.each do |key, value|
@@ -761,19 +741,19 @@ module ActionDispatch
req.path_parameters = old_params.merge params
app = route.app
if app.matches?(req) && app.dispatcher?
- dispatcher = app.app
-
- if dispatcher.controller(params, false)
- dispatcher.prepare_params!(params)
- return params
- else
+ begin
+ req.controller_class
+ rescue NameError
raise ActionController::RoutingError, "A route matches #{path.inspect}, but references missing controller: #{params[:controller].camelize}Controller"
end
+
+ return req.path_parameters
end
end
raise ActionController::RoutingError, "No route matches #{path.inspect}"
end
end
+ # :startdoc:
end
end
diff --git a/actionpack/lib/action_dispatch/routing/routes_proxy.rb b/actionpack/lib/action_dispatch/routing/routes_proxy.rb
index e2393d3799..040ea04046 100644
--- a/actionpack/lib/action_dispatch/routing/routes_proxy.rb
+++ b/actionpack/lib/action_dispatch/routing/routes_proxy.rb
@@ -8,8 +8,9 @@ module ActionDispatch
attr_accessor :scope, :routes
alias :_routes :routes
- def initialize(routes, scope)
+ def initialize(routes, scope, helpers)
@routes, @scope = routes, scope
+ @helpers = helpers
end
def url_options
@@ -19,16 +20,16 @@ module ActionDispatch
end
def respond_to?(method, include_private = false)
- super || routes.url_helpers.respond_to?(method)
+ super || @helpers.respond_to?(method)
end
def method_missing(method, *args)
- if routes.url_helpers.respond_to?(method)
+ if @helpers.respond_to?(method)
self.class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{method}(*args)
options = args.extract_options!
args << url_options.merge((options || {}).symbolize_keys)
- routes.url_helpers.#{method}(*args)
+ @helpers.#{method}(*args)
end
RUBY
send(method, *args)
diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb
index eb554ec383..b6c031dcf4 100644
--- a/actionpack/lib/action_dispatch/routing/url_for.rb
+++ b/actionpack/lib/action_dispatch/routing/url_for.rb
@@ -1,7 +1,7 @@
module ActionDispatch
module Routing
# In <tt>config/routes.rb</tt> you define URL-to-controller mappings, but the reverse
- # is also possible: an URL can be generated from one of your routing definitions.
+ # is also possible: a URL can be generated from one of your routing definitions.
# URL generation functionality is centralized in this module.
#
# See ActionDispatch::Routing for general information about routing and routes.rb.
@@ -52,9 +52,11 @@ module ActionDispatch
# argument.
#
# For convenience reasons, mailers provide a shortcut for ActionController::UrlFor#url_for.
- # So within mailers, you only have to type 'url_for' instead of 'ActionController::UrlFor#url_for'
- # in full. However, mailers don't have hostname information, and that's why you'll still
- # have to specify the <tt>:host</tt> argument when generating URLs in mailers.
+ # So within mailers, you only have to type +url_for+ instead of 'ActionController::UrlFor#url_for'
+ # in full. However, mailers don't have hostname information, and you still have to provide
+ # the +:host+ argument or set the default host that will be used in all mailers using the
+ # configuration option +config.action_mailer.default_url_options+. For more information on
+ # url_for in mailers read the ActionMailer#Base documentation.
#
#
# == URL generation for named routes
@@ -147,6 +149,20 @@ module ActionDispatch
# # => 'http://somehost.org/myapp/tasks/testing'
# url_for controller: 'tasks', action: 'testing', host: 'somehost.org', script_name: "/myapp", only_path: true
# # => '/myapp/tasks/testing'
+ #
+ # Missing routes keys may be filled in from the current request's parameters
+ # (e.g. +:controller+, +:action+, +:id+ and any other parameters that are
+ # placed in the path). Given that the current action has been reached
+ # through `GET /users/1`:
+ #
+ # url_for(only_path: true) # => '/users/1'
+ # url_for(only_path: true, action: 'edit') # => '/users/1/edit'
+ # url_for(only_path: true, action: 'edit', id: 2) # => '/users/2/edit'
+ #
+ # Notice that no +:id+ parameter was provided to the first +url_for+ call
+ # and the helper used the one from the route's path. Any path parameter
+ # implicitly used by +url_for+ can always be overwritten like shown on the
+ # last +url_for+ calls.
def url_for(options = nil)
case options
when nil
@@ -155,12 +171,17 @@ module ActionDispatch
route_name = options.delete :use_route
_routes.url_for(options.symbolize_keys.reverse_merge!(url_options),
route_name)
+ when ActionController::Parameters
+ route_name = options.delete :use_route
+ _routes.url_for(options.to_unsafe_h.symbolize_keys.
+ reverse_merge!(url_options), route_name)
when String
options
when Symbol
HelperMethodBuilder.url.handle_string_call self, options
when Array
- polymorphic_url(options, options.extract_options!)
+ components = options.dup
+ polymorphic_url(components, components.extract_options!)
when Class
HelperMethodBuilder.url.handle_class_call self, options
else
diff --git a/actionpack/lib/action_dispatch/testing/assertions.rb b/actionpack/lib/action_dispatch/testing/assertions.rb
index 226baf9ad0..fae266273e 100644
--- a/actionpack/lib/action_dispatch/testing/assertions.rb
+++ b/actionpack/lib/action_dispatch/testing/assertions.rb
@@ -1,18 +1,22 @@
+require 'rails-dom-testing'
+
module ActionDispatch
module Assertions
- autoload :DomAssertions, 'action_dispatch/testing/assertions/dom'
autoload :ResponseAssertions, 'action_dispatch/testing/assertions/response'
autoload :RoutingAssertions, 'action_dispatch/testing/assertions/routing'
- autoload :SelectorAssertions, 'action_dispatch/testing/assertions/selector'
- autoload :TagAssertions, 'action_dispatch/testing/assertions/tag'
extend ActiveSupport::Concern
- include DomAssertions
include ResponseAssertions
include RoutingAssertions
- include SelectorAssertions
- include TagAssertions
+ include Rails::Dom::Testing::Assertions
+
+ def html_document
+ @html_document ||= if @response.content_type.to_s =~ /xml\z/
+ Nokogiri::XML::Document.parse(@response.body)
+ else
+ Nokogiri::HTML::Document.parse(@response.body)
+ end
+ end
end
end
-
diff --git a/actionpack/lib/action_dispatch/testing/assertions/dom.rb b/actionpack/lib/action_dispatch/testing/assertions/dom.rb
deleted file mode 100644
index 241a39393a..0000000000
--- a/actionpack/lib/action_dispatch/testing/assertions/dom.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-require 'action_view/vendor/html-scanner'
-
-module ActionDispatch
- module Assertions
- module DomAssertions
- # \Test two HTML strings for equivalency (e.g., identical up to reordering of attributes)
- #
- # # assert that the referenced method generates the appropriate HTML string
- # assert_dom_equal '<a href="http://www.example.com">Apples</a>', link_to("Apples", "http://www.example.com")
- def assert_dom_equal(expected, actual, message = nil)
- expected_dom = HTML::Document.new(expected).root
- actual_dom = HTML::Document.new(actual).root
- assert_equal expected_dom, actual_dom, message
- end
-
- # The negated form of +assert_dom_equivalent+.
- #
- # # assert that the referenced method does not generate the specified HTML string
- # assert_dom_not_equal '<a href="http://www.example.com">Apples</a>', link_to("Oranges", "http://www.example.com")
- def assert_dom_not_equal(expected, actual, message = nil)
- expected_dom = HTML::Document.new(expected).root
- actual_dom = HTML::Document.new(actual).root
- assert_not_equal expected_dom, actual_dom, message
- end
- end
- end
-end
diff --git a/actionpack/lib/action_dispatch/testing/assertions/response.rb b/actionpack/lib/action_dispatch/testing/assertions/response.rb
index 0adc6c84ff..b6e21b0d28 100644
--- a/actionpack/lib/action_dispatch/testing/assertions/response.rb
+++ b/actionpack/lib/action_dispatch/testing/assertions/response.rb
@@ -3,6 +3,13 @@ module ActionDispatch
module Assertions
# A small suite of assertions that test responses from \Rails applications.
module ResponseAssertions
+ RESPONSE_PREDICATES = { # :nodoc:
+ success: :successful?,
+ missing: :not_found?,
+ redirect: :redirection?,
+ error: :server_error?,
+ }
+
# Asserts that the response is one of the following types:
#
# * <tt>:success</tt> - Status code was in the 200-299 range
@@ -20,11 +27,9 @@ module ActionDispatch
# # assert that the response code was status code 401 (unauthorized)
# assert_response 401
def assert_response(type, message = nil)
- message ||= "Expected response to be a <#{type}>, but was <#{@response.response_code}>"
-
if Symbol === type
if [:success, :missing, :redirect, :error].include?(type)
- assert @response.send("#{type}?"), message
+ assert_predicate @response, RESPONSE_PREDICATES[type], message
else
code = Rack::Utils::SYMBOL_TO_STATUS_CODE[type]
if code.nil?
@@ -73,13 +78,8 @@ module ActionDispatch
if Regexp === fragment
fragment
else
- handle = @controller || Class.new(ActionController::Metal) do
- include ActionController::Redirecting
- def initialize(request)
- @_request = request
- end
- end.new(@request)
- handle._compute_redirect_to_location(fragment)
+ handle = @controller || ActionController::Redirecting
+ handle._compute_redirect_to_location(@request, fragment)
end
end
end
diff --git a/actionpack/lib/action_dispatch/testing/assertions/routing.rb b/actionpack/lib/action_dispatch/testing/assertions/routing.rb
index 2cf38a9c2d..54e24ed6bf 100644
--- a/actionpack/lib/action_dispatch/testing/assertions/routing.rb
+++ b/actionpack/lib/action_dispatch/testing/assertions/routing.rb
@@ -38,18 +38,24 @@ 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)
- request = recognized_request_for(path, extras)
+ if path.is_a?(Hash) && path[:method].to_s == "all"
+ [:get, :post, :put, :delete].each do |method|
+ assert_recognizes(expected_options, path.merge(method: method), extras, msg)
+ end
+ else
+ request = recognized_request_for(path, extras, msg)
- expected_options = expected_options.clone
+ expected_options = expected_options.clone
- expected_options.stringify_keys!
+ expected_options.stringify_keys!
- msg = message(msg, "") {
- sprintf("The recognized options <%s> did not match <%s>, difference:",
- request.path_parameters, expected_options)
- }
+ msg = message(msg, "") {
+ sprintf("The recognized options <%s> did not match <%s>, difference:",
+ request.path_parameters, expected_options)
+ }
- assert_equal(expected_options, request.path_parameters, msg)
+ assert_equal(expected_options, request.path_parameters, msg)
+ end
end
# Asserts that the provided options can be used to generate the provided path. This is the inverse of +assert_recognizes+.
@@ -69,9 +75,9 @@ 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)
+ def assert_generates(expected_path, options, defaults={}, extras={}, message=nil)
if expected_path =~ %r{://}
- fail_on(URI::InvalidURIError) do
+ fail_on(URI::InvalidURIError, message) do
uri = URI.parse(expected_path)
expected_path = uri.path.to_s.empty? ? "/" : uri.path
end
@@ -80,8 +86,8 @@ module ActionDispatch
end
# Load routes.rb if it hasn't been loaded.
- generated_path, extra_keys = @routes.generate_extras(options, defaults)
- found_extras = options.reject { |k, _| ! extra_keys.include? k }
+ generated_path, query_string_keys = @routes.generate_extras(options, defaults)
+ found_extras = options.reject { |k, _| ! query_string_keys.include? k }
msg = message || sprintf("found extras <%s>, not <%s>", found_extras, extras)
assert_equal(extras, found_extras, msg)
@@ -144,13 +150,7 @@ module ActionDispatch
old_controller, @controller = @controller, @controller.clone
_routes = @routes
- # Unfortunately, there is currently an abstraction leak between AC::Base
- # and AV::Base which requires having the URL helpers in both AC and AV.
- # To do this safely at runtime for tests, we need to bump up the helper serial
- # to that the old AV subclass isn't cached.
- #
- # TODO: Make this unnecessary
- @controller.singleton_class.send(:include, _routes.url_helpers)
+ @controller.singleton_class.include(_routes.url_helpers)
@controller.view_context_class = Class.new(@controller.view_context_class) do
include _routes.url_helpers
end
@@ -165,7 +165,7 @@ module ActionDispatch
# ROUTES TODO: These assertions should really work in an integration context
def method_missing(selector, *args, &block)
- if defined?(@controller) && @controller && @routes && @routes.named_routes.route_defined?(selector)
+ if defined?(@controller) && @controller && defined?(@routes) && @routes && @routes.named_routes.route_defined?(selector)
@controller.send(selector, *args, &block)
else
super
@@ -174,7 +174,7 @@ module ActionDispatch
private
# Recognizes the route for a given path.
- def recognized_request_for(path, extras = {})
+ def recognized_request_for(path, extras = {}, msg)
if path.is_a?(Hash)
method = path[:method]
path = path[:path]
@@ -183,10 +183,10 @@ module ActionDispatch
end
# Assume given controller
- request = ActionController::TestRequest.new
+ request = ActionController::TestRequest.create
if path =~ %r{://}
- fail_on(URI::InvalidURIError) do
+ fail_on(URI::InvalidURIError, msg) do
uri = URI.parse(path)
request.env["rack.url_scheme"] = uri.scheme || "http"
request.host = uri.host if uri.host
@@ -200,7 +200,7 @@ module ActionDispatch
request.request_method = method if method
- params = fail_on(ActionController::RoutingError) do
+ params = fail_on(ActionController::RoutingError, msg) do
@routes.recognize_path(path, { :method => method, :extras => extras })
end
request.path_parameters = params.with_indifferent_access
@@ -208,10 +208,10 @@ module ActionDispatch
request
end
- def fail_on(exception_class)
+ def fail_on(exception_class, message)
yield
rescue exception_class => e
- raise Minitest::Assertion, e.message
+ raise Minitest::Assertion, message || e.message
end
end
end
diff --git a/actionpack/lib/action_dispatch/testing/assertions/selector.rb b/actionpack/lib/action_dispatch/testing/assertions/selector.rb
deleted file mode 100644
index 12023e6f77..0000000000
--- a/actionpack/lib/action_dispatch/testing/assertions/selector.rb
+++ /dev/null
@@ -1,430 +0,0 @@
-require 'action_view/vendor/html-scanner'
-require 'active_support/core_ext/object/inclusion'
-
-#--
-# Copyright (c) 2006 Assaf Arkin (http://labnotes.org)
-# Under MIT and/or CC By license.
-#++
-
-module ActionDispatch
- module Assertions
- NO_STRIP = %w{pre script style textarea}
-
- # Adds the +assert_select+ method for use in Rails functional
- # test cases, which can be used to make assertions on the response HTML of a controller
- # action. You can also call +assert_select+ within another +assert_select+ to
- # make assertions on elements selected by the enclosing assertion.
- #
- # Use +css_select+ to select elements without making an assertions, either
- # from the response HTML or elements selected by the enclosing assertion.
- #
- # In addition to HTML responses, you can make the following assertions:
- #
- # * +assert_select_encoded+ - Assertions on HTML encoded inside XML, for example for dealing with feed item descriptions.
- # * +assert_select_email+ - Assertions on the HTML body of an e-mail.
- #
- # Also see HTML::Selector to learn how to use selectors.
- module SelectorAssertions
- # Select and return all matching elements.
- #
- # If called with a single argument, uses that argument as a selector
- # to match all elements of the current page. Returns an empty array
- # if no match is found.
- #
- # If called with two arguments, uses the first argument as the base
- # element and the second argument as the selector. Attempts to match the
- # base element and any of its children. Returns an empty array if no
- # match is found.
- #
- # The selector may be a CSS selector expression (String), an expression
- # with substitution values (Array) or an HTML::Selector object.
- #
- # # Selects all div tags
- # divs = css_select("div")
- #
- # # Selects all paragraph tags and does something interesting
- # pars = css_select("p")
- # pars.each do |par|
- # # Do something fun with paragraphs here...
- # end
- #
- # # Selects all list items in unordered lists
- # items = css_select("ul>li")
- #
- # # Selects all form tags and then all inputs inside the form
- # forms = css_select("form")
- # forms.each do |form|
- # inputs = css_select(form, "input")
- # ...
- # end
- def css_select(*args)
- # See assert_select to understand what's going on here.
- arg = args.shift
-
- if arg.is_a?(HTML::Node)
- root = arg
- arg = args.shift
- elsif arg == nil
- raise ArgumentError, "First argument is either selector or element to select, but nil found. Perhaps you called assert_select with an element that does not exist?"
- elsif defined?(@selected) && @selected
- matches = []
-
- @selected.each do |selected|
- subset = css_select(selected, HTML::Selector.new(arg.dup, args.dup))
- subset.each do |match|
- matches << match unless matches.any? { |m| m.equal?(match) }
- end
- end
-
- return matches
- else
- root = response_from_page
- end
-
- case arg
- when String
- selector = HTML::Selector.new(arg, args)
- when Array
- selector = HTML::Selector.new(*arg)
- when HTML::Selector
- selector = arg
- else raise ArgumentError, "Expecting a selector as the first argument"
- end
-
- selector.select(root)
- end
-
- # An assertion that selects elements and makes one or more equality tests.
- #
- # If the first argument is an element, selects all matching elements
- # starting from (and including) that element and all its children in
- # depth-first order.
- #
- # If no element if specified, calling +assert_select+ selects from the
- # response HTML unless +assert_select+ is called from within an +assert_select+ block.
- #
- # When called with a block +assert_select+ passes an array of selected elements
- # to the block. Calling +assert_select+ from the block, with no element specified,
- # runs the assertion on the complete set of elements selected by the enclosing assertion.
- # Alternatively the array may be iterated through so that +assert_select+ can be called
- # separately for each element.
- #
- #
- # ==== Example
- # If the response contains two ordered lists, each with four list elements then:
- # assert_select "ol" do |elements|
- # elements.each do |element|
- # assert_select element, "li", 4
- # end
- # end
- #
- # will pass, as will:
- # assert_select "ol" do
- # assert_select "li", 8
- # end
- #
- # The selector may be a CSS selector expression (String), an expression
- # with substitution values, or an HTML::Selector object.
- #
- # === Equality Tests
- #
- # The equality test may be one of the following:
- # * <tt>true</tt> - Assertion is true if at least one element selected.
- # * <tt>false</tt> - Assertion is true if no element selected.
- # * <tt>String/Regexp</tt> - Assertion is true if the text value of at least
- # one element matches the string or regular expression.
- # * <tt>Integer</tt> - Assertion is true if exactly that number of
- # elements are selected.
- # * <tt>Range</tt> - Assertion is true if the number of selected
- # elements fit the range.
- # If no equality test specified, the assertion is true if at least one
- # element selected.
- #
- # To perform more than one equality tests, use a hash with the following keys:
- # * <tt>:text</tt> - Narrow the selection to elements that have this text
- # value (string or regexp).
- # * <tt>:html</tt> - Narrow the selection to elements that have this HTML
- # content (string or regexp).
- # * <tt>:count</tt> - Assertion is true if the number of selected elements
- # is equal to this value.
- # * <tt>:minimum</tt> - Assertion is true if the number of selected
- # elements is at least this value.
- # * <tt>:maximum</tt> - Assertion is true if the number of selected
- # elements is at most this value.
- #
- # If the method is called with a block, once all equality tests are
- # evaluated the block is called with an array of all matched elements.
- #
- # # At least one form element
- # assert_select "form"
- #
- # # Form element includes four input fields
- # assert_select "form input", 4
- #
- # # Page title is "Welcome"
- # assert_select "title", "Welcome"
- #
- # # Page title is "Welcome" and there is only one title element
- # assert_select "title", {count: 1, text: "Welcome"},
- # "Wrong title or more than one title element"
- #
- # # Page contains no forms
- # assert_select "form", false, "This page must contain no forms"
- #
- # # Test the content and style
- # assert_select "body div.header ul.menu"
- #
- # # Use substitution values
- # assert_select "ol>li#?", /item-\d+/
- #
- # # All input fields in the form have a name
- # assert_select "form input" do
- # assert_select "[name=?]", /.+/ # Not empty
- # end
- def assert_select(*args, &block)
- # Start with optional element followed by mandatory selector.
- arg = args.shift
- @selected ||= nil
-
- if arg.is_a?(HTML::Node)
- # First argument is a node (tag or text, but also HTML root),
- # so we know what we're selecting from.
- root = arg
- arg = args.shift
- elsif arg == nil
- # This usually happens when passing a node/element that
- # happens to be nil.
- raise ArgumentError, "First argument is either selector or element to select, but nil found. Perhaps you called assert_select with an element that does not exist?"
- elsif @selected
- root = HTML::Node.new(nil)
- root.children.concat @selected
- else
- # Otherwise just operate on the response document.
- root = response_from_page
- end
-
- # First or second argument is the selector: string and we pass
- # all remaining arguments. Array and we pass the argument. Also
- # accepts selector itself.
- case arg
- when String
- selector = HTML::Selector.new(arg, args)
- when Array
- selector = HTML::Selector.new(*arg)
- when HTML::Selector
- selector = arg
- else raise ArgumentError, "Expecting a selector as the first argument"
- end
-
- # Next argument is used for equality tests.
- equals = {}
- case arg = args.shift
- when Hash
- equals = arg
- when String, Regexp
- equals[:text] = arg
- when Integer
- equals[:count] = arg
- when Range
- equals[:minimum] = arg.begin
- equals[:maximum] = arg.end
- when FalseClass
- equals[:count] = 0
- when NilClass, TrueClass
- equals[:minimum] = 1
- else raise ArgumentError, "I don't understand what you're trying to match"
- end
-
- # By default we're looking for at least one match.
- if equals[:count]
- equals[:minimum] = equals[:maximum] = equals[:count]
- else
- equals[:minimum] = 1 unless equals[:minimum]
- end
-
- # Last argument is the message we use if the assertion fails.
- message = args.shift
- #- message = "No match made with selector #{selector.inspect}" unless message
- if args.shift
- raise ArgumentError, "Not expecting that last argument, you either have too many arguments, or they're the wrong type"
- end
-
- matches = selector.select(root)
- # If text/html, narrow down to those elements that match it.
- content_mismatch = nil
- if match_with = equals[:text]
- matches.delete_if do |match|
- text = ""
- stack = match.children.reverse
- while node = stack.pop
- if node.tag?
- stack.concat node.children.reverse
- else
- content = node.content
- text << content
- end
- end
- text.strip! unless NO_STRIP.include?(match.name)
- text.sub!(/\A\n/, '') if match.name == "textarea"
- unless match_with.is_a?(Regexp) ? (text =~ match_with) : (text == match_with.to_s)
- content_mismatch ||= sprintf("<%s> expected but was\n<%s>", match_with, text)
- true
- end
- end
- elsif match_with = equals[:html]
- matches.delete_if do |match|
- html = match.children.map(&:to_s).join
- html.strip! unless NO_STRIP.include?(match.name)
- unless match_with.is_a?(Regexp) ? (html =~ match_with) : (html == match_with.to_s)
- content_mismatch ||= sprintf("<%s> expected but was\n<%s>", match_with, html)
- true
- end
- end
- end
- # Expecting foo found bar element only if found zero, not if
- # found one but expecting two.
- message ||= content_mismatch if matches.empty?
- # Test minimum/maximum occurrence.
- min, max, count = equals[:minimum], equals[:maximum], equals[:count]
-
- # FIXME: minitest provides messaging when we use assert_operator,
- # so is this custom message really needed?
- message = message || %(Expected #{count_description(min, max, count)} matching "#{selector.to_s}", found #{matches.size})
- if count
- assert_equal count, matches.size, message
- else
- assert_operator matches.size, :>=, min, message if min
- assert_operator matches.size, :<=, max, message if max
- end
-
- # If a block is given call that block. Set @selected to allow
- # nested assert_select, which can be nested several levels deep.
- if block_given? && !matches.empty?
- begin
- in_scope, @selected = @selected, matches
- yield matches
- ensure
- @selected = in_scope
- end
- end
-
- # Returns all matches elements.
- matches
- end
-
- def count_description(min, max, count) #:nodoc:
- pluralize = lambda {|word, quantity| word << (quantity == 1 ? '' : 's')}
-
- if min && max && (max != min)
- "between #{min} and #{max} elements"
- elsif min && max && max == min && count
- "exactly #{count} #{pluralize['element', min]}"
- elsif min && !(min == 1 && max == 1)
- "at least #{min} #{pluralize['element', min]}"
- elsif max
- "at most #{max} #{pluralize['element', max]}"
- end
- end
-
- # Extracts the content of an element, treats it as encoded HTML and runs
- # nested assertion on it.
- #
- # You typically call this method within another assertion to operate on
- # all currently selected elements. You can also pass an element or array
- # of elements.
- #
- # The content of each element is un-encoded, and wrapped in the root
- # element +encoded+. It then calls the block with all un-encoded elements.
- #
- # # Selects all bold tags from within the title of an Atom feed's entries (perhaps to nab a section name prefix)
- # assert_select "feed[xmlns='http://www.w3.org/2005/Atom']" do
- # # Select each entry item and then the title item
- # assert_select "entry>title" do
- # # Run assertions on the encoded title elements
- # assert_select_encoded do
- # assert_select "b"
- # end
- # end
- # end
- #
- #
- # # Selects all paragraph tags from within the description of an RSS feed
- # assert_select "rss[version=2.0]" do
- # # Select description element of each feed item.
- # assert_select "channel>item>description" do
- # # Run assertions on the encoded elements.
- # assert_select_encoded do
- # assert_select "p"
- # end
- # end
- # end
- def assert_select_encoded(element = nil, &block)
- case element
- when Array
- elements = element
- when HTML::Node
- elements = [element]
- when nil
- unless elements = @selected
- raise ArgumentError, "First argument is optional, but must be called from a nested assert_select"
- end
- else
- raise ArgumentError, "Argument is optional, and may be node or array of nodes"
- end
-
- fix_content = lambda do |node|
- # Gets around a bug in the Rails 1.1 HTML parser.
- node.content.gsub(/<!\[CDATA\[(.*)(\]\]>)?/m) { Rack::Utils.escapeHTML($1) }
- end
-
- selected = elements.map do |elem|
- text = elem.children.select{ |c| not c.tag? }.map{ |c| fix_content[c] }.join
- root = HTML::Document.new(CGI.unescapeHTML("<encoded>#{text}</encoded>")).root
- css_select(root, "encoded:root", &block)[0]
- end
-
- begin
- old_selected, @selected = @selected, selected
- assert_select ":root", &block
- ensure
- @selected = old_selected
- end
- end
-
- # Extracts the body of an email and runs nested assertions on it.
- #
- # You must enable deliveries for this assertion to work, use:
- # ActionMailer::Base.perform_deliveries = true
- #
- # assert_select_email do
- # assert_select "h1", "Email alert"
- # end
- #
- # assert_select_email do
- # items = assert_select "ol>li"
- # items.each do
- # # Work with items here...
- # end
- # end
- def assert_select_email(&block)
- deliveries = ActionMailer::Base.deliveries
- assert !deliveries.empty?, "No e-mail in delivery list"
-
- deliveries.each do |delivery|
- (delivery.parts.empty? ? [delivery] : delivery.parts).each do |part|
- if part["Content-Type"].to_s =~ /^text\/html\W/
- root = HTML::Document.new(part.body.to_s).root
- assert_select root, ":root", &block
- end
- end
- end
- end
-
- protected
- # +assert_select+ and +css_select+ call this to obtain the content in the HTML page.
- def response_from_page
- html_document.root
- end
- end
- end
-end
diff --git a/actionpack/lib/action_dispatch/testing/assertions/tag.rb b/actionpack/lib/action_dispatch/testing/assertions/tag.rb
deleted file mode 100644
index e5fe30ba82..0000000000
--- a/actionpack/lib/action_dispatch/testing/assertions/tag.rb
+++ /dev/null
@@ -1,135 +0,0 @@
-require 'action_view/vendor/html-scanner'
-
-module ActionDispatch
- module Assertions
- # Pair of assertions to testing elements in the HTML output of the response.
- module TagAssertions
- # Asserts that there is a tag/node/element in the body of the response
- # that meets all of the given conditions. The +conditions+ parameter must
- # be a hash of any of the following keys (all are optional):
- #
- # * <tt>:tag</tt>: the node type must match the corresponding value
- # * <tt>:attributes</tt>: a hash. The node's attributes must match the
- # corresponding values in the hash.
- # * <tt>:parent</tt>: a hash. The node's parent must match the
- # corresponding hash.
- # * <tt>:child</tt>: a hash. At least one of the node's immediate children
- # must meet the criteria described by the hash.
- # * <tt>:ancestor</tt>: a hash. At least one of the node's ancestors must
- # meet the criteria described by the hash.
- # * <tt>:descendant</tt>: a hash. At least one of the node's descendants
- # must meet the criteria described by the hash.
- # * <tt>:sibling</tt>: a hash. At least one of the node's siblings must
- # meet the criteria described by the hash.
- # * <tt>:after</tt>: a hash. The node must be after any sibling meeting
- # the criteria described by the hash, and at least one sibling must match.
- # * <tt>:before</tt>: a hash. The node must be before any sibling meeting
- # the criteria described by the hash, and at least one sibling must match.
- # * <tt>:children</tt>: a hash, for counting children of a node. Accepts
- # the keys:
- # * <tt>:count</tt>: either a number or a range which must equal (or
- # include) the number of children that match.
- # * <tt>:less_than</tt>: the number of matching children must be less
- # than this number.
- # * <tt>:greater_than</tt>: the number of matching children must be
- # greater than this number.
- # * <tt>:only</tt>: another hash consisting of the keys to use
- # to match on the children, and only matching children will be
- # counted.
- # * <tt>:content</tt>: the textual content of the node must match the
- # given value. This will not match HTML tags in the body of a
- # tag--only text.
- #
- # Conditions are matched using the following algorithm:
- #
- # * if the condition is a string, it must be a substring of the value.
- # * if the condition is a regexp, it must match the value.
- # * if the condition is a number, the value must match number.to_s.
- # * if the condition is +true+, the value must not be +nil+.
- # * if the condition is +false+ or +nil+, the value must be +nil+.
- #
- # # Assert that there is a "span" tag
- # assert_tag tag: "span"
- #
- # # Assert that there is a "span" tag with id="x"
- # assert_tag tag: "span", attributes: { id: "x" }
- #
- # # Assert that there is a "span" tag using the short-hand
- # assert_tag :span
- #
- # # Assert that there is a "span" tag with id="x" using the short-hand
- # assert_tag :span, attributes: { id: "x" }
- #
- # # Assert that there is a "span" inside of a "div"
- # assert_tag tag: "span", parent: { tag: "div" }
- #
- # # Assert that there is a "span" somewhere inside a table
- # assert_tag tag: "span", ancestor: { tag: "table" }
- #
- # # Assert that there is a "span" with at least one "em" child
- # assert_tag tag: "span", child: { tag: "em" }
- #
- # # Assert that there is a "span" containing a (possibly nested)
- # # "strong" tag.
- # assert_tag tag: "span", descendant: { tag: "strong" }
- #
- # # Assert that there is a "span" containing between 2 and 4 "em" tags
- # # as immediate children
- # assert_tag tag: "span",
- # children: { count: 2..4, only: { tag: "em" } }
- #
- # # Get funky: assert that there is a "div", with an "ul" ancestor
- # # and an "li" parent (with "class" = "enum"), and containing a
- # # "span" descendant that contains text matching /hello world/
- # assert_tag tag: "div",
- # ancestor: { tag: "ul" },
- # parent: { tag: "li",
- # attributes: { class: "enum" } },
- # descendant: { tag: "span",
- # child: /hello world/ }
- #
- # <b>Please note</b>: +assert_tag+ and +assert_no_tag+ only work
- # with well-formed XHTML. They recognize a few tags as implicitly self-closing
- # (like br and hr and such) but will not work correctly with tags
- # that allow optional closing tags (p, li, td). <em>You must explicitly
- # close all of your tags to use these assertions.</em>
- def assert_tag(*opts)
- opts = opts.size > 1 ? opts.last.merge({ :tag => opts.first.to_s }) : opts.first
- tag = find_tag(opts)
- assert tag, "expected tag, but no tag found matching #{opts.inspect} in:\n#{@response.body.inspect}"
- end
-
- # Identical to +assert_tag+, but asserts that a matching tag does _not_
- # exist. (See +assert_tag+ for a full discussion of the syntax.)
- #
- # # Assert that there is not a "div" containing a "p"
- # assert_no_tag tag: "div", descendant: { tag: "p" }
- #
- # # Assert that an unordered list is empty
- # assert_no_tag tag: "ul", descendant: { tag: "li" }
- #
- # # Assert that there is not a "p" tag with between 1 to 3 "img" tags
- # # as immediate children
- # assert_no_tag tag: "p",
- # children: { count: 1..3, only: { tag: "img" } }
- def assert_no_tag(*opts)
- opts = opts.size > 1 ? opts.last.merge({ :tag => opts.first.to_s }) : opts.first
- tag = find_tag(opts)
- assert !tag, "expected no tag, but found tag matching #{opts.inspect} in:\n#{@response.body.inspect}"
- end
-
- def find_tag(conditions)
- html_document.find(conditions)
- end
-
- def find_all_tag(conditions)
- html_document.find_all(conditions)
- end
-
- def html_document
- xml = @response.content_type =~ /xml$/
- @html_document ||= HTML::Document.new(@response.body, false, xml)
- end
- end
- end
-end
diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb
index 192ccdb9d5..790f9ea5d2 100644
--- a/actionpack/lib/action_dispatch/testing/integration.rb
+++ b/actionpack/lib/action_dispatch/testing/integration.rb
@@ -2,6 +2,7 @@ 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'
@@ -12,12 +13,14 @@ module ActionDispatch
#
# - +path+: The URI (as a String) on which you want to perform a GET
# request.
- # - +parameters+: The HTTP parameters that you want to pass. This may
+ # - +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_or_env+: Additional headers to pass, as a Hash. The headers will be
+ # - +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
@@ -28,38 +31,43 @@ module ActionDispatch
#
# You can also perform POST, PATCH, PUT, DELETE, and HEAD requests with
# +#post+, +#patch+, +#put+, +#delete+, and +#head+.
- def get(path, parameters = nil, headers_or_env = nil)
- process :get, path, parameters, headers_or_env
+ #
+ # Example:
+ #
+ # get '/feed', params: { since: 201501011400 }
+ # post '/profile', headers: { "X-Test-Header" => "testvalue" }
+ def get(path, *args)
+ process_with_kwargs(:get, path, *args)
end
# Performs a POST request with the given parameters. See +#get+ for more
# details.
- def post(path, parameters = nil, headers_or_env = nil)
- process :post, path, parameters, headers_or_env
+ def post(path, *args)
+ process_with_kwargs(:post, path, *args)
end
# Performs a PATCH request with the given parameters. See +#get+ for more
# details.
- def patch(path, parameters = nil, headers_or_env = nil)
- process :patch, path, parameters, headers_or_env
+ def patch(path, *args)
+ process_with_kwargs(:patch, path, *args)
end
# Performs a PUT request with the given parameters. See +#get+ for more
# details.
- def put(path, parameters = nil, headers_or_env = nil)
- process :put, path, parameters, headers_or_env
+ def put(path, *args)
+ process_with_kwargs(:put, path, *args)
end
# Performs a DELETE request with the given parameters. See +#get+ for
# more details.
- def delete(path, parameters = nil, headers_or_env = nil)
- process :delete, path, parameters, headers_or_env
+ def delete(path, *args)
+ process_with_kwargs(:delete, path, *args)
end
# Performs a HEAD request with the given parameters. See +#get+ for more
# details.
- def head(path, parameters = nil, headers_or_env = nil)
- process :head, path, parameters, headers_or_env
+ def head(path, *args)
+ process_with_kwargs(:head, path, *args)
end
# Performs an XMLHttpRequest request with the given parameters, mirroring
@@ -68,11 +76,29 @@ module ActionDispatch
# 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.
- def xml_http_request(request_method, path, parameters = nil, headers_or_env = nil)
- headers_or_env ||= {}
- headers_or_env['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest'
- headers_or_env['HTTP_ACCEPT'] ||= [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ')
- process(request_method, path, parameters, headers_or_env)
+ #
+ # 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)
end
alias xhr :xml_http_request
@@ -89,40 +115,52 @@ module ActionDispatch
# 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.
- def request_via_redirect(http_method, path, parameters = nil, headers_or_env = nil)
- process(http_method, path, parameters, headers_or_env)
+ #
+ # 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, parameters = nil, headers_or_env = nil)
- request_via_redirect(:get, path, parameters, headers_or_env)
+ 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, parameters = nil, headers_or_env = nil)
- request_via_redirect(:post, path, parameters, headers_or_env)
+ 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, parameters = nil, headers_or_env = nil)
- request_via_redirect(:patch, path, parameters, headers_or_env)
+ 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, parameters = nil, headers_or_env = nil)
- request_via_redirect(:put, path, parameters, headers_or_env)
+ 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, parameters = nil, headers_or_env = nil)
- request_via_redirect(:delete, path, parameters, headers_or_env)
+ 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
@@ -185,15 +223,6 @@ module ActionDispatch
super()
@app = app
- # 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)
- singleton_class.class_eval do
- include app.routes.url_helpers
- include app.routes.mounted_helpers
- end
- end
-
reset!
end
@@ -261,20 +290,54 @@ module ActionDispatch
@_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
+ end
+
# Performs the actual request.
- def process(method, path, parameters = nil, headers_or_env = nil)
+ def process(method, path, params: nil, headers: nil, env: nil, xhr: false)
if path =~ %r{://}
location = URI.parse(path)
https! URI::HTTPS === location if location.scheme
- host! "#{location.host}:#{location.port}" if location.host
+ 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 = location.query ? "#{location.path}?#{location.query}" : location.path
end
hostname, port = host.split(':')
- env = {
+ request_env = {
:method => method,
- :params => parameters,
+ :params => params,
"SERVER_NAME" => hostname,
"SERVER_PORT" => port || (https? ? "443" : "80"),
@@ -287,25 +350,37 @@ module ActionDispatch
"CONTENT_TYPE" => "application/x-www-form-urlencoded",
"HTTP_ACCEPT" => accept
}
- # this modifies the passed env directly
- Http::Headers.new(env).merge!(headers_or_env || {})
+
+ if xhr
+ headers ||= {}
+ headers['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest'
+ headers['HTTP_ACCEPT'] ||= [Mime[:js], Mime[:html], Mime[:xml], 'text/xml', '*/*'].join(', ')
+ 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
session = Rack::Test::Session.new(_mock_session)
# NOTE: rack-test v0.5 doesn't build a default uri correctly
# Make sure requested path is always a full uri
- session.request(build_full_uri(path, env), env)
+ session.request(build_full_uri(path, request_env), request_env)
@request_count += 1
@request = ActionDispatch::Request.new(session.last_request.env)
response = _mock_session.last_response
- @response = ActionDispatch::TestResponse.new(response.status, response.headers, response.body)
+ @response = ActionDispatch::TestResponse.from_response(response)
@html_document = nil
@url_options = nil
- @controller = session.last_request.env['action_controller.instance']
+ @controller = @request.controller_instance
- return response.status
+ response.status
end
def build_full_uri(path, env)
@@ -316,23 +391,50 @@ module ActionDispatch
module Runner
include ActionDispatch::Assertions
- def app
- @app ||= nil
+ APP_SESSIONS = {}
+
+ attr_reader :app
+
+ def before_setup # :nodoc:
+ @app = nil
+ @integration_session = nil
+ super
+ end
+
+ def integration_session
+ @integration_session ||= create_session(app)
end
# Reset the current session. This is useful for testing multiple sessions
# in a single test case.
def reset!
- @integration_session = Integration::Session.new(app)
+ @integration_session = create_session(app)
+ end
+
+ 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)
+ include app.routes.url_helpers
+ include app.routes.mounted_helpers
+ end
+ }
+ klass.new(app)
+ end
+
+ def remove! # :nodoc:
+ @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|
define_method(method) do |*args|
- reset! unless integration_session
- reset_template_assertion
- # reset the html_document variable, but only for new get/post calls
- @html_document = nil unless method == 'cookies' || method == 'assigns'
+ # reset the html_document variable, except for cookies/assigns calls
+ unless method == 'cookies' || method == 'assigns'
+ @html_document = nil
+ end
+
integration_session.__send__(method, *args).tap do
copy_session_variables!
end
@@ -358,19 +460,16 @@ module ActionDispatch
# Copy the instance variables from the current session instance into the
# test instance.
def copy_session_variables! #:nodoc:
- return unless integration_session
- %w(controller response request).each do |var|
- instance_variable_set("@#{var}", @integration_session.__send__(var))
- end
+ @controller = @integration_session.controller
+ @response = @integration_session.response
+ @request = @integration_session.request
end
def default_url_options
- reset! unless integration_session
integration_session.default_url_options
end
def default_url_options=(options)
- reset! unless integration_session
integration_session.default_url_options = options
end
@@ -380,7 +479,6 @@ module ActionDispatch
# Delegate unhandled messages to the current session instance.
def method_missing(sym, *args, &block)
- reset! unless integration_session
if integration_session.respond_to?(sym)
integration_session.__send__(sym, *args, &block).tap do
copy_session_variables!
@@ -389,11 +487,6 @@ module ActionDispatch
super
end
end
-
- private
- def integration_session
- @integration_session ||= nil
- end
end
end
@@ -416,8 +509,8 @@ module ActionDispatch
# assert_equal 200, status
#
# # post the login and follow through to the home page
- # post "/login", username: people(:jamis).username,
- # password: people(:jamis).password
+ # post "/login", params: { username: people(:jamis).username,
+ # password: people(:jamis).password }
# follow_redirect!
# assert_equal 200, status
# assert_equal "/home", path
@@ -456,7 +549,7 @@ module ActionDispatch
# end
#
# def speak(room, message)
- # xml_http_request "/say/#{room.id}", message: message
+ # post "/say/#{room.id}", xhr: true, params: { message: message }
# assert(...)
# ...
# end
@@ -466,12 +559,91 @@ module ActionDispatch
# open_session do |sess|
# sess.extend(CustomAssertions)
# who = people(who)
- # sess.post "/login", username: who.username,
- # password: who.password
+ # sess.post "/login", params: { username: who.username,
+ # password: who.password }
# assert(...)
# end
# end
# end
+ #
+ # Another longer example would be:
+ #
+ # A simple integration test that exercises multiple controllers:
+ #
+ # require 'test_helper'
+ #
+ # class UserFlowsTest < ActionDispatch::IntegrationTest
+ # test "login and browse site" do
+ # # login via https
+ # https!
+ # get "/login"
+ # assert_response :success
+ #
+ # post "/login", params: { username: users(:david).username, password: users(:david).password }
+ # follow_redirect!
+ # assert_equal '/welcome', path
+ # assert_equal 'Welcome david!', flash[:notice]
+ #
+ # https!(false)
+ # get "/articles/all"
+ # assert_response :success
+ # assert_select 'h1', 'Articles'
+ # end
+ # end
+ #
+ # As you can see the integration test involves multiple controllers and
+ # exercises the entire stack from database to dispatcher. In addition you can
+ # have multiple session instances open simultaneously in a test and extend
+ # those instances with assertion methods to create a very powerful testing
+ # DSL (domain-specific language) just for your application.
+ #
+ # Here's an example of multiple sessions and custom DSL in an integration test
+ #
+ # require 'test_helper'
+ #
+ # class UserFlowsTest < ActionDispatch::IntegrationTest
+ # test "login and browse site" do
+ # # User david logs in
+ # david = login(:david)
+ # # User guest logs in
+ # guest = login(:guest)
+ #
+ # # Both are now available in different sessions
+ # assert_equal 'Welcome david!', david.flash[:notice]
+ # assert_equal 'Welcome guest!', guest.flash[:notice]
+ #
+ # # User david can browse site
+ # david.browses_site
+ # # User guest can browse site as well
+ # guest.browses_site
+ #
+ # # Continue with other assertions
+ # end
+ #
+ # private
+ #
+ # module CustomDsl
+ # def browses_site
+ # get "/products/all"
+ # assert_response :success
+ # assert_select 'h1', 'Products'
+ # end
+ # end
+ #
+ # def login(user)
+ # open_session do |sess|
+ # sess.extend(CustomDsl)
+ # u = users(user)
+ # sess.https!
+ # sess.post "/login", params: { username: u.username, password: u.password }
+ # assert_equal '/welcome', sess.path
+ # sess.https!(false)
+ # end
+ # end
+ # end
+ #
+ # Consult the Rails Testing Guide for more.
+
class IntegrationTest < ActiveSupport::TestCase
include Integration::Runner
include ActionController::TemplateAssertions
@@ -492,8 +664,11 @@ module ActionDispatch
end
def url_options
- reset! unless integration_session
integration_session.url_options
end
+
+ def document_root_element
+ html_document.root
+ end
end
end
diff --git a/actionpack/lib/action_dispatch/testing/test_process.rb b/actionpack/lib/action_dispatch/testing/test_process.rb
index 630e6a9b78..c28d701b48 100644
--- a/actionpack/lib/action_dispatch/testing/test_process.rb
+++ b/actionpack/lib/action_dispatch/testing/test_process.rb
@@ -5,9 +5,9 @@ require 'active_support/core_ext/hash/indifferent_access'
module ActionDispatch
module TestProcess
def assigns(key = nil)
- assigns = {}.with_indifferent_access
- @controller.view_assigns.each { |k, v| assigns.regular_writer(k, v) }
- key.nil? ? assigns : assigns[key]
+ raise NoMethodError,
+ "assigns has been extracted to a gem. To continue using it,
+ add `gem 'rails-controller-testing'` to your Gemfile."
end
def session
@@ -19,7 +19,7 @@ module ActionDispatch
end
def cookies
- @request.cookie_jar
+ @cookie_jar ||= Cookies::CookieJar.build(@request, @request.cookies)
end
def redirect_to_url
diff --git a/actionpack/lib/action_dispatch/testing/test_request.rb b/actionpack/lib/action_dispatch/testing/test_request.rb
index de3dc5f924..ad1a7f7109 100644
--- a/actionpack/lib/action_dispatch/testing/test_request.rb
+++ b/actionpack/lib/action_dispatch/testing/test_request.rb
@@ -4,19 +4,22 @@ 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'
+ 'HTTP_HOST' => 'test.host',
+ 'REMOTE_ADDR' => '0.0.0.0',
+ 'HTTP_USER_AGENT' => 'Rails Testing',
)
- def self.new(env = {})
- super
+ # 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
+ new(default_env.merge(env))
end
- def initialize(env = {})
- env = Rails.application.env_config.merge(env) if defined?(Rails.application) && Rails.application
- super(default_env.merge(env))
+ def self.default_env
+ DEFAULT_ENV
end
+ private_class_method :default_env
def request_method=(method)
@env['REQUEST_METHOD'] = method.to_s.upcase
@@ -60,19 +63,7 @@ module ActionDispatch
def accept=(mime_types)
@env.delete('action_dispatch.request.accepts')
- @env['HTTP_ACCEPT'] = Array(mime_types).collect { |mime_type| mime_type.to_s }.join(",")
- end
-
- alias :rack_cookies :cookies
-
- def cookies
- @cookies ||= {}.with_indifferent_access
- end
-
- private
-
- def default_env
- DEFAULT_ENV
+ @env['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 82039e72e7..4b79a90242 100644
--- a/actionpack/lib/action_dispatch/testing/test_response.rb
+++ b/actionpack/lib/action_dispatch/testing/test_response.rb
@@ -7,11 +7,7 @@ module ActionDispatch
# See Response for more information on controller response objects.
class TestResponse < Response
def self.from_response(response)
- new.tap do |resp|
- resp.status = response.status
- resp.headers = response.headers
- resp.body = response.body
- end
+ new response.status, response.headers, response.body
end
# Was the response successful?
@@ -20,9 +16,6 @@ module ActionDispatch
# Was the URL not found?
alias_method :missing?, :not_found?
- # Were we redirected?
- alias_method :redirect?, :redirection?
-
# Was there a server-side error?
alias_method :error?, :server_error?
end
diff --git a/actionpack/lib/action_pack.rb b/actionpack/lib/action_pack.rb
index 77f656d6f1..f664dab620 100644
--- a/actionpack/lib/action_pack.rb
+++ b/actionpack/lib/action_pack.rb
@@ -1,5 +1,5 @@
#--
-# Copyright (c) 2004-2014 David Heinemeier Hansson
+# Copyright (c) 2004-2015 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
diff --git a/actionpack/lib/action_pack/gem_version.rb b/actionpack/lib/action_pack/gem_version.rb
index beaf35d3da..255ac9f4ed 100644
--- a/actionpack/lib/action_pack/gem_version.rb
+++ b/actionpack/lib/action_pack/gem_version.rb
@@ -1,12 +1,12 @@
module ActionPack
- # Returns the version of the currently loaded ActionPack as a <tt>Gem::Version</tt>
+ # Returns the version of the currently loaded Action Pack as a <tt>Gem::Version</tt>
def self.gem_version
Gem::Version.new VERSION::STRING
end
module VERSION
- MAJOR = 4
- MINOR = 2
+ MAJOR = 5
+ MINOR = 0
TINY = 0
PRE = "alpha"