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.rb46
-rw-r--r--actionpack/lib/abstract_controller/callbacks.rb21
-rw-r--r--actionpack/lib/abstract_controller/helpers.rb31
-rw-r--r--actionpack/lib/abstract_controller/layouts.rb38
-rw-r--r--actionpack/lib/abstract_controller/railties/routes_helpers.rb18
-rw-r--r--actionpack/lib/abstract_controller/rendering.rb56
-rw-r--r--actionpack/lib/abstract_controller/url_for.rb27
-rw-r--r--actionpack/lib/abstract_controller/view_paths.rb16
-rw-r--r--actionpack/lib/action_controller.rb2
-rw-r--r--actionpack/lib/action_controller/base.rb172
-rw-r--r--actionpack/lib/action_controller/caching.rb4
-rw-r--r--actionpack/lib/action_controller/caching/actions.rb94
-rw-r--r--actionpack/lib/action_controller/caching/fragments.rb60
-rw-r--r--actionpack/lib/action_controller/caching/pages.rb29
-rw-r--r--actionpack/lib/action_controller/deprecated.rb2
-rw-r--r--actionpack/lib/action_controller/deprecated/base.rb133
-rw-r--r--actionpack/lib/action_controller/deprecated/dispatcher.rb28
-rw-r--r--actionpack/lib/action_controller/log_subscriber.rb8
-rw-r--r--actionpack/lib/action_controller/metal.rb101
-rw-r--r--actionpack/lib/action_controller/metal/compatibility.rb9
-rw-r--r--actionpack/lib/action_controller/metal/conditional_get.rb18
-rw-r--r--actionpack/lib/action_controller/metal/head.rb8
-rw-r--r--actionpack/lib/action_controller/metal/helpers.rb49
-rw-r--r--actionpack/lib/action_controller/metal/hide_actions.rb5
-rw-r--r--actionpack/lib/action_controller/metal/http_authentication.rb69
-rw-r--r--actionpack/lib/action_controller/metal/implicit_render.rb4
-rw-r--r--actionpack/lib/action_controller/metal/instrumentation.rb2
-rw-r--r--actionpack/lib/action_controller/metal/mime_responds.rb22
-rw-r--r--actionpack/lib/action_controller/metal/redirecting.rb10
-rw-r--r--actionpack/lib/action_controller/metal/renderers.rb75
-rw-r--r--actionpack/lib/action_controller/metal/rendering.rb50
-rw-r--r--actionpack/lib/action_controller/metal/request_forgery_protection.rb85
-rw-r--r--actionpack/lib/action_controller/metal/responder.rb46
-rw-r--r--actionpack/lib/action_controller/metal/streaming.rb12
-rw-r--r--actionpack/lib/action_controller/metal/testing.rb9
-rw-r--r--actionpack/lib/action_controller/metal/url_for.rb26
-rw-r--r--actionpack/lib/action_controller/middleware.rb2
-rw-r--r--actionpack/lib/action_controller/railtie.rb55
-rw-r--r--actionpack/lib/action_controller/railties/paths.rb32
-rw-r--r--actionpack/lib/action_controller/test_case.rb65
-rw-r--r--actionpack/lib/action_controller/vendor/html-scanner/html/document.rb4
-rw-r--r--actionpack/lib/action_controller/vendor/html-scanner/html/node.rb82
-rw-r--r--actionpack/lib/action_controller/vendor/html-scanner/html/sanitizer.rb56
-rw-r--r--actionpack/lib/action_controller/vendor/html-scanner/html/selector.rb2
-rw-r--r--actionpack/lib/action_controller/vendor/html-scanner/html/tokenizer.rb16
-rw-r--r--actionpack/lib/action_dispatch.rb4
-rw-r--r--actionpack/lib/action_dispatch/http/cache.rb41
-rw-r--r--actionpack/lib/action_dispatch/http/filter_parameters.rb21
-rw-r--r--actionpack/lib/action_dispatch/http/mime_negotiation.rb8
-rw-r--r--actionpack/lib/action_dispatch/http/mime_type.rb74
-rw-r--r--actionpack/lib/action_dispatch/http/parameters.rb4
-rw-r--r--actionpack/lib/action_dispatch/http/rack_cache.rb58
-rw-r--r--actionpack/lib/action_dispatch/http/request.rb77
-rw-r--r--actionpack/lib/action_dispatch/http/response.rb26
-rw-r--r--actionpack/lib/action_dispatch/http/upload.rb60
-rw-r--r--actionpack/lib/action_dispatch/http/url.rb142
-rw-r--r--actionpack/lib/action_dispatch/middleware/callbacks.rb33
-rw-r--r--actionpack/lib/action_dispatch/middleware/cookies.rb70
-rw-r--r--actionpack/lib/action_dispatch/middleware/flash.rb9
-rw-r--r--actionpack/lib/action_dispatch/middleware/reloader.rb76
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/abstract_store.rb280
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/cookie_store.rb64
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb53
-rw-r--r--actionpack/lib/action_dispatch/middleware/show_exceptions.rb48
-rw-r--r--actionpack/lib/action_dispatch/middleware/stack.rb67
-rw-r--r--actionpack/lib/action_dispatch/middleware/static.rb64
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb4
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.erb6
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.erb4
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.erb8
-rw-r--r--actionpack/lib/action_dispatch/railtie.rb7
-rw-r--r--actionpack/lib/action_dispatch/routing.rb117
-rw-r--r--actionpack/lib/action_dispatch/routing/deprecated_mapper.rb525
-rw-r--r--actionpack/lib/action_dispatch/routing/mapper.rb1077
-rw-r--r--actionpack/lib/action_dispatch/routing/polymorphic_routes.rb64
-rw-r--r--actionpack/lib/action_dispatch/routing/redirection.rb110
-rw-r--r--actionpack/lib/action_dispatch/routing/route.rb27
-rw-r--r--actionpack/lib/action_dispatch/routing/route_set.rb206
-rw-r--r--actionpack/lib/action_dispatch/routing/routes_proxy.rb35
-rw-r--r--actionpack/lib/action_dispatch/routing/url_for.rb33
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions/dom.rb2
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions/response.rb20
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions/routing.rb85
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions/selector.rb27
-rw-r--r--actionpack/lib/action_dispatch/testing/integration.rb53
-rw-r--r--actionpack/lib/action_dispatch/testing/performance_test.rb4
-rw-r--r--actionpack/lib/action_dispatch/testing/test_process.rb2
-rw-r--r--actionpack/lib/action_dispatch/testing/test_request.rb12
-rw-r--r--actionpack/lib/action_dispatch/testing/test_response.rb115
-rw-r--r--actionpack/lib/action_pack.rb2
-rw-r--r--actionpack/lib/action_pack/version.rb6
-rw-r--r--actionpack/lib/action_view.rb53
-rw-r--r--actionpack/lib/action_view/base.rb67
-rw-r--r--actionpack/lib/action_view/helpers.rb5
-rw-r--r--actionpack/lib/action_view/helpers/active_model_helper.rb23
-rw-r--r--actionpack/lib/action_view/helpers/asset_tag_helper.rb494
-rw-r--r--actionpack/lib/action_view/helpers/asset_tag_helpers/asset_include_tag.rb146
-rw-r--r--actionpack/lib/action_view/helpers/asset_tag_helpers/asset_paths.rb153
-rw-r--r--actionpack/lib/action_view/helpers/asset_tag_helpers/javascript_tag_helpers.rb184
-rw-r--r--actionpack/lib/action_view/helpers/asset_tag_helpers/stylesheet_tag_helpers.rb147
-rw-r--r--actionpack/lib/action_view/helpers/atom_feed_helper.rb2
-rw-r--r--actionpack/lib/action_view/helpers/cache_helper.rb26
-rw-r--r--actionpack/lib/action_view/helpers/capture_helper.rb5
-rw-r--r--actionpack/lib/action_view/helpers/csrf_helper.rb28
-rw-r--r--actionpack/lib/action_view/helpers/date_helper.rb70
-rw-r--r--actionpack/lib/action_view/helpers/debug_helper.rb2
-rw-r--r--actionpack/lib/action_view/helpers/form_helper.rb307
-rw-r--r--actionpack/lib/action_view/helpers/form_options_helper.rb60
-rw-r--r--actionpack/lib/action_view/helpers/form_tag_helper.rb114
-rw-r--r--actionpack/lib/action_view/helpers/javascript_helper.rb3
-rw-r--r--actionpack/lib/action_view/helpers/number_helper.rb94
-rw-r--r--actionpack/lib/action_view/helpers/output_safety_helper.rb38
-rw-r--r--actionpack/lib/action_view/helpers/prototype_helper.rb26
-rw-r--r--actionpack/lib/action_view/helpers/raw_output_helper.rb18
-rw-r--r--actionpack/lib/action_view/helpers/record_tag_helper.rb6
-rw-r--r--actionpack/lib/action_view/helpers/sanitize_helper.rb4
-rw-r--r--actionpack/lib/action_view/helpers/tag_helper.rb46
-rw-r--r--actionpack/lib/action_view/helpers/text_helper.rb62
-rw-r--r--actionpack/lib/action_view/helpers/translation_helper.rb50
-rw-r--r--actionpack/lib/action_view/helpers/url_helper.rb136
-rw-r--r--actionpack/lib/action_view/locale/en.yml2
-rw-r--r--actionpack/lib/action_view/log_subscriber.rb2
-rw-r--r--actionpack/lib/action_view/lookup_context.rb55
-rw-r--r--actionpack/lib/action_view/partials.rb (renamed from actionpack/lib/action_view/render/partials.rb)216
-rw-r--r--actionpack/lib/action_view/path_set.rb (renamed from actionpack/lib/action_view/paths.rb)16
-rw-r--r--actionpack/lib/action_view/railtie.rb14
-rw-r--r--actionpack/lib/action_view/render/layouts.rb79
-rw-r--r--actionpack/lib/action_view/render/rendering.rb67
-rw-r--r--actionpack/lib/action_view/renderer/abstract_renderer.rb37
-rw-r--r--actionpack/lib/action_view/renderer/partial_renderer.rb167
-rw-r--r--actionpack/lib/action_view/renderer/template_renderer.rb98
-rw-r--r--actionpack/lib/action_view/rendering.rb106
-rw-r--r--actionpack/lib/action_view/template.rb155
-rw-r--r--actionpack/lib/action_view/template/error.rb19
-rw-r--r--actionpack/lib/action_view/template/handler.rb12
-rw-r--r--actionpack/lib/action_view/template/handlers.rb20
-rw-r--r--actionpack/lib/action_view/template/handlers/builder.rb8
-rw-r--r--actionpack/lib/action_view/template/handlers/erb.rb43
-rw-r--r--actionpack/lib/action_view/template/handlers/rjs.rb12
-rw-r--r--actionpack/lib/action_view/template/resolver.rb221
-rw-r--r--actionpack/lib/action_view/template/text.rb4
-rw-r--r--actionpack/lib/action_view/test_case.rb67
-rw-r--r--actionpack/lib/action_view/testing/resolvers.rb31
144 files changed, 5341 insertions, 4010 deletions
diff --git a/actionpack/lib/abstract_controller.rb b/actionpack/lib/abstract_controller.rb
index c565c940a1..cc5878c88e 100644
--- a/actionpack/lib/abstract_controller.rb
+++ b/actionpack/lib/abstract_controller.rb
@@ -2,6 +2,7 @@ activesupport_path = File.expand_path('../../../activesupport/lib', __FILE__)
$:.unshift(activesupport_path) if File.directory?(activesupport_path) && !$:.include?(activesupport_path)
require 'action_pack'
+require 'active_support/concern'
require 'active_support/ruby/shim'
require 'active_support/dependencies/autoload'
require 'active_support/core_ext/class/attribute'
@@ -23,4 +24,5 @@ module AbstractController
autoload :Translation
autoload :AssetPaths
autoload :ViewPaths
+ autoload :UrlFor
end
diff --git a/actionpack/lib/abstract_controller/base.rb b/actionpack/lib/abstract_controller/base.rb
index db0a6736e0..c384fd0978 100644
--- a/actionpack/lib/abstract_controller/base.rb
+++ b/actionpack/lib/abstract_controller/base.rb
@@ -6,9 +6,14 @@ module AbstractController
class Error < StandardError; end
class ActionNotFound < StandardError; end
+ # <tt>AbstractController::Base</tt> 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.
class Base
attr_internal :response_body
attr_internal :action_name
+ attr_internal :formats
include ActiveSupport::Configurable
extend ActiveSupport::DescendantsTracker
@@ -26,23 +31,21 @@ module AbstractController
# A list of all internal methods for a controller. This finds the first
# abstract superclass of a controller, and gets a list of all public
# instance methods on that abstract class. Public instance methods of
- # a controller would normally be considered action methods, so we
- # are removing those methods on classes declared as abstract
- # (ActionController::Metal and ActionController::Base are defined
- # as abstract)
+ # a controller would normally be considered action methods, so methods
+ # declared on abstract classes are being removed.
+ # (ActionController::Metal and ActionController::Base are defined as abstract)
def internal_methods
controller = self
controller = controller.superclass until controller.abstract?
controller.public_instance_methods(true)
end
- # The list of hidden actions to an empty Array. Defaults to an
- # empty Array. This can be modified by other modules or subclasses
+ # The list of hidden actions to an empty array. Defaults to an
+ # empty array. This can be modified by other modules or subclasses
# to specify particular actions as hidden.
#
# ==== Returns
- # Array[String]:: An array of method names that should not be
- # considered actions.
+ # * <tt>array</tt> - An array of method names that should not be considered actions.
def hidden_actions
[]
end
@@ -54,18 +57,17 @@ module AbstractController
# itself. Finally, #hidden_actions are removed.
#
# ==== Returns
- # Array[String]:: A list of all methods that should be considered
- # actions.
+ # * <tt>array</tt> - A list of all methods that should be considered actions.
def action_methods
@action_methods ||= begin
# All public instance methods of this class, including ancestors
- methods = public_instance_methods(true).map { |m| m.to_s }.to_set -
+ methods = (public_instance_methods(true) -
# Except for public instance methods of Base and its ancestors
- internal_methods.map { |m| m.to_s } +
+ internal_methods +
# Be sure to include shadowed public instance methods of this class
- public_instance_methods(false).map { |m| m.to_s } -
+ public_instance_methods(false)).uniq.map { |x| x.to_s } -
# And always exclude explicitly hidden actions
- hidden_actions
+ hidden_actions.to_a
# Clear out AS callback method pollution
methods.reject { |method| method =~ /_one_time_conditions/ }
@@ -84,7 +86,7 @@ module AbstractController
# controller_name.
#
# ==== Returns
- # String
+ # * <tt>string</tt>
def controller_path
@controller_path ||= name.sub(/Controller$/, '').underscore unless anonymous?
end
@@ -104,12 +106,12 @@ module AbstractController
# ActionNotFound error is raised.
#
# ==== Returns
- # self
+ # * <tt>self</tt>
def process(action, *args)
@_action_name = action_name = action.to_s
unless action_name = method_for_action(action_name)
- raise ActionNotFound, "The action '#{action}' could not be found for #{self.class.name}"
+ raise ActionNotFound, "The action '#{action}' could not be found for #{self.class.name}"
end
@_response_body = nil
@@ -133,10 +135,10 @@ module AbstractController
# can be considered an action.
#
# ==== Parameters
- # name<String>:: The name of an action to be tested
+ # * <tt>name</tt> - The name of an action to be tested
#
# ==== Returns
- # TrueClass, FalseClass
+ # * <tt>TrueClass</tt>, <tt>FalseClass</tt>
def action_method?(name)
self.class.action_methods.include?(name)
end
@@ -180,11 +182,11 @@ module AbstractController
# returns nil, an ActionNotFound exception will be raised.
#
# ==== Parameters
- # action_name<String>:: An action name to find a method name for
+ # * <tt>action_name</tt> - An action name to find a method name for
#
# ==== Returns
- # String:: The name of the method that handles the action
- # nil:: No method name could be found. Raise ActionNotFound.
+ # * <tt>string</tt> - The name of the method that handles the action
+ # * <tt>nil</tt> - No method name could be found. Raise ActionNotFound.
def method_for_action(action_name)
if action_method?(action_name) then action_name
elsif respond_to?(:action_missing, true) then "_handle_action_missing"
diff --git a/actionpack/lib/abstract_controller/callbacks.rb b/actionpack/lib/abstract_controller/callbacks.rb
index 67efeb7063..1943ca4436 100644
--- a/actionpack/lib/abstract_controller/callbacks.rb
+++ b/actionpack/lib/abstract_controller/callbacks.rb
@@ -13,8 +13,8 @@ module AbstractController
# Override AbstractController::Base's process_action to run the
# process_action callbacks around the normal behavior.
- def process_action(method_name)
- run_callbacks(:process_action, method_name) do
+ def process_action(method_name, *args)
+ run_callbacks(:process_action, action_name) do
super
end
end
@@ -28,9 +28,8 @@ module AbstractController
# a Rails process.
#
# ==== Options
- # :only<#to_s>:: The callback should be run only for this action
- # :except<#to_s>:: The callback should be run for all actions
- # except this action
+ # * <tt>only</tt> - The callback should be run only for this action
+ # * <tt>except<tt> - The callback should be run for all actions except this action
def _normalize_callback_options(options)
if only = options[:only]
only = Array(only).map {|o| "action_name == '#{o}'"}.join(" || ")
@@ -45,7 +44,7 @@ module AbstractController
# Skip before, after, and around filters matching any of the names
#
# ==== Parameters
- # *names<Object>:: A list of valid names that could be used for
+ # * <tt>names</tt> - A list of valid names that could be used for
# callbacks. Note that skipping uses Ruby equality, so it's
# impossible to skip a callback defined using an anonymous proc
# using #skip_filter
@@ -60,13 +59,13 @@ module AbstractController
# the normalization across several methods that use it.
#
# ==== Parameters
- # callbacks<Array[*Object, Hash]>:: A list of callbacks, with an optional
+ # * <tt>callbacks</tt> - An array of callbacks, with an optional
# options hash as the last parameter.
- # block<Proc>:: A proc that should be added to the callbacks.
+ # * <tt>block</tt> - A proc that should be added to the callbacks.
#
# ==== Block Parameters
- # name<Symbol>:: The callback to be added
- # options<Hash>:: A list of options to be used when adding the callback
+ # * <tt>name</tt> - The callback to be added
+ # * <tt>options</tt> - A hash of options to be used when adding the callback
def _insert_callbacks(callbacks, block)
options = callbacks.last.is_a?(Hash) ? callbacks.pop : {}
_normalize_callback_options(options)
@@ -84,6 +83,7 @@ module AbstractController
# for details on the allowed parameters.
def #{filter}_filter(*names, &blk)
_insert_callbacks(names, blk) do |name, options|
+ options[:if] = (Array.wrap(options[:if]) << "!halted") if #{filter == :after}
set_callback(:process_action, :#{filter}, name, options)
end
end
@@ -92,6 +92,7 @@ module AbstractController
# for details on the allowed parameters.
def prepend_#{filter}_filter(*names, &blk)
_insert_callbacks(names, blk) do |name, options|
+ options[:if] = (Array.wrap(options[:if]) << "!halted") if #{filter == :after}
set_callback(:process_action, :#{filter}, name, options.merge(:prepend => true))
end
end
diff --git a/actionpack/lib/abstract_controller/helpers.rb b/actionpack/lib/abstract_controller/helpers.rb
index 4374b439d0..20f8601a8e 100644
--- a/actionpack/lib/abstract_controller/helpers.rb
+++ b/actionpack/lib/abstract_controller/helpers.rb
@@ -9,6 +9,9 @@ module AbstractController
included do
class_attribute :_helpers
self._helpers = Module.new
+
+ class_attribute :_helper_methods
+ self._helper_methods = Array.new
end
module ClassMethods
@@ -40,10 +43,13 @@ module AbstractController
# <% if logged_in? -%>Welcome, <%= current_user.name %><% end -%>
#
# ==== Parameters
- # meths<Array[#to_s]>:: The name of a method on the controller
+ # * <tt>method[, method]</tt> - A name or names of a method on the controller
# to be made available on the view.
def helper_method(*meths)
- meths.flatten.each do |meth|
+ meths.flatten!
+ self._helper_methods += meths
+
+ meths.each do |meth|
_helpers.class_eval <<-ruby_eval, __FILE__, __LINE__ + 1
def #{meth}(*args, &blk)
controller.send(%(#{meth}), *args, &blk)
@@ -55,8 +61,8 @@ module AbstractController
# The +helper+ class method can take a series of helper module names, a block, or both.
#
# ==== Parameters
- # *args<Array[Module, Symbol, String, :all]>
- # block<Block>:: A block defining helper methods
+ # * <tt>*args</tt> - Module, Symbol, String, :all
+ # * <tt>block</tt> - A block defining helper methods
#
# ==== Examples
# When the argument is a module it will be included directly in the template class.
@@ -95,12 +101,23 @@ module AbstractController
_helpers.module_eval(&block) if block_given?
end
+ # Clears up all existing helpers in this class, only keeping the helper
+ # with the same name as this class.
+ def clear_helpers
+ inherited_helper_methods = _helper_methods
+ self._helpers = Module.new
+ self._helper_methods = Array.new
+
+ inherited_helper_methods.each { |meth| helper_method meth }
+ default_helper_module! unless anonymous?
+ end
+
private
# Makes all the (instance) methods in the helper module available to templates
# rendered through this controller.
#
# ==== Parameters
- # mod<Module>:: The module to include into the current helper module
+ # * <tt>module</tt> - The module to include into the current helper module
# for the class
def add_template_helper(mod)
_helpers.module_eval { include mod }
@@ -118,10 +135,10 @@ module AbstractController
# are returned.
#
# ==== Parameters
- # args<Array[String, Symbol, Module]>:: A list of helpers
+ # * <tt>args</tt> - An array of helpers
#
# ==== Returns
- # Array[Module]:: A normalized list of modules for the list of
+ # * <tt>Array</tt> - A normalized list of modules for the list of
# helpers provided.
def modules_for_helpers(args)
args.flatten.map! do |arg|
diff --git a/actionpack/lib/abstract_controller/layouts.rb b/actionpack/lib/abstract_controller/layouts.rb
index 5cd7a90ab5..4ee54474cc 100644
--- a/actionpack/lib/abstract_controller/layouts.rb
+++ b/actionpack/lib/abstract_controller/layouts.rb
@@ -114,11 +114,13 @@ module AbstractController
#
# class WeblogController < ActionController::Base
# layout proc{ |controller| controller.logged_in? ? "writer_layout" : "reader_layout" }
+ # end
#
# Of course, the most common way of specifying a layout is still just as a plain template name:
#
# class WeblogController < ActionController::Base
# layout "weblog_standard"
+ # end
#
# If no directory is specified for the template name, the template will by default be looked for in <tt>app/views/layouts/</tt>.
# Otherwise, it will be looked up relative to the template root.
@@ -183,7 +185,7 @@ module AbstractController
# layout.
#
# ==== Returns
- # Boolean:: True if the action has a layout, false otherwise.
+ # * <tt> Boolean</tt> - True if the action has a layout, false otherwise.
def action_has_layout?
return unless super
@@ -209,11 +211,11 @@ module AbstractController
# true:: raise an ArgumentError
#
# ==== Parameters
- # layout<String, Symbol, false)>:: The layout to use.
+ # * <tt>String, Symbol, false</tt> - The layout to use.
#
# ==== Options (conditions)
- # :only<#to_s, Array[#to_s]>:: A list of actions to apply this layout to.
- # :except<#to_s, Array[#to_s]>:: Apply this layout to all actions but this one
+ # * :only - A list of actions to apply this layout to.
+ # * :except - Apply this layout to all actions but this one.
def layout(layout, conditions = {})
include LayoutConditions unless conditions.empty?
@@ -228,18 +230,15 @@ module AbstractController
# value of this method.
#
# ==== Returns
- # String:: A template name
+ # * <tt>String</tt> - A template name
def _implied_layout_name
controller_path
end
- # Takes the specified layout and creates a _layout method to be called
- # by _default_layout
+ # Creates a _layout method to be called by _default_layout .
#
- # If there is no explicit layout specified:
- # If a layout is found in the view paths with the controller's
- # name, return that string. Otherwise, use the superclass'
- # layout (which might also be implied)
+ # If a layout is not explicitly mentioned then look for a layout with the controller's name.
+ # if nothing is found then try same procedure to find super class's layout.
def _write_layout_method
remove_possible_method(:_layout)
@@ -266,11 +265,11 @@ module AbstractController
raise ArgumentError, "Layouts must be specified as a String, Symbol, false, or nil"
when nil
if name
- _prefix = "layouts" unless _implied_layout_name =~ /\blayouts/
+ _prefixes = _implied_layout_name =~ /\blayouts/ ? [] : ["layouts"]
self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
def _layout
- if template_exists?("#{_implied_layout_name}", #{_prefix.inspect})
+ if template_exists?("#{_implied_layout_name}", #{_prefixes.inspect})
"#{_implied_layout_name}"
else
super
@@ -313,8 +312,8 @@ module AbstractController
# the name type.
#
# ==== Parameters
- # name<String|TrueClass|FalseClass|Symbol>:: The name of the template
- # details<Hash{Symbol => Object}>:: A list of details to restrict
+ # * <tt>name</tt> - The name of the template
+ # * <tt>details</tt> - A list of details to restrict
# the lookup to. By default, layout lookup is limited to the
# formats specified for the current request.
def _layout_for_option(name)
@@ -333,20 +332,19 @@ module AbstractController
# Optionally raises an exception if the layout could not be found.
#
# ==== Parameters
- # details<Hash>:: A list of details to restrict the search by. This
+ # * <tt>details</tt> - A list of details to restrict the search by. This
# might include details like the format or locale of the template.
- # require_layout<Boolean>:: If this is true, raise an ArgumentError
+ # * <tt>require_logout</tt> - If this is true, raise an ArgumentError
# with details about the fact that the exception could not be
# found (defaults to false)
#
# ==== Returns
- # Template:: The template object for the default layout (or nil)
+ # * <tt>template</tt> - The template object for the default layout (or nil)
def _default_layout(require_layout = false)
begin
layout_name = _layout if action_has_layout?
rescue NameError => e
- raise NoMethodError,
- "You specified #{@_layout.inspect} as the layout, but no such method was found"
+ raise e, "Could not render layout: #{e.message}"
end
if require_layout && action_has_layout? && !layout_name
diff --git a/actionpack/lib/abstract_controller/railties/routes_helpers.rb b/actionpack/lib/abstract_controller/railties/routes_helpers.rb
new file mode 100644
index 0000000000..dec1e9d6d9
--- /dev/null
+++ b/actionpack/lib/abstract_controller/railties/routes_helpers.rb
@@ -0,0 +1,18 @@
+module AbstractController
+ module Railties
+ module RoutesHelpers
+ def self.with(routes)
+ Module.new do
+ define_method(:inherited) do |klass|
+ super(klass)
+ if namespace = klass.parents.detect {|m| m.respond_to?(:_railtie) }
+ klass.send(:include, namespace._railtie.routes.url_helpers)
+ else
+ klass.send(:include, routes.url_helpers)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/abstract_controller/rendering.rb b/actionpack/lib/abstract_controller/rendering.rb
index b81d5954eb..691310d5d2 100644
--- a/actionpack/lib/abstract_controller/rendering.rb
+++ b/actionpack/lib/abstract_controller/rendering.rb
@@ -12,16 +12,16 @@ module AbstractController
# This is a class to fix I18n global state. Whenever you provide I18n.locale during a request,
# it will trigger the lookup_context and consequently expire the cache.
- # TODO Add some deprecation warnings to remove I18n.locale from controllers
class I18nProxy < ::I18n::Config #:nodoc:
- attr_reader :i18n_config, :lookup_context
+ attr_reader :original_config, :lookup_context
- def initialize(i18n_config, lookup_context)
- @i18n_config, @lookup_context = i18n_config, lookup_context
+ def initialize(original_config, lookup_context)
+ original_config = original_config.original_config if original_config.respond_to?(:original_config)
+ @original_config, @lookup_context = original_config, lookup_context
end
def locale
- @i18n_config.locale
+ @original_config.locale
end
def locale=(value)
@@ -47,19 +47,34 @@ module AbstractController
@view_context_class ||= begin
controller = self
Class.new(ActionView::Base) do
+ if controller.respond_to?(:_routes) && controller._routes
+ include controller._routes.url_helpers
+ include controller._routes.mounted_helpers
+ end
+
if controller.respond_to?(:_helpers)
include controller._helpers
- if controller.respond_to?(:_routes)
- include controller._routes.url_helpers
- end
-
# TODO: Fix RJS to not require this
self.helpers = controller._helpers
end
end
end
end
+
+ def parent_prefixes
+ @parent_prefixes ||= begin
+ parent_controller = superclass
+ prefixes = []
+
+ until parent_controller.abstract?
+ prefixes << parent_controller.controller_path
+ parent_controller = parent_controller.superclass
+ end
+
+ prefixes
+ end
+ end
end
attr_writer :view_context_class
@@ -98,7 +113,7 @@ module AbstractController
def render_to_string(*args, &block)
options = _normalize_args(*args, &block)
_normalize_options(options)
- render_to_body(options)
+ render_to_body(options).tap { self.response_body = nil }
end
# Raw rendering of a template to a Rack-compatible body.
@@ -114,12 +129,15 @@ module AbstractController
view_context.render(options)
end
- # The prefix used in render "foo" shortcuts.
- def _prefix
- controller_path
+ # The prefixes used in render "foo" shortcuts.
+ def _prefixes
+ @_prefixes ||= begin
+ parent_prefixes = self.class.parent_prefixes
+ parent_prefixes.dup.unshift(controller_path)
+ end
end
- private
+ private
# This method should return a hash with assigns.
# You can overwrite this configuration per controller.
@@ -128,7 +146,7 @@ module AbstractController
hash = {}
variables = instance_variable_names
variables -= protected_instance_variables if respond_to?(:protected_instance_variables)
- variables.each { |name| hash[name.to_s[1..-1]] = instance_variable_get(name) }
+ variables.each { |name| hash[name.to_s[1, name.length]] = instance_variable_get(name) }
hash
end
@@ -138,13 +156,13 @@ module AbstractController
case action
when NilClass
when Hash
- options, action = action, nil
+ options = action
when String, Symbol
action = action.to_s
key = action.include?(?/) ? :file : :action
options[key] = action
else
- options.merge!(:partial => action)
+ options[:partial] = action
end
options
@@ -155,8 +173,8 @@ module AbstractController
options[:partial] = action_name
end
- if (options.keys & [:partial, :file, :template]).empty?
- options[:prefix] ||= _prefix
+ if (options.keys & [:partial, :file, :template, :once]).empty?
+ options[:prefixes] ||= _prefixes
end
options[:template] ||= (options[:action] || action_name).to_s
diff --git a/actionpack/lib/abstract_controller/url_for.rb b/actionpack/lib/abstract_controller/url_for.rb
new file mode 100644
index 0000000000..e5d5bef6b4
--- /dev/null
+++ b/actionpack/lib/abstract_controller/url_for.rb
@@ -0,0 +1,27 @@
+module AbstractController
+ module UrlFor
+ extend ActiveSupport::Concern
+ include ActionDispatch::Routing::UrlFor
+
+ def _routes
+ raise "In order to use #url_for, you must include routing helpers explicitly. " \
+ "For instance, `include Rails.application.routes.url_helpers"
+ end
+
+ module ClassMethods
+ def _routes
+ nil
+ end
+
+ def action_methods
+ @action_methods ||= begin
+ if _routes
+ super - _routes.named_routes.helper_names
+ else
+ super
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/abstract_controller/view_paths.rb b/actionpack/lib/abstract_controller/view_paths.rb
index b552a649d1..cea0f5ad1e 100644
--- a/actionpack/lib/abstract_controller/view_paths.rb
+++ b/actionpack/lib/abstract_controller/view_paths.rb
@@ -34,9 +34,9 @@ module AbstractController
# Append a path to the list of view paths for this controller.
#
# ==== Parameters
- # path<String, ViewPath>:: If a String is provided, it gets converted into
- # the default view path. You may also provide a custom view path
- # (see ActionView::ViewPathSet for more information)
+ # * <tt>path</tt> - If a String is provided, it gets converted into
+ # the default view path. You may also provide a custom view path
+ # (see ActionView::PathSet for more information)
def append_view_path(path)
self.view_paths = view_paths.dup + Array(path)
end
@@ -44,9 +44,9 @@ module AbstractController
# Prepend a path to the list of view paths for this controller.
#
# ==== Parameters
- # path<String, ViewPath>:: If a String is provided, it gets converted into
- # the default view path. You may also provide a custom view path
- # (see ActionView::ViewPathSet for more information)
+ # * <tt>path</tt> - If a String is provided, it gets converted into
+ # the default view path. You may also provide a custom view path
+ # (see ActionView::PathSet for more information)
def prepend_view_path(path)
self.view_paths = Array(path) + view_paths.dup
end
@@ -59,8 +59,8 @@ module AbstractController
# Set the view paths.
#
# ==== Parameters
- # paths<ViewPathSet, Object>:: If a ViewPathSet is provided, use that;
- # otherwise, process the parameter into a ViewPathSet.
+ # * <tt>paths</tt> - If a PathSet is provided, use that;
+ # otherwise, process the parameter into a PathSet.
def view_paths=(paths)
self._view_paths = ActionView::Base.process_view_paths(paths)
self._view_paths.freeze
diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb
index ca0e5d6ff6..5b81cd39f4 100644
--- a/actionpack/lib/action_controller.rb
+++ b/actionpack/lib/action_controller.rb
@@ -34,7 +34,6 @@ module ActionController
autoload :UrlFor
end
- autoload :Dispatcher, 'action_controller/deprecated/dispatcher'
autoload :Integration, 'action_controller/deprecated/integration_test'
autoload :IntegrationTest, 'action_controller/deprecated/integration_test'
autoload :PerformanceTest, 'action_controller/deprecated/performance_test'
@@ -73,4 +72,5 @@ require 'active_support/core_ext/load_error'
require 'active_support/core_ext/module/attr_internal'
require 'active_support/core_ext/module/delegation'
require 'active_support/core_ext/name_error'
+require 'active_support/core_ext/uri'
require 'active_support/inflector'
diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb
index 9dfffced75..81c0698fb8 100644
--- a/actionpack/lib/action_controller/base.rb
+++ b/actionpack/lib/action_controller/base.rb
@@ -1,6 +1,171 @@
require "action_controller/log_subscriber"
module ActionController
+ # Action Controllers are the core of a web request in \Rails. They are made up of one or more actions that are executed
+ # on request and then either render a template or redirect to another action. An action is defined as a public method
+ # on the controller, which will automatically be made accessible to the web-server through \Rails Routes.
+ #
+ # By default, only the ApplicationController in a \Rails application inherits from <tt>ActionController::Base</tt>. All other
+ # controllers in turn inherit from ApplicationController. This gives you one class to configure things such as
+ # request forgery protection and filtering of sensitive request parameters.
+ #
+ # A sample controller could look like this:
+ #
+ # class PostsController < ApplicationController
+ # def index
+ # @posts = Post.all
+ # end
+ #
+ # def create
+ # @post = Post.create params[:post]
+ # redirect_to posts_path
+ # end
+ # end
+ #
+ # Actions, by default, render a template in the <tt>app/views</tt> directory corresponding to the name of the controller and action
+ # after executing code in the action. For example, the +index+ action of the PostsController would render the
+ # template <tt>app/views/posts/index.html.erb</tt> by default after populating the <tt>@posts</tt> instance variable.
+ #
+ # Unlike index, the create action will not render a template. After performing its main purpose (creating a
+ # new post), it initiates a redirect instead. This redirect works by returning an external
+ # "302 Moved" HTTP response that takes the user to the index action.
+ #
+ # These two methods represent the two basic action archetypes used in Action Controllers. Get-and-show and do-and-redirect.
+ # Most actions are variations of these themes.
+ #
+ # == Requests
+ #
+ # For every request, the router determines the value of the +controller+ and +action+ keys. These determine which controller
+ # and action are called. The remaining request parameters, the session (if one is available), and the full request with
+ # all the HTTP headers are made available to the action through accessor methods. Then the action is performed.
+ #
+ # The full request object is available via the request accessor and is primarily used to query for HTTP headers:
+ #
+ # def server_ip
+ # location = request.env["SERVER_ADDR"]
+ # render :text => "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.
+ #
+ # It's also possible to construct multi-dimensional parameter hashes by specifying keys using brackets, such as:
+ #
+ # <input type="text" name="post[name]" value="david">
+ # <input type="text" name="post[address]" value="hyacintvej">
+ #
+ # A request stemming from a form holding these inputs will include <tt>{ "post" => { "name" => "david", "address" => "hyacintvej" } }</tt>.
+ # If the address input had been named "post[address][street]", the params would have included
+ # <tt>{ "post" => { "address" => { "street" => "hyacintvej" } } }</tt>. There's no limit to the depth of the nesting.
+ #
+ # == Sessions
+ #
+ # Sessions allows you to store objects in between requests. This is useful for objects that are not yet ready to be persisted,
+ # such as a Signup object constructed in a multi-paged process, or objects that don't change much and are needed all the time, such
+ # as a User object for a system that requires login. The session should not be used, however, as a cache for objects where it's likely
+ # they could be changed unknowingly. It's usually too much work to keep it all synchronized -- something databases already excel at.
+ #
+ # You can place objects in the session by using the <tt>session</tt> method, which accesses a hash:
+ #
+ # session[:person] = Person.authenticate(user_name, password)
+ #
+ # And retrieved again through the same hash:
+ #
+ # Hello #{session[:person]}
+ #
+ # For removing objects from the session, you can either assign a single key to +nil+:
+ #
+ # # removes :person from session
+ # session[:person] = nil
+ #
+ # or you can remove the entire session with +reset_session+.
+ #
+ # Sessions are stored by default in a browser cookie that's cryptographically signed, but unencrypted.
+ # This prevents the user from tampering with the session but also allows him to see its contents.
+ #
+ # Do not put secret information in cookie-based sessions!
+ #
+ # Other options for session storage:
+ #
+ # * ActiveRecord::SessionStore - Sessions are stored in your database, which works better than PStore with multiple app servers and,
+ # unlike CookieStore, hides your session contents from the user. To use ActiveRecord::SessionStore, set
+ #
+ # MyApplication::Application.config.session_store :active_record_store
+ #
+ # in your <tt>config/initializers/session_store.rb</tt> and run <tt>script/rails g session_migration</tt>.
+ #
+ # == Responses
+ #
+ # Each action results in a response, which holds the headers and document to be sent to the user's browser. The actual response
+ # object is generated automatically through the use of renders and redirects and requires no user intervention.
+ #
+ # == Renders
+ #
+ # Action Controller sends content to the user by using one of five rendering methods. The most versatile and common is the rendering
+ # of a template. Included in the Action Pack is the Action View, which enables rendering of ERb templates. It's automatically configured.
+ # The controller passes objects to the view by assigning instance variables:
+ #
+ # def show
+ # @post = Post.find(params[:id])
+ # end
+ #
+ # Which are then automatically available to the view:
+ #
+ # Title: <%= @post.title %>
+ #
+ # You don't have to rely on the automated rendering. Especially actions that could result in the rendering of different templates will use
+ # the manual rendering methods:
+ #
+ # def search
+ # @results = Search.find(params[:query])
+ # case @results
+ # when 0 then render :action => "no_results"
+ # when 1 then render :action => "show"
+ # when 2..10 then render :action => "show_many"
+ # end
+ # end
+ #
+ # Read more about writing ERb and Builder templates in ActionView::Base.
+ #
+ # == Redirects
+ #
+ # Redirects are used to move from one action to another. For example, after a <tt>create</tt> action, which stores a blog entry to a database,
+ # we might like to show the user the new entry. Because we're following good DRY principles (Don't Repeat Yourself), we're going to reuse (and redirect to)
+ # a <tt>show</tt> action that we'll assume has already been created. The code might look like this:
+ #
+ # def create
+ # @entry = Entry.new(params[:entry])
+ # if @entry.save
+ # # The entry was saved correctly, redirect to show
+ # redirect_to :action => 'show', :id => @entry.id
+ # else
+ # # things didn't go so well, do something else
+ # end
+ # end
+ #
+ # In this case, after saving our new entry to the database, the user is redirected to the <tt>show</tt> method which is then executed.
+ #
+ # Learn more about <tt>redirect_to</tt> and what options you have in ActionController::Redirecting.
+ #
+ # == Calling multiple redirects or renders
+ #
+ # An action may contain only a single render or a single redirect. Attempting to try to do either again will result in a DoubleRenderError:
+ #
+ # def do_something
+ # redirect_to :action => "elsewhere"
+ # render :action => "overthere" # raises DoubleRenderError
+ # end
+ #
+ # If you need to redirect on the condition of something, then be sure to add "and return" to halt execution.
+ #
+ # def do_something
+ # redirect_to(:action => "elsewhere") and return if monkeys.nil?
+ # render :action => "overthere" # won't be called if monkeys is nil
+ # end
+ #
class Base < Metal
abstract!
@@ -58,13 +223,6 @@ module ActionController
# Rails 2.x compatibility
include ActionController::Compatibility
- def self.inherited(klass)
- super
- klass.helper :all
- end
-
ActiveSupport.run_load_hooks(:action_controller, self)
end
end
-
-require "action_controller/deprecated/base"
diff --git a/actionpack/lib/action_controller/caching.rb b/actionpack/lib/action_controller/caching.rb
index 4105f9e14f..14137f2886 100644
--- a/actionpack/lib/action_controller/caching.rb
+++ b/actionpack/lib/action_controller/caching.rb
@@ -3,7 +3,7 @@ require 'uri'
require 'set'
module ActionController #:nodoc:
- # Caching is a cheap way of speeding up slow applications by keeping the result of
+ # \Caching is a cheap way of speeding up slow applications by keeping the result of
# calculations, renderings, and database calls around for subsequent requests.
# Action Controller affords you three approaches in varying levels of granularity:
# Page, Action, Fragment.
@@ -14,7 +14,7 @@ module ActionController #:nodoc:
# Note: To turn off all caching and sweeping, set
# config.action_controller.perform_caching = false.
#
- # == Caching stores
+ # == \Caching stores
#
# All the caching stores from ActiveSupport::Cache are available to be used as backends
# for Action Controller caching. This setting only affects action and fragment caching
diff --git a/actionpack/lib/action_controller/caching/actions.rb b/actionpack/lib/action_controller/caching/actions.rb
index 546f043c58..2c8a6e4d4d 100644
--- a/actionpack/lib/action_controller/caching/actions.rb
+++ b/actionpack/lib/action_controller/caching/actions.rb
@@ -4,53 +4,58 @@ module ActionController #:nodoc:
module Caching
# Action caching is similar to page caching by the fact that the entire
# output of the response is cached, but unlike page caching, every
- # request still goes through the Action Pack. The key benefit
- # of this is that filters are run before the cache is served, which
- # allows for authentication and other restrictions on whether someone
- # is allowed to see the cache. Example:
+ # request still goes through Action Pack. The key benefit of this is
+ # that filters run before the cache is served, which allows for
+ # authentication and other restrictions on whether someone is allowed
+ # to execute such action. Example:
#
# class ListsController < ApplicationController
# before_filter :authenticate, :except => :public
+ #
# caches_page :public
- # caches_action :index, :show, :feed
+ # caches_action :index, :show
# end
#
- # In this example, the public action doesn't require authentication,
- # so it's possible to use the faster page caching method. But both
- # the show and feed action are to be shielded behind the authenticate
- # filter, so we need to implement those as action caches.
- #
- # Action caching internally uses the fragment caching and an around
- # filter to do the job. The fragment cache is named according to both
- # the current host and the path. So a page that is accessed at
- # http://david.somewhere.com/lists/show/1 will result in a fragment named
- # "david.somewhere.com/lists/show/1". This allows the cacher to
- # differentiate between "david.somewhere.com/lists/" and
- # "jamis.somewhere.com/lists/" -- which is a helpful way of assisting
+ # In this example, the +public+ action doesn't require authentication
+ # so it's possible to use the faster page caching. On the other hand
+ # +index+ and +show+ require authentication. They can still be cached,
+ # but we need action caching for them.
+ #
+ # Action caching uses fragment caching internally and an around
+ # filter to do the job. The fragment cache is named according to
+ # the host and path of the request. A page that is accessed at
+ # <tt>http://david.example.com/lists/show/1</tt> will result in a fragment named
+ # <tt>david.example.com/lists/show/1</tt>. This allows the cacher to
+ # differentiate between <tt>david.example.com/lists/</tt> and
+ # <tt>jamis.example.com/lists/</tt> -- which is a helpful way of assisting
# the subdomain-as-account-key pattern.
#
# Different representations of the same resource, e.g.
- # <tt>http://david.somewhere.com/lists</tt> and
- # <tt>http://david.somewhere.com/lists.xml</tt>
+ # <tt>http://david.example.com/lists</tt> and
+ # <tt>http://david.example.com/lists.xml</tt>
# are treated like separate requests and so are cached separately.
# Keep in mind when expiring an action cache that
# <tt>:action => 'lists'</tt> is not the same as
# <tt>:action => 'list', :format => :xml</tt>.
#
# You can set modify the default action cache path by passing a
- # :cache_path option. This will be passed directly to
- # ActionCachePath.path_for. This is handy for actions with multiple
- # possible routes that should be cached differently. If a block is
- # given, it is called with the current controller instance.
+ # <tt>:cache_path</tt> option. This will be passed directly to
+ # <tt>ActionCachePath.path_for</tt>. This is handy for actions with
+ # multiple possible routes that should be cached differently. If a
+ # block is given, it is called with the current controller instance.
+ #
+ # And you can also use <tt>:if</tt> (or <tt>:unless</tt>) to pass a
+ # proc that specifies when the action should be cached.
#
- # And you can also use :if (or :unless) to pass a Proc that
- # specifies when the action should be cached.
+ # Finally, if you are using memcached, you can also pass <tt>:expires_in</tt>.
#
- # Finally, if you are using memcached, you can also pass :expires_in.
+ # The following example depicts some of the points made above:
#
# class ListsController < ApplicationController
# before_filter :authenticate, :except => :public
- # caches_page :public
+ #
+ # caches_page :public
+ #
# caches_action :index, :if => proc do |c|
# !c.request.format.json? # cache if is not a JSON request
# end
@@ -58,19 +63,28 @@ module ActionController #:nodoc:
# caches_action :show, :cache_path => { :project => 1 },
# :expires_in => 1.hour
#
- # caches_action :feed, :cache_path => proc do |controller|
- # if controller.params[:user_id]
- # controller.send(:user_list_url,
- # controller.params[:user_id], controller.params[:id])
+ # caches_action :feed, :cache_path => proc do |c|
+ # if c.params[:user_id]
+ # c.send(:user_list_url,
+ # c.params[:user_id], c.params[:id])
# else
- # controller.send(:list_url, controller.params[:id])
+ # c.send(:list_url, c.params[:id])
# end
# end
# end
#
- # If you pass :layout => false, it will only cache your action
- # content. It is useful when your layout has dynamic information.
+ # If you pass <tt>:layout => false</tt>, it will only cache your action
+ # content. That's useful when your layout has dynamic information.
+ #
+ # Warning: If the format of the request is determined by the Accept HTTP
+ # header the Content-Type of the cached response could be wrong because
+ # no information about the MIME type is stored in the cache key. So, if
+ # you first ask for MIME type M in the Accept header, a cache entry is
+ # created, and then perform a second request to the same resource asking
+ # for a different MIME type, you'd get the content cached for M.
#
+ # The <tt>:format</tt> parameter is taken into account though. The safest
+ # way to cache by MIME type is to pass the format in the route.
module Actions
extend ActiveSupport::Concern
@@ -89,12 +103,14 @@ module ActionController #:nodoc:
end
def _save_fragment(name, options)
- return unless caching_allowed?
-
content = response_body
content = content.join if content.is_a?(Array)
- write_fragment(name, content, options)
+ if caching_allowed?
+ write_fragment(name, content, options)
+ else
+ content
+ end
end
protected
@@ -144,7 +160,7 @@ module ActionController #:nodoc:
attr_reader :path, :extension
# If +infer_extension+ is true, the cache path extension is looked up from the request's
- # path & format. This is desirable when reading and writing the cache, but not when
+ # path and format. This is desirable when reading and writing the cache, but not when
# expiring the cache - expire_action should expire the same files regardless of the
# request format.
def initialize(controller, options = {}, infer_extension = true)
@@ -161,7 +177,7 @@ module ActionController #:nodoc:
def normalize!(path)
path << 'index' if path[-1] == ?/
path << ".#{extension}" if extension and !path.ends_with?(extension)
- URI.unescape(path)
+ URI.parser.unescape(path)
end
end
end
diff --git a/actionpack/lib/action_controller/caching/fragments.rb b/actionpack/lib/action_controller/caching/fragments.rb
index 460273dac1..0be04b70a1 100644
--- a/actionpack/lib/action_controller/caching/fragments.rb
+++ b/actionpack/lib/action_controller/caching/fragments.rb
@@ -1,52 +1,72 @@
module ActionController #:nodoc:
module Caching
- # Fragment caching is used for caching various blocks within templates without caching the entire action as a whole. This is useful when
- # certain elements of an action change frequently or depend on complicated state while other parts rarely change or can be shared amongst multiple
- # parties. The caching is done using the cache helper available in the Action View. A template with caching might look something like:
+ # Fragment caching is used for caching various blocks within
+ # views without caching the entire action as a whole. This is
+ # useful when certain elements of an action change frequently or
+ # depend on complicated state while other parts rarely change or
+ # can be shared amongst multiple parties. The caching is done using
+ # the <tt>cache</tt> helper available in the Action View. A
+ # template with fragment caching might look like:
#
# <b>Hello <%= @name %></b>
+ #
# <% cache do %>
# All the topics in the system:
# <%= render :partial => "topic", :collection => Topic.find(:all) %>
# <% end %>
#
- # This cache will bind to the name of the action that called it, so if this code was part of the view for the topics/list action, you would
- # be able to invalidate it using <tt>expire_fragment(:controller => "topics", :action => "list")</tt>.
+ # This cache will bind the name of the action that called it, so if
+ # this code was part of the view for the topics/list action, you
+ # would be able to invalidate it using:
+ #
+ # expire_fragment(:controller => "topics", :action => "list")
#
- # This default behavior is of limited use if you need to cache multiple fragments per action or if the action itself is cached using
- # <tt>caches_action</tt>, so we also have the option to qualify the name of the cached fragment with something like:
+ # This default behavior is limited if you need to cache multiple
+ # fragments per action or if the action itself is cached using
+ # <tt>caches_action</tt>. To remedy this, there is an option to
+ # qualify the name of the cached fragment by using the
+ # <tt>:action_suffix</tt> option:
#
# <% cache(:action => "list", :action_suffix => "all_topics") do %>
#
- # That would result in a name such as "/topics/list/all_topics", avoiding conflicts with the action cache and with any fragments that use a
- # different suffix. Note that the URL doesn't have to really exist or be callable - the url_for system is just used to generate unique
- # cache names that we can refer to when we need to expire the cache.
+ # That would result in a name such as
+ # <tt>/topics/list/all_topics</tt>, avoiding conflicts with the
+ # action cache and with any fragments that use a different suffix.
+ # Note that the URL doesn't have to really exist or be callable
+ # - the url_for system is just used to generate unique cache names
+ # that we can refer to when we need to expire the cache.
#
# The expiration call for this example is:
#
- # expire_fragment(:controller => "topics", :action => "list", :action_suffix => "all_topics")
+ # expire_fragment(:controller => "topics",
+ # :action => "list",
+ # :action_suffix => "all_topics")
module Fragments
- # Given a key (as described in <tt>expire_fragment</tt>), returns a key suitable for use in reading,
- # writing, or expiring a cached fragment. If the key is a hash, the generated key is the return
- # value of url_for on that hash (without the protocol). All keys are prefixed with "views/" and uses
+ # Given a key (as described in <tt>expire_fragment</tt>), returns
+ # a key suitable for use in reading, writing, or expiring a
+ # cached fragment. If the key is a hash, the generated key is the
+ # return value of url_for on that hash (without the protocol).
+ # All keys are prefixed with <tt>views/</tt> and uses
# ActiveSupport::Cache.expand_cache_key for the expansion.
def fragment_cache_key(key)
ActiveSupport::Cache.expand_cache_key(key.is_a?(Hash) ? url_for(key).split("://").last : key, :views)
end
- # Writes <tt>content</tt> to the location signified by <tt>key</tt> (see <tt>expire_fragment</tt> for acceptable formats)
+ # Writes <tt>content</tt> to the location signified by
+ # <tt>key</tt> (see <tt>expire_fragment</tt> for acceptable formats).
def write_fragment(key, content, options = nil)
return content unless cache_configured?
key = fragment_cache_key(key)
instrument_fragment_cache :write_fragment, key do
- content = content.html_safe.to_str if content.respond_to?(:html_safe)
+ content = content.to_str
cache_store.write(key, content, options)
end
content
end
- # Reads a cached fragment from the location signified by <tt>key</tt> (see <tt>expire_fragment</tt> for acceptable formats)
+ # Reads a cached fragment from the location signified by <tt>key</tt>
+ # (see <tt>expire_fragment</tt> for acceptable formats).
def read_fragment(key, options = nil)
return unless cache_configured?
@@ -57,7 +77,8 @@ module ActionController #:nodoc:
end
end
- # Check if a cached fragment from the location signified by <tt>key</tt> exists (see <tt>expire_fragment</tt> for acceptable formats)
+ # Check if a cached fragment from the location signified by
+ # <tt>key</tt> exists (see <tt>expire_fragment</tt> for acceptable formats)
def fragment_exist?(key, options = nil)
return unless cache_configured?
key = fragment_cache_key(key)
@@ -70,8 +91,9 @@ module ActionController #:nodoc:
# Removes fragments from the cache.
#
# +key+ can take one of three forms:
+ #
# * String - This would normally take the form of a path, like
- # <tt>"pages/45/notes"</tt>.
+ # <tt>pages/45/notes</tt>.
# * Hash - Treated as an implicit call to +url_for+, like
# <tt>{:controller => "pages", :action => "notes", :id => 45}</tt>
# * Regexp - Will remove any fragment that matches, so
diff --git a/actionpack/lib/action_controller/caching/pages.rb b/actionpack/lib/action_controller/caching/pages.rb
index 4f7a5d3f55..8c583c7ce0 100644
--- a/actionpack/lib/action_controller/caching/pages.rb
+++ b/actionpack/lib/action_controller/caching/pages.rb
@@ -1,5 +1,4 @@
require 'fileutils'
-require 'uri'
require 'active_support/core_ext/class/attribute_accessors'
module ActionController #:nodoc:
@@ -71,9 +70,9 @@ module ActionController #:nodoc:
# Manually cache the +content+ in the key determined by +path+. Example:
# cache_page "I'm the cached content", "/lists/show"
- def cache_page(content, path)
+ def cache_page(content, path, extension = nil)
return unless perform_caching
- path = page_cache_path(path)
+ path = page_cache_path(path, extension)
instrument_page_cache :write_page, path do
FileUtils.makedirs(File.dirname(path))
@@ -98,14 +97,16 @@ module ActionController #:nodoc:
end
private
- def page_cache_file(path)
- name = (path.empty? || path == "/") ? "/index" : URI.unescape(path.chomp('/'))
- name << page_cache_extension unless (name.split('/').last || name).include? '.'
+ def page_cache_file(path, extension)
+ name = (path.empty? || path == "/") ? "/index" : URI.parser.unescape(path.chomp('/'))
+ unless (name.split('/').last || name).include? '.'
+ name << (extension || self.page_cache_extension)
+ end
return name
end
- def page_cache_path(path)
- page_cache_directory + page_cache_file(path)
+ def page_cache_path(path, extension = nil)
+ page_cache_directory.to_s + page_cache_file(path, extension)
end
def instrument_page_cache(name, path)
@@ -135,7 +136,7 @@ module ActionController #:nodoc:
# If no options are provided, the requested url is used. Example:
# cache_page "I'm the cached content", :controller => "lists", :action => "show"
def cache_page(content = nil, options = nil)
- return unless self.class.perform_caching && caching_allowed
+ return unless self.class.perform_caching && caching_allowed?
path = case options
when Hash
@@ -146,13 +147,13 @@ module ActionController #:nodoc:
request.path
end
- self.class.cache_page(content || response.body, path)
+ if (type = Mime::LOOKUP[self.content_type]) && (type_symbol = type.symbol).present?
+ extension = ".#{type_symbol}"
+ end
+
+ self.class.cache_page(content || response.body, path, extension)
end
- private
- def caching_allowed
- request.get? && response.status.to_i == 200
- end
end
end
end
diff --git a/actionpack/lib/action_controller/deprecated.rb b/actionpack/lib/action_controller/deprecated.rb
index 9f2de57033..aa0cfc9395 100644
--- a/actionpack/lib/action_controller/deprecated.rb
+++ b/actionpack/lib/action_controller/deprecated.rb
@@ -1,3 +1,3 @@
ActionController::AbstractRequest = ActionController::Request = ActionDispatch::Request
ActionController::AbstractResponse = ActionController::Response = ActionDispatch::Response
-ActionController::Routing = ActionDispatch::Routing
+ActionController::Routing = ActionDispatch::Routing \ No newline at end of file
diff --git a/actionpack/lib/action_controller/deprecated/base.rb b/actionpack/lib/action_controller/deprecated/base.rb
deleted file mode 100644
index 3975afcaf0..0000000000
--- a/actionpack/lib/action_controller/deprecated/base.rb
+++ /dev/null
@@ -1,133 +0,0 @@
-module ActionController
- class Base
- # Deprecated methods. Wrap them in a module so they can be overwritten by plugins
- # (like the verify method.)
- module DeprecatedBehavior #:nodoc:
- def relative_url_root
- ActiveSupport::Deprecation.warn "ActionController::Base.relative_url_root is ineffective. " <<
- "Please stop using it.", caller
- end
-
- def relative_url_root=
- ActiveSupport::Deprecation.warn "ActionController::Base.relative_url_root= is ineffective. " <<
- "Please stop using it.", caller
- end
-
- def consider_all_requests_local
- ActiveSupport::Deprecation.warn "ActionController::Base.consider_all_requests_local is deprecated, " <<
- "use Rails.application.config.consider_all_requests_local instead", caller
- Rails.application.config.consider_all_requests_local
- end
-
- def consider_all_requests_local=(value)
- ActiveSupport::Deprecation.warn "ActionController::Base.consider_all_requests_local= is deprecated. " <<
- "Please configure it on your application with config.consider_all_requests_local=", caller
- Rails.application.config.consider_all_requests_local = value
- end
-
- def allow_concurrency
- ActiveSupport::Deprecation.warn "ActionController::Base.allow_concurrency is deprecated, " <<
- "use Rails.application.config.allow_concurrency instead", caller
- Rails.application.config.allow_concurrency
- end
-
- def allow_concurrency=(value)
- ActiveSupport::Deprecation.warn "ActionController::Base.allow_concurrency= is deprecated. " <<
- "Please configure it on your application with config.allow_concurrency=", caller
- Rails.application.config.allow_concurrency = value
- end
-
- def ip_spoofing_check=(value)
- ActiveSupport::Deprecation.warn "ActionController::Base.ip_spoofing_check= is deprecated. " <<
- "Please configure it on your application with config.action_dispatch.ip_spoofing_check=", caller
- Rails.application.config.action_dispatch.ip_spoofing_check = value
- end
-
- def ip_spoofing_check
- ActiveSupport::Deprecation.warn "ActionController::Base.ip_spoofing_check is deprecated. " <<
- "Configuring ip_spoofing_check on the application configures a middleware.", caller
- Rails.application.config.action_dispatch.ip_spoofing_check
- end
-
- def cookie_verifier_secret=(value)
- ActiveSupport::Deprecation.warn "ActionController::Base.cookie_verifier_secret= is deprecated. " <<
- "Please configure it on your application with config.secret_token=", caller
- end
-
- def cookie_verifier_secret
- ActiveSupport::Deprecation.warn "ActionController::Base.cookie_verifier_secret is deprecated.", caller
- end
-
- def trusted_proxies=(value)
- ActiveSupport::Deprecation.warn "ActionController::Base.trusted_proxies= is deprecated. " <<
- "Please configure it on your application with config.action_dispatch.trusted_proxies=", caller
- Rails.application.config.action_dispatch.ip_spoofing_check = value
- end
-
- def trusted_proxies
- ActiveSupport::Deprecation.warn "ActionController::Base.trusted_proxies is deprecated. " <<
- "Configuring trusted_proxies on the application configures a middleware.", caller
- Rails.application.config.action_dispatch.ip_spoofing_check = value
- end
-
- def session(*args)
- ActiveSupport::Deprecation.warn(
- "Disabling sessions for a single controller has been deprecated. " +
- "Sessions are now lazy loaded. So if you don't access them, " +
- "consider them off. You can still modify the session cookie " +
- "options with request.session_options.", caller)
- end
-
- def session=(value)
- ActiveSupport::Deprecation.warn "ActionController::Base.session= is deprecated. " <<
- "Please configure it on your application with config.session_store :cookie_store, :key => '....'", caller
- if value.delete(:disabled)
- Rails.application.config.session_store :disabled
- else
- store = Rails.application.config.session_store
- Rails.application.config.session_store store, value
- end
- end
-
- # Controls the resource action separator
- def resource_action_separator
- @resource_action_separator ||= "/"
- end
-
- def resource_action_separator=(val)
- ActiveSupport::Deprecation.warn "ActionController::Base.resource_action_separator is deprecated and only " \
- "works with the deprecated router DSL."
- @resource_action_separator = val
- end
-
- def use_accept_header
- ActiveSupport::Deprecation.warn "ActionController::Base.use_accept_header doesn't do anything anymore. " \
- "The accept header is always taken into account."
- end
-
- def use_accept_header=(val)
- use_accept_header
- end
-
- # This method has been moved to ActionDispatch::Request.filter_parameters
- def filter_parameter_logging(*args, &block)
- ActiveSupport::Deprecation.warn("Setting filter_parameter_logging in ActionController is deprecated and has no longer effect, please set 'config.filter_parameters' in config/application.rb instead", caller)
- filter = Rails.application.config.filter_parameters
- filter.concat(args)
- filter << block if block
- filter
- end
-
- # This was moved to a plugin
- def verify(*args)
- ActiveSupport::Deprecation.warn "verify was removed from Rails and is now available as a plugin. " <<
- "Please install it with `rails plugin install git://github.com/rails/verification.git`.", caller
- end
- end
-
- extend DeprecatedBehavior
-
- delegate :consider_all_requests_local, :consider_all_requests_local=,
- :allow_concurrency, :allow_concurrency=, :to => :"self.class"
- end
-end
diff --git a/actionpack/lib/action_controller/deprecated/dispatcher.rb b/actionpack/lib/action_controller/deprecated/dispatcher.rb
deleted file mode 100644
index 8c21e375dd..0000000000
--- a/actionpack/lib/action_controller/deprecated/dispatcher.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-module ActionController
- class Dispatcher
- class << self
- def before_dispatch(*args, &block)
- ActiveSupport::Deprecation.warn "ActionController::Dispatcher.before_dispatch is deprecated. " <<
- "Please use ActionDispatch::Callbacks.before instead.", caller
- ActionDispatch::Callbacks.before(*args, &block)
- end
-
- def after_dispatch(*args, &block)
- ActiveSupport::Deprecation.warn "ActionController::Dispatcher.after_dispatch is deprecated. " <<
- "Please use ActionDispatch::Callbacks.after instead.", caller
- ActionDispatch::Callbacks.after(*args, &block)
- end
-
- def to_prepare(*args, &block)
- ActiveSupport::Deprecation.warn "ActionController::Dispatcher.to_prepare is deprecated. " <<
- "Please use config.to_prepare instead", caller
- ActionDispatch::Callbacks.after(*args, &block)
- end
-
- def new
- ActiveSupport::Deprecation.warn "ActionController::Dispatcher.new is deprecated, use Rails.application instead."
- Rails.application
- end
- end
- end
-end
diff --git a/actionpack/lib/action_controller/log_subscriber.rb b/actionpack/lib/action_controller/log_subscriber.rb
index ece270b3ce..3fae697cc3 100644
--- a/actionpack/lib/action_controller/log_subscriber.rb
+++ b/actionpack/lib/action_controller/log_subscriber.rb
@@ -16,7 +16,11 @@ module ActionController
payload = event.payload
additions = ActionController::Base.log_process_action(payload)
- message = "Completed #{payload[:status]} #{Rack::Utils::HTTP_STATUS_CODES[payload[:status]]} in %.0fms" % event.duration
+ status = payload[:status]
+ if status.nil? && payload[:exception].present?
+ status = Rack::Utils.status_code(ActionDispatch::ShowExceptions.rescue_responses[payload[:exception].first]) rescue nil
+ end
+ message = "Completed #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]} in %.0fms" % event.duration
message << " (#{additions.join(" | ")})" unless additions.blank?
info(message)
@@ -42,7 +46,7 @@ module ActionController
def #{method}(event)
key_or_path = event.payload[:key] || event.payload[:path]
human_name = #{method.to_s.humanize.inspect}
- info("\#{human_name} \#{key_or_path} (%.1fms)" % event.duration)
+ info("\#{human_name} \#{key_or_path} \#{"(%.1fms)" % event.duration}")
end
METHOD
end
diff --git a/actionpack/lib/action_controller/metal.rb b/actionpack/lib/action_controller/metal.rb
index 2281c500c5..e5db31061b 100644
--- a/actionpack/lib/action_controller/metal.rb
+++ b/actionpack/lib/action_controller/metal.rb
@@ -12,7 +12,7 @@ module ActionController
#
class MiddlewareStack < ActionDispatch::MiddlewareStack #:nodoc:
class Middleware < ActionDispatch::MiddlewareStack::Middleware #:nodoc:
- def initialize(klass, *args)
+ 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)
@@ -36,35 +36,88 @@ module ActionController
action = action.to_s
raise "MiddlewareStack#build requires an app" unless app
- reverse.inject(app) do |a, middleware|
+ middlewares.reverse.inject(app) do |a, middleware|
middleware.valid?(action) ?
middleware.build(a) : a
end
end
end
- # ActionController::Metal provides a way to get a valid Rack application from a controller.
+ # <tt>ActionController::Metal</tt> is the simplest possible controller, providing a
+ # valid Rack interface without the additional niceties provided by
+ # <tt>ActionController::Base</tt>.
+ #
+ # A sample metal controller might look like this:
+ #
+ # class HelloController < ActionController::Metal
+ # def index
+ # self.response_body = "Hello World!"
+ # end
+ # end
+ #
+ # And then to route requests to your metal controller, you would add
+ # something like this to <tt>config/routes.rb</tt>:
+ #
+ # match 'hello', :to => HelloController.action(:index)
+ #
+ # The +action+ method returns a valid Rack application for the \Rails
+ # router to dispatch to.
+ #
+ # == Rendering Helpers
+ #
+ # <tt>ActionController::Metal</tt> by default provides no utilities for rendering
+ # views, partials, or other responses aside from explicitly calling of
+ # <tt>response_body=</tt>, <tt>content_type=</tt>, and <tt>status=</tt>. To
+ # add the render helpers you're used to having in a normal controller, you
+ # can do the following:
+ #
+ # class HelloController < ActionController::Metal
+ # include ActionController::Rendering
+ # append_view_path "#{Rails.root}/app/views"
+ #
+ # def index
+ # render "hello/index"
+ # end
+ # end
+ #
+ # == Redirection Helpers
+ #
+ # To add redirection helpers to your metal controller, do the following:
+ #
+ # class HelloController < ActionController::Metal
+ # include ActionController::Redirecting
+ # include Rails.application.routes.url_helpers
+ #
+ # def index
+ # redirect_to root_url
+ # end
+ # end
+ #
+ # == Other Helpers
+ #
+ # You can refer to the modules included in <tt>ActionController::Base</tt> to see
+ # other features you can bring into your metal controller.
#
- # In AbstractController, dispatching is triggered directly by calling #process on a new controller.
- # ActionController::Metal provides an #action method that returns a valid Rack application for a
- # given action. Other rack builders, such as Rack::Builder, Rack::URLMap, and the Rails router,
- # can dispatch directly to the action returned by FooController.action(:index).
class Metal < AbstractController::Base
abstract!
- attr_internal :env
+ attr_internal_writer :env
+
+ def env
+ @_env ||= {}
+ end
# Returns the last part of the controller's name, underscored, without the ending
- # "Controller". For instance, MyApp::MyPostsController would return "my_posts" for
- # controller_name
+ # <tt>Controller</tt>. For instance, PostsController returns <tt>posts</tt>.
+ # Namespaces are left out, so Admin::PostsController returns <tt>posts</tt> as well.
#
# ==== Returns
- # String
+ # * <tt>string</tt>
def self.controller_name
@controller_name ||= self.name.demodulize.sub(/Controller$/, '').underscore
end
- # Delegates to the class' #controller_name
+ # Delegates to the class' <tt>controller_name</tt>
def controller_name
self.class.controller_name
end
@@ -81,6 +134,9 @@ module ActionController
def initialize(*)
@_headers = {"Content-Type" => "text/html"}
@_status = 200
+ @_request = nil
+ @_response = nil
+ @_routes = nil
super
end
@@ -112,6 +168,11 @@ module ActionController
headers["Location"] = url
end
+ # basic url_for that can be overridden for more robust functionality
+ def url_for(string)
+ string
+ end
+
def status
@_status
end
@@ -121,12 +182,11 @@ module ActionController
end
def response_body=(val)
- body = val.respond_to?(:each) ? val : [val]
+ body = val.nil? ? nil : (val.respond_to?(:each) ? val : [val])
super body
end
- # :api: private
- def dispatch(name, request)
+ def dispatch(name, request) #:nodoc:
@_request = request
@_env = request.env
@_env['action_controller.instance'] = self
@@ -134,8 +194,7 @@ module ActionController
to_a
end
- # :api: private
- def to_a
+ def to_a #:nodoc:
response ? response.to_a : [status, headers, response_body]
end
@@ -147,8 +206,8 @@ module ActionController
super
end
- def self.use(*args)
- middleware_stack.use(*args)
+ def self.use(*args, &block)
+ middleware_stack.use(*args, &block)
end
def self.middleware
@@ -164,10 +223,10 @@ module ActionController
# for the same action.
#
# ==== Parameters
- # action<#to_s>:: An action name
+ # * <tt>action</tt> - An action name
#
# ==== Returns
- # Proc:: A rack application
+ # * <tt>proc</tt> - A rack application
def self.action(name, klass = ActionDispatch::Request)
middleware_stack.build(name.to_s) do |env|
new.dispatch(name, klass.new(env))
diff --git a/actionpack/lib/action_controller/metal/compatibility.rb b/actionpack/lib/action_controller/metal/compatibility.rb
index d49465fa0b..006b9fd456 100644
--- a/actionpack/lib/action_controller/metal/compatibility.rb
+++ b/actionpack/lib/action_controller/metal/compatibility.rb
@@ -5,9 +5,6 @@ module ActionController
class ::ActionController::ActionControllerError < StandardError #:nodoc:
end
- module ClassMethods
- end
-
# Temporary hax
included do
::ActionController::UnknownAction = ::AbstractController::ActionNotFound
@@ -39,12 +36,6 @@ module ActionController
def assign_shortcuts(*) end
def _normalize_options(options)
- if options[:action] && options[:action].to_s.include?(?/)
- ActiveSupport::Deprecation.warn "Giving a path to render :action is deprecated. " <<
- "Please use render :template instead", caller
- options[:template] = options.delete(:action)
- end
-
options[:text] = nil if options.delete(:nothing) == true
options[:text] = " " if options.key?(:text) && options[:text].nil?
super
diff --git a/actionpack/lib/action_controller/metal/conditional_get.rb b/actionpack/lib/action_controller/metal/conditional_get.rb
index 61e7ece90d..a5e37172c9 100644
--- a/actionpack/lib/action_controller/metal/conditional_get.rb
+++ b/actionpack/lib/action_controller/metal/conditional_get.rb
@@ -6,7 +6,7 @@ module ActionController
include Head
# Sets the etag, last_modified, or both on the response and renders a
- # "304 Not Modified" response if the request is already fresh.
+ # <tt>304 Not Modified</tt> response if the request is already fresh.
#
# Parameters:
# * <tt>:etag</tt>
@@ -17,11 +17,11 @@ module ActionController
#
# def show
# @article = Article.find(params[:id])
- # fresh_when(:etag => @article, :last_modified => @article.created_at.utc, :public => true)
+ # fresh_when(:etag => @article, :last_modified => @article.created_at, :public => true)
# end
#
# This will render the show template if the request isn't sending a matching etag or
- # If-Modified-Since header and just a "304 Not Modified" response if there's a match.
+ # If-Modified-Since header and just a <tt>304 Not Modified</tt> response if there's a match.
#
def fresh_when(options)
options.assert_valid_keys(:etag, :last_modified, :public)
@@ -36,7 +36,7 @@ module ActionController
# Sets the etag and/or last_modified on the response and checks it against
# the client request. If the request doesn't match the options provided, the
# request is considered stale and should be generated from scratch. Otherwise,
- # it's fresh and we don't need to generate anything and a reply of "304 Not Modified" is sent.
+ # it's fresh and we don't need to generate anything and a reply of <tt>304 Not Modified</tt> is sent.
#
# Parameters:
# * <tt>:etag</tt>
@@ -48,7 +48,7 @@ module ActionController
# def show
# @article = Article.find(params[:id])
#
- # if stale?(:etag => @article, :last_modified => @article.created_at.utc)
+ # if stale?(:etag => @article, :last_modified => @article.created_at)
# @statistics = @article.really_expensive_call
# respond_to do |format|
# # all the supported formats
@@ -60,13 +60,13 @@ module ActionController
!request.fresh?(response)
end
- # Sets a HTTP 1.1 Cache-Control header. Defaults to issuing a "private" instruction, so that
- # intermediate caches shouldn't cache the response.
+ # Sets a HTTP 1.1 Cache-Control header. Defaults to issuing a <tt>private</tt> instruction, so that
+ # intermediate caches must not cache the response.
#
# Examples:
# expires_in 20.minutes
# expires_in 3.hours, :public => true
- # expires in 3.hours, 'max-stale' => 5.hours, :public => true
+ # expires_in 3.hours, 'max-stale' => 5.hours, :public => true
#
# This method will overwrite an existing Cache-Control header.
# See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html for more possibilities.
@@ -77,7 +77,7 @@ module ActionController
response.cache_control[:extras] = options.map {|k,v| "#{k}=#{v}"}
end
- # Sets a HTTP 1.1 Cache-Control header of "no-cache" so no caching should occur by the browser or
+ # Sets a HTTP 1.1 Cache-Control header of <tt>no-cache</tt> so no caching should occur by the browser or
# intermediate caches (like caching proxy servers).
def expires_now #:doc:
response.cache_control.replace(:no_cache => true)
diff --git a/actionpack/lib/action_controller/metal/head.rb b/actionpack/lib/action_controller/metal/head.rb
index a5c9910d68..8abcad55a2 100644
--- a/actionpack/lib/action_controller/metal/head.rb
+++ b/actionpack/lib/action_controller/metal/head.rb
@@ -2,8 +2,6 @@ module ActionController
module Head
extend ActiveSupport::Concern
- include ActionController::UrlFor
-
# Return a response that has no content (merely headers). The options
# argument is interpreted to be a hash of header names and values.
# This allows you to easily return a response that consists only of
@@ -22,13 +20,13 @@ module ActionController
location = options.delete(:location)
options.each do |key, value|
- headers[key.to_s.dasherize.split(/-/).map { |v| v.capitalize }.join("-")] = value.to_s
+ headers[key.to_s.dasherize.split('-').each { |v| v[0] = v[0].chr.upcase }.join('-')] = value.to_s
end
self.status = status
self.location = url_for(location) if location
- self.content_type = Mime[formats.first]
+ self.content_type = Mime[formats.first] if formats
self.response_body = " "
end
end
-end \ No newline at end of file
+end
diff --git a/actionpack/lib/action_controller/metal/helpers.rb b/actionpack/lib/action_controller/metal/helpers.rb
index e0bc47318a..91a88ab68a 100644
--- a/actionpack/lib/action_controller/metal/helpers.rb
+++ b/actionpack/lib/action_controller/metal/helpers.rb
@@ -2,21 +2,21 @@ require 'active_support/core_ext/array/wrap'
require 'active_support/core_ext/class/attribute'
module ActionController
- # The Rails framework provides a large number of helpers for working with +assets+, +dates+, +forms+,
- # +numbers+ and model objects, to name a few. These helpers are available to all templates
+ # The \Rails framework provides a large number of helpers for working with assets, dates, forms,
+ # numbers and model objects, to name a few. These helpers are available to all templates
# by default.
#
- # In addition to using the standard template helpers provided in the Rails framework, creating custom helpers to
+ # In addition to using the standard template helpers provided, creating custom helpers to
# extract complicated logic or reusable functionality is strongly encouraged. By default, the controller will
# include a helper whose name matches that of the controller, e.g., <tt>MyController</tt> will automatically
# include <tt>MyHelper</tt>.
#
- # Additional helpers can be specified using the +helper+ class method in <tt>ActionController::Base</tt> or any
+ # Additional helpers can be specified using the +helper+ class method in ActionController::Base or any
# controller which inherits from it.
#
# ==== Examples
- # The +to_s+ method from the Time class can be wrapped in a helper method to display a custom message if
- # the Time object is blank:
+ # The +to_s+ method from the \Time class can be wrapped in a helper method to display a custom message if
+ # a \Time object is blank:
#
# module FormattedTimeHelper
# def format_time(time, format=:long, blank_message="&nbsp;")
@@ -53,30 +53,20 @@ module ActionController
include AbstractController::Helpers
included do
- config_accessor :helpers_path
+ config_accessor :helpers_path, :include_all_helpers
self.helpers_path ||= []
+ self.include_all_helpers = true
end
module ClassMethods
- def helpers_dir
- ActiveSupport::Deprecation.warn "helpers_dir is deprecated, use helpers_path instead", caller
- self.helpers_path
- end
-
- def helpers_dir=(value)
- ActiveSupport::Deprecation.warn "helpers_dir= is deprecated, use helpers_path= instead", caller
- self.helpers_path = Array.wrap(value)
- end
-
# Declares helper accessors for controller attributes. For example, the
# following adds new +name+ and <tt>name=</tt> instance methods to a
# controller and makes them available to the view:
- # helper_attr :name
# attr_accessor :name
+ # helper_attr :name
#
# ==== Parameters
- # *attrs<Array[String, Symbol]>:: Names of attributes to be converted
- # into helpers.
+ # * <tt>attrs</tt> - Names of attributes to be converted into helpers.
def helper_attr(*attrs)
attrs.flatten.each { |attr| helper_method(attr, "#{attr}=") }
end
@@ -88,25 +78,28 @@ module ActionController
private
# Overwrite modules_for_helpers to accept :all as argument, which loads
- # all helpers in helpers_dir.
+ # all helpers in helpers_path.
#
# ==== Parameters
- # args<Array[String, Symbol, Module, all]>:: A list of helpers
+ # * <tt>args</tt> - A list of helpers
#
# ==== Returns
- # Array[Module]:: A normalized list of modules for the list of
- # helpers provided.
+ # * <tt>array</tt> - A normalized list of modules for the list of helpers provided.
def modules_for_helpers(args)
args += all_application_helpers if args.delete(:all)
super(args)
end
- # Extract helper names from files in app/helpers/**/*_helper.rb
+ # Extract helper names from files in <tt>app/helpers/**/*_helper.rb</tt>
def all_application_helpers
+ all_helpers_from_path(helpers_path)
+ end
+
+ def all_helpers_from_path(path)
helpers = []
- Array.wrap(helpers_path).each do |path|
- extract = /^#{Regexp.quote(path.to_s)}\/?(.*)_helper.rb$/
- helpers += Dir["#{path}/**/*_helper.rb"].map { |file| file.sub(extract, '\1') }
+ Array.wrap(path).each do |_path|
+ extract = /^#{Regexp.quote(_path.to_s)}\/?(.*)_helper.rb$/
+ helpers += Dir["#{_path}/**/*_helper.rb"].map { |file| file.sub(extract, '\1') }
end
helpers.sort!
helpers.uniq!
diff --git a/actionpack/lib/action_controller/metal/hide_actions.rb b/actionpack/lib/action_controller/metal/hide_actions.rb
index 32d7a96701..b55c4643be 100644
--- a/actionpack/lib/action_controller/metal/hide_actions.rb
+++ b/actionpack/lib/action_controller/metal/hide_actions.rb
@@ -1,8 +1,7 @@
require 'active_support/core_ext/class/attribute'
module ActionController
- # ActionController::HideActions adds the ability to prevent public methods on a controller
- # to be called as actions.
+ # Adds the ability to prevent public methods on a controller to be called as actions.
module HideActions
extend ActiveSupport::Concern
@@ -23,7 +22,7 @@ module ActionController
# Sets all of the actions passed in as hidden actions.
#
# ==== Parameters
- # *args<#to_s>:: A list of actions
+ # * <tt>args</tt> - A list of actions
def hide_action(*args)
self.hidden_actions = hidden_actions.dup.merge(args.map(&:to_s)).freeze
end
diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb
index b0eb24a4a8..39c804d707 100644
--- a/actionpack/lib/action_controller/metal/http_authentication.rb
+++ b/actionpack/lib/action_controller/metal/http_authentication.rb
@@ -3,9 +3,9 @@ require 'active_support/core_ext/object/blank'
module ActionController
module HttpAuthentication
- # Makes it dead easy to do HTTP Basic authentication.
+ # Makes it dead easy to do HTTP \Basic and \Digest authentication.
#
- # Simple Basic example:
+ # === Simple \Basic example
#
# class PostsController < ApplicationController
# USER_NAME, PASSWORD = "dhh", "secret"
@@ -29,7 +29,9 @@ module ActionController
# end
#
#
- # Here is a more advanced Basic example where only Atom feeds and the XML API is protected by HTTP authentication,
+ # === Advanced \Basic example
+ #
+ # Here is a more advanced \Basic example where only Atom feeds and the XML API is protected by HTTP authentication,
# the regular HTML interface is protected by a session approach:
#
# class ApplicationController < ActionController::Base
@@ -69,7 +71,7 @@ module ActionController
# assert_equal 200, status
# end
#
- # Simple Digest example:
+ # === Simple \Digest example
#
# require 'digest/md5'
# class PostsController < ApplicationController
@@ -95,18 +97,20 @@ module ActionController
# end
# end
#
- # NOTE: The +authenticate_or_request_with_http_digest+ block must return the user's password or the ha1 digest hash so the framework can appropriately
- # hash to check the user's credentials. Returning +nil+ will cause authentication to fail.
- # Storing the ha1 hash: MD5(username:realm:password), is better than storing a plain password. If
- # the password file or database is compromised, the attacker would be able to use the ha1 hash to
- # authenticate as the user at this +realm+, but would not have the user's password to try using at
- # other sites.
+ # === Notes
#
- # On shared hosts, Apache sometimes doesn't pass authentication headers to
- # FCGI instances. If your environment matches this description and you cannot
- # authenticate, try this rule in your Apache setup:
+ # The +authenticate_or_request_with_http_digest+ block must return the user's password
+ # or the ha1 digest hash so the framework can appropriately hash to check the user's
+ # credentials. Returning +nil+ will cause authentication to fail.
#
- # RewriteRule ^(.*)$ dispatch.fcgi [E=X-HTTP_AUTHORIZATION:%{HTTP:Authorization},QSA,L]
+ # Storing the ha1 hash: MD5(username:realm:password), is better than storing a plain password. If
+ # the password file or database is compromised, the attacker would be able to use the ha1 hash to
+ # authenticate as the user at this +realm+, but would not have the user's password to try using at
+ # other sites.
+ #
+ # In rare instances, web servers or front proxies strip authorization headers before
+ # they reach your application. You can debug this situation by logging all environment
+ # variables, and check for HTTP_AUTHORIZATION, amongst others.
module Basic
extend self
@@ -210,7 +214,7 @@ module ActionController
def encode_credentials(http_method, credentials, password, password_is_ha1)
credentials[:response] = expected_response(http_method, credentials[:uri], credentials, password, password_is_ha1)
- "Digest " + credentials.sort_by {|x| x[0].to_s }.inject([]) {|a, v| a << "#{v[0]}='#{v[1]}'" }.join(', ')
+ "Digest " + credentials.sort_by {|x| x[0].to_s }.map {|v| "#{v[0]}='#{v[1]}'" }.join(', ')
end
def decode_credentials_header(request)
@@ -218,11 +222,10 @@ module ActionController
end
def decode_credentials(header)
- header.to_s.gsub(/^Digest\s+/,'').split(',').inject({}) do |hash, pair|
+ Hash[header.to_s.gsub(/^Digest\s+/,'').split(',').map do |pair|
key, value = pair.split('=', 2)
- hash[key.strip.to_sym] = value.to_s.gsub(/^"|"$/,'').gsub(/'/, '')
- hash
- end
+ [key.strip.to_sym, value.to_s.gsub(/^"|"$/,'').gsub(/'/, '')]
+ end]
end
def authentication_header(controller, realm)
@@ -392,11 +395,11 @@ module ActionController
end
end
- # If token Authorization header is present, call the login procedure with
+ # If token Authorization header is present, call the login procedure with
# the present token and options.
#
# controller - ActionController::Base instance for the current request.
- # login_procedure - Proc to call if a token is present. The Proc should
+ # login_procedure - Proc to call if a token is present. The Proc should
# take 2 arguments:
# authenticate(controller) { |token, options| ... }
#
@@ -404,7 +407,7 @@ module ActionController
# Returns nil if no token is found.
def authenticate(controller, &login_procedure)
token, options = token_and_options(controller.request)
- if !token.blank?
+ unless token.blank?
login_procedure.call(token, options)
end
end
@@ -414,20 +417,19 @@ module ActionController
# Authorization: Token token="abc", nonce="def"
# Then the returned token is "abc", and the options is {:nonce => "def"}
#
- # request - ActionController::Request instance with the current headers.
+ # 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.
def token_and_options(request)
if header = request.authorization.to_s[/^Token (.*)/]
- values = $1.split(',').
- inject({}) do |memo, value|
- value.strip! # remove any spaces between commas and values
- key, value = value.split(/\=\"?/) # split key=value pairs
- value.chomp!('"') # chomp trailing " in value
- value.gsub!(/\\\"/, '"') # unescape remaining quotes
- memo.update(key => value)
- end
+ values = Hash[$1.split(',').map do |value|
+ value.strip! # remove any spaces between commas and values
+ key, value = value.split(/\=\"?/) # split key=value pairs
+ value.chomp!('"') # chomp trailing " in value
+ value.gsub!(/\\\"/, '"') # unescape remaining quotes
+ [key, value]
+ end]
[values.delete("token"), values.with_indifferent_access]
end
end
@@ -439,9 +441,8 @@ module ActionController
#
# Returns String.
def encode_credentials(token, options = {})
- values = ["token=#{token.to_s.inspect}"]
- options.each do |key, value|
- values << "#{key}=#{value.to_s.inspect}"
+ values = ["token=#{token.to_s.inspect}"] + options.map do |key, value|
+ "#{key}=#{value.to_s.inspect}"
end
"Token #{values * ", "}"
end
diff --git a/actionpack/lib/action_controller/metal/implicit_render.rb b/actionpack/lib/action_controller/metal/implicit_render.rb
index 282dcf66b3..cfa7004048 100644
--- a/actionpack/lib/action_controller/metal/implicit_render.rb
+++ b/actionpack/lib/action_controller/metal/implicit_render.rb
@@ -12,10 +12,10 @@ module ActionController
def method_for_action(action_name)
super || begin
- if template_exists?(action_name.to_s, _prefix)
+ if template_exists?(action_name.to_s, _prefixes)
"default_render"
end
end
end
end
-end \ No newline at end of file
+end
diff --git a/actionpack/lib/action_controller/metal/instrumentation.rb b/actionpack/lib/action_controller/metal/instrumentation.rb
index b08d9a8434..dc3ea939e6 100644
--- a/actionpack/lib/action_controller/metal/instrumentation.rb
+++ b/actionpack/lib/action_controller/metal/instrumentation.rb
@@ -78,7 +78,7 @@ module ActionController
yield
end
- # Everytime after an action is processed, this method is invoked
+ # Every time after an action is processed, this method is invoked
# with the payload, so you can add more information.
# :api: plugin
def append_info_to_payload(payload) #:nodoc:
diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb
index c6d4c6d936..a2e06fe0a6 100644
--- a/actionpack/lib/action_controller/metal/mime_responds.rb
+++ b/actionpack/lib/action_controller/metal/mime_responds.rb
@@ -63,13 +63,13 @@ module ActionController #:nodoc:
# might look something like this:
#
# def index
- # @people = Person.find(:all)
+ # @people = Person.all
# end
#
# Here's the same action, with web-service support baked in:
#
# def index
- # @people = Person.find(:all)
+ # @people = Person.all
#
# respond_to do |format|
# format.html
@@ -155,7 +155,7 @@ module ActionController #:nodoc:
# Respond to also allows you to specify a common block for different formats by using any:
#
# def index
- # @people = Person.find(:all)
+ # @people = Person.all
#
# respond_to do |format|
# format.html
@@ -178,7 +178,7 @@ module ActionController #:nodoc:
# respond_to :html, :xml, :json
#
# def index
- # @people = Person.find(:all)
+ # @people = Person.all
# respond_with(@person)
# end
# end
@@ -208,8 +208,8 @@ module ActionController #:nodoc:
# It also accepts a block to be given. It's used to overwrite a default
# response:
#
- # def destroy
- # @user = User.find(params[:id])
+ # def create
+ # @user = User.new(params[:user])
# flash[:notice] = "User was successfully created." if @user.save
#
# respond_with(@user) do |format|
@@ -227,7 +227,7 @@ module ActionController #:nodoc:
"controller responds to in the class level" if self.class.mimes_for_respond_to.empty?
if response = retrieve_response_from_mimes(&block)
- options = resources.extract_options!
+ options = resources.size == 1 ? {} : resources.extract_options!
options.merge!(:default_response => response)
(options.delete(:responder) || self.class.responder).call(self, resources, options)
end
@@ -258,9 +258,8 @@ module ActionController #:nodoc:
# nil if :not_acceptable was sent to the client.
#
def retrieve_response_from_mimes(mimes=nil, &block)
- collector = Collector.new { default_render }
mimes ||= collect_mimes_from_class_level
- mimes.each { |mime| collector.send(mime) }
+ collector = Collector.new(mimes) { default_render }
block.call(collector) if block_given?
if format = request.negotiate_mime(collector.order)
@@ -277,8 +276,9 @@ module ActionController #:nodoc:
include AbstractController::Collector
attr_accessor :order
- def initialize(&block)
+ def initialize(mimes, &block)
@order, @responses, @default_response = [], {}, block
+ mimes.each { |mime| send(mime) }
end
def any(*args, &block)
@@ -291,7 +291,7 @@ module ActionController #:nodoc:
alias :all :any
def custom(mime_type, &block)
- mime_type = mime_type.is_a?(Mime::Type) ? mime_type : Mime::Type.lookup(mime_type.to_s)
+ mime_type = Mime::Type.lookup(mime_type.to_s) unless mime_type.is_a?(Mime::Type)
@order << mime_type
@responses[mime_type] ||= block
end
diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb
index b5f1d23ef0..55c650df6c 100644
--- a/actionpack/lib/action_controller/metal/redirecting.rb
+++ b/actionpack/lib/action_controller/metal/redirecting.rb
@@ -20,6 +20,7 @@ module ActionController
# * <tt>Record</tt> - The URL will be generated by calling url_for with the +options+, which will reference a named URL for that record.
# * <tt>String</tt> starting with <tt>protocol://</tt> (like <tt>http://</tt>) - Is passed straight through as the target for redirection.
# * <tt>String</tt> not containing a protocol - The current protocol and host is prepended to the string.
+ # * <tt>Proc</tt> - A block that will be executed in the controller's context. Should return any option accepted by +redirect_to+.
# * <tt>:back</tt> - Back to the page that issued the request. Useful for forms that are triggered from multiple places.
# Short-hand for <tt>redirect_to(request.env["HTTP_REFERER"])</tt>
#
@@ -30,6 +31,7 @@ module ActionController
# redirect_to "/images/screenshot.jpg"
# redirect_to articles_url
# redirect_to :back
+ # redirect_to proc { edit_post_url(@post) }
#
# The redirection happens as a "302 Moved" header unless otherwise specified.
#
@@ -39,6 +41,9 @@ module ActionController
# redirect_to post_url(@post), :status => 301
# redirect_to :action=>'atom', :status => 302
#
+ # The status code can either be a standard {HTTP Status code}[http://www.iana.org/assignments/http-status-codes] as an
+ # integer, or a symbol representing the downcased, underscored and symbolized description.
+ #
# It is also possible to assign a flash message as part of the redirection. There are two special accessors for commonly used the flash names
# +alert+ and +notice+ as well as a general purpose +flash+ bucket.
#
@@ -48,8 +53,7 @@ module ActionController
# redirect_to post_url(@post), :status => 301, :flash => { :updated_post_id => @post.id }
# redirect_to { :action=>'atom' }, :alert => "Something serious happened"
#
- # When using <tt>redirect_to :back</tt>, if there is no referrer,
- # RedirectBackError will be raised. You may specify some fallback
+ # When using <tt>redirect_to :back</tt>, if there is no referrer, RedirectBackError will be raised. You may specify some fallback
# behavior for this case by rescuing RedirectBackError.
def redirect_to(options = {}, response_status = {}) #:doc:
raise ActionControllerError.new("Cannot redirect to nil!") if options.nil?
@@ -83,6 +87,8 @@ module ActionController
when :back
raise RedirectBackError unless refer = request.headers["Referer"]
refer
+ when Proc
+ _compute_redirect_to_location options.call
else
url_for(options)
end.gsub(/[\r\n]/, '')
diff --git a/actionpack/lib/action_controller/metal/renderers.rb b/actionpack/lib/action_controller/metal/renderers.rb
index 0be07cd1fc..38711c8462 100644
--- a/actionpack/lib/action_controller/metal/renderers.rb
+++ b/actionpack/lib/action_controller/metal/renderers.rb
@@ -2,6 +2,7 @@ require 'active_support/core_ext/class/attribute'
require 'active_support/core_ext/object/blank'
module ActionController
+ # See <tt>Renderers.add</tt>
def self.add_renderer(key, &block)
Renderers.add(key, &block)
end
@@ -15,30 +16,12 @@ module ActionController
end
module ClassMethods
- def _write_render_options
- renderers = _renderers.map do |name, value|
- <<-RUBY_EVAL
- if options.key?(:#{name})
- _process_options(options)
- return _render_option_#{name}(options.delete(:#{name}), options)
- end
- RUBY_EVAL
- end
-
- class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
- def _handle_render_options(options)
- #{renderers.join}
- end
- RUBY_EVAL
- end
-
def use_renderers(*args)
new = _renderers.dup
args.each do |key|
new[key] = RENDERERS[key]
end
self._renderers = new.freeze
- _write_render_options
end
alias use_renderer use_renderers
end
@@ -47,31 +30,69 @@ module ActionController
_handle_render_options(options) || super
end
+ def _handle_render_options(options)
+ _renderers.each do |name, value|
+ if options.key?(name.to_sym)
+ _process_options(options)
+ return send("_render_option_#{name}", options.delete(name.to_sym), options)
+ end
+ end
+ nil
+ end
+
+ # Hash of available renderers, mapping a renderer name to its proc.
+ # Default keys are :json, :js, :xml and :update.
RENDERERS = {}
+
+ # Adds a new renderer to call within controller actions.
+ # A renderer is invoked by passing its name as an option to
+ # <tt>AbstractController::Rendering#render</tt>. To create a renderer
+ # pass it a name and a block. The block takes two arguments, the first
+ # is the value paired with its key and the second is the remaining
+ # hash of options passed to +render+.
+ #
+ # === Example
+ # Create a csv renderer:
+ #
+ # ActionController::Renderers.add :csv do |obj, options|
+ # filename = options[:filename] || 'data'
+ # str = obj.respond_to?(:to_csv) ? obj.to_csv : obj.to_s
+ # send_data str, :type => Mime::CSV,
+ # :disposition => "attachment; filename=#{filename}.csv"
+ # end
+ #
+ # Note that we used Mime::CSV for the csv mime type as it comes with Rails.
+ # For a custom renderer, you'll need to register a mime type with
+ # <tt>Mime::Type.register</tt>.
+ #
+ # To use the csv renderer in a controller action:
+ #
+ # def show
+ # @csvable = Csvable.find(params[:id])
+ # respond_to do |format|
+ # format.html
+ # format.csv { render :csv => @csvable, :filename => @csvable.name }
+ # }
+ # end
+ # 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>
def self.add(key, &block)
define_method("_render_option_#{key}", &block)
RENDERERS[key] = block
- All._write_render_options
end
module All
extend ActiveSupport::Concern
include Renderers
- INCLUDED = []
included do
self._renderers = RENDERERS
- _write_render_options
- INCLUDED << self
- end
-
- def self._write_render_options
- INCLUDED.each(&:_write_render_options)
end
end
add :json do |json, options|
- json = ActiveSupport::JSON.encode(json, options) unless json.respond_to?(:to_str)
+ json = json.to_json(options) unless json.kind_of?(String)
json = "#{options[:callback]}(#{json})" unless options[:callback].blank?
self.content_type ||= Mime::JSON
self.response_body = json
diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb
index 86bb810947..32d52c84c4 100644
--- a/actionpack/lib/action_controller/metal/rendering.rb
+++ b/actionpack/lib/action_controller/metal/rendering.rb
@@ -2,12 +2,11 @@ module ActionController
module Rendering
extend ActiveSupport::Concern
- include ActionController::RackDelegation
include AbstractController::Rendering
# Before processing, set the request formats in current controller formats.
def process_action(*) #:nodoc:
- self.formats = request.formats.map { |x| x.to_sym }
+ self.formats = request.formats.map { |x| x.ref }
super
end
@@ -21,36 +20,35 @@ module ActionController
private
- # Normalize arguments by catching blocks and setting them on :update.
- def _normalize_args(action=nil, options={}, &blk) #:nodoc:
- options = super
- options[:update] = blk if block_given?
- options
- end
-
- # Normalize both text and status options.
- def _normalize_options(options) #:nodoc:
- if options.key?(:text) && options[:text].respond_to?(:to_text)
- options[:text] = options[:text].to_text
- end
+ # Normalize arguments by catching blocks and setting them on :update.
+ def _normalize_args(action=nil, options={}, &blk) #:nodoc:
+ options = super
+ options[:update] = blk if block_given?
+ options
+ end
- if options[:status]
- options[:status] = Rack::Utils.status_code(options[:status])
- end
+ # Normalize both text and status options.
+ def _normalize_options(options) #:nodoc:
+ if options.key?(:text) && options[:text].respond_to?(:to_text)
+ options[:text] = options[:text].to_text
+ end
- super
+ if options[:status]
+ options[:status] = Rack::Utils.status_code(options[:status])
end
- # Process controller specific options, as status, content-type and location.
- def _process_options(options) #:nodoc:
- status, content_type, location = options.values_at(:status, :content_type, :location)
+ super
+ end
- self.status = status if status
- self.content_type = content_type if content_type
- self.headers["Location"] = url_for(location) if location
+ # Process controller specific options, as status, content-type and location.
+ def _process_options(options) #:nodoc:
+ status, content_type, location = options.values_at(:status, :content_type, :location)
- super
- end
+ self.status = status if status
+ self.content_type = content_type if content_type
+ self.headers["Location"] = url_for(location) if location
+ super
+ end
end
end
diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb
index b632e7aab6..1cd93a188c 100644
--- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb
+++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb
@@ -4,45 +4,27 @@ module ActionController #:nodoc:
class InvalidAuthenticityToken < ActionControllerError #:nodoc:
end
- # Protecting controller actions from CSRF attacks by ensuring that all forms are coming from the current
- # web application, not a forged link from another site, is done by embedding a token based on a random
- # string stored in the session (which an attacker wouldn't know) in all forms and Ajax requests generated
- # by Rails and then verifying the authenticity of that token in the controller. Only HTML/JavaScript
- # requests are checked, so this will not protect your XML API (presumably you'll have a different
- # authentication scheme there anyway). Also, GET requests are not protected as these should be
- # idempotent anyway.
+ # Controller actions are protected from Cross-Site Request Forgery (CSRF) attacks
+ # by including a token in the rendered html for your application. This token is
+ # stored as a random string in the session, to which an attacker does not have
+ # access. When a request reaches your application, \Rails then 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). Also, GET requests are not protected as these
+ # should be idempotent.
#
- # This is turned on with the <tt>protect_from_forgery</tt> method, which will check the token and raise an
- # ActionController::InvalidAuthenticityToken if it doesn't match what was expected. You can customize the
- # error message in production by editing public/422.html. A call to this method in ApplicationController is
- # generated by default in post-Rails 2.0 applications.
+ # CSRF protection is turned on with the <tt>protect_from_forgery</tt> method,
+ # which will check the token and raise an ActionController::InvalidAuthenticityToken
+ # if it doesn't match what was expected. A call to this method is generated for new
+ # \Rails applications by default. You can customize the error message by editing
+ # public/422.html.
#
- # The token parameter is named <tt>authenticity_token</tt> by default. If you are generating an HTML form
- # manually (without the use of Rails' <tt>form_for</tt>, <tt>form_tag</tt> or other helpers), you have to
- # include a hidden field named like that and set its value to what is returned by
- # <tt>form_authenticity_token</tt>.
- #
- # Request forgery protection is disabled by default in test environment. If you are upgrading from Rails
- # 1.x, add this to config/environments/test.rb:
- #
- # # Disable request forgery protection in test environment
- # config.action_controller.allow_forgery_protection = false
- #
- # == Learn more about CSRF (Cross-Site Request Forgery) attacks
- #
- # Here are some resources:
- # * http://isc.sans.org/diary.html?storyid=1750
- # * http://en.wikipedia.org/wiki/Cross-site_request_forgery
- #
- # Keep in mind, this is NOT a silver-bullet, plug 'n' play, warm security blanket for your rails application.
- # There are a few guidelines you should follow:
- #
- # * Keep your GET requests safe and idempotent. More reading material:
- # * http://www.xml.com/pub/a/2002/04/24/deviant.html
- # * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.1.1
- # * Make sure the session cookies that Rails creates are non-persistent. Check in Firefox and look
- # for "Expires: at end of session"
+ # The token parameter is named <tt>authenticity_token</tt> by default. The name and
+ # value of this token must be added to every layout that renders forms by including
+ # <tt>csrf_meta_tags</tt> in the html +head+.
#
+ # Learn more about CSRF attacks and securing your application in the
+ # {Ruby on Rails Security Guide}[http://guides.rubyonrails.org/security.html].
module RequestForgeryProtection
extend ActiveSupport::Concern
@@ -71,39 +53,42 @@ module ActionController #:nodoc:
# class FooController < ApplicationController
# protect_from_forgery :except => :index
#
- # # you can disable csrf protection on controller-by-controller basis:
- # skip_before_filter :verify_authenticity_token
- # end
+ # You can disable csrf protection on controller-by-controller basis:
+ #
+ # skip_before_filter :verify_authenticity_token
+ #
+ # It can also be disabled for specific controller actions:
+ #
+ # skip_before_filter :verify_authenticity_token, :except => [:create]
#
# Valid Options:
#
# * <tt>:only/:except</tt> - Passed to the <tt>before_filter</tt> call. Set which actions are verified.
def protect_from_forgery(options = {})
self.request_forgery_protection_token ||= :authenticity_token
- before_filter :verify_authenticity_token, options
+ prepend_before_filter :verify_authenticity_token, options
end
end
protected
-
- def protect_from_forgery(options = {})
- self.request_forgery_protection_token ||= :authenticity_token
- before_filter :verify_authenticity_token, options
- end
-
# The actual before_filter that is used. Modify this to change how you handle unverified requests.
def verify_authenticity_token
- verified_request? || raise(ActionController::InvalidAuthenticityToken)
+ verified_request? || handle_unverified_request
+ end
+
+ def handle_unverified_request
+ reset_session
end
# Returns true or false if a request is verified. Checks:
#
- # * is the format restricted? By default, only HTML requests are checked.
# * is it a GET 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.forgery_whitelisted? ||
- form_authenticity_token == params[request_forgery_protection_token]
+ !protect_against_forgery? || request.get? ||
+ form_authenticity_token == params[request_forgery_protection_token] ||
+ form_authenticity_token == request.headers['X-CSRF-Token']
end
# Sets the token value for the current session.
diff --git a/actionpack/lib/action_controller/metal/responder.rb b/actionpack/lib/action_controller/metal/responder.rb
index cb644dfd16..4b45413cf8 100644
--- a/actionpack/lib/action_controller/metal/responder.rb
+++ b/actionpack/lib/action_controller/metal/responder.rb
@@ -1,7 +1,7 @@
require 'active_support/json'
module ActionController #:nodoc:
- # Responder is responsible for exposing a resource to different mime requests,
+ # 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:
#
@@ -24,10 +24,10 @@ module ActionController #:nodoc:
#
# === Builtin HTTP verb semantics
#
- # The default Rails responder holds semantics for each HTTP verb. Depending on the
+ # 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
+ # Using \Rails default responder, a POST request for creating an object could
# be written as:
#
# def create
@@ -77,8 +77,6 @@ module ActionController #:nodoc:
#
# respond_with(@project, :manager, @task)
#
- # Check <code>polymorphic_url</code> documentation for more examples.
- #
class Responder
attr_reader :controller, :request, :format, :resource, :resources, :options
@@ -89,6 +87,8 @@ module ActionController #:nodoc:
def initialize(controller, resources, options={})
@controller = controller
+ @request = @controller.request
+ @format = @controller.formats.first
@resource = resources.last
@resources = resources
@options = options
@@ -99,14 +99,6 @@ module ActionController #:nodoc:
delegate :head, :render, :redirect_to, :to => :controller
delegate :get?, :post?, :put?, :delete?, :to => :request
- def request
- @request ||= @controller.request
- end
-
- def format
- @format ||= @controller.formats.first
- end
-
# 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)
@@ -121,7 +113,7 @@ module ActionController #:nodoc:
# Main entry point for responder responsible to dispatch to the proper format.
#
def respond
- method = :"to_#{format}"
+ method = "to_#{format}"
respond_to?(method) ? send(method) : to_format
end
@@ -146,7 +138,7 @@ module ActionController #:nodoc:
protected
- # This is the common behavior for "navigation" requests, like :html, :iphone and so forth.
+ # This is the common behavior for formats associated with browsing, like :html, :iphone and so forth.
def navigation_behavior(error)
if get?
raise error
@@ -157,7 +149,7 @@ module ActionController #:nodoc:
end
end
- # This is the common behavior for "API" requests, like :xml and :json.
+ # This is the common behavior for formats associated with APIs, such as :xml and :json.
def api_behavior(error)
raise error unless resourceful?
@@ -167,6 +159,8 @@ module ActionController #:nodoc:
display resource.errors, :status => :unprocessable_entity
elsif post?
display resource, :status => :created, :location => api_location
+ elsif has_empty_resource_definition?
+ display empty_resource, :status => :ok
else
head :ok
end
@@ -175,7 +169,7 @@ module ActionController #:nodoc:
# Checks whether the resource responds to the current format or not.
#
def resourceful?
- resource.respond_to?(:"to_#{format}")
+ resource.respond_to?("to_#{format}")
end
# Returns the resource location by retrieving it from the options or
@@ -227,5 +221,23 @@ module ActionController #:nodoc:
def default_action
@action ||= ACTIONS_FOR_VERBS[request.request_method_symbol]
end
+
+ # Check whether resource needs a specific definition of empty resource to be valid
+ #
+ def has_empty_resource_definition?
+ respond_to?("empty_#{format}_resource")
+ end
+
+ # Delegate to proper empty resource method
+ #
+ def empty_resource
+ send("empty_#{format}_resource")
+ end
+
+ # Return a valid empty JSON resource
+ #
+ def empty_json_resource
+ "{}"
+ end
end
end
diff --git a/actionpack/lib/action_controller/metal/streaming.rb b/actionpack/lib/action_controller/metal/streaming.rb
index d75b46dace..312dc8eb3e 100644
--- a/actionpack/lib/action_controller/metal/streaming.rb
+++ b/actionpack/lib/action_controller/metal/streaming.rb
@@ -69,10 +69,6 @@ module ActionController #:nodoc:
options[:filename] ||= File.basename(path) unless options[:url_based_filename]
send_file_headers! options
- if options[:x_sendfile]
- ActiveSupport::Deprecation.warn(":x_sendfile is no longer needed in send_file", caller)
- end
-
self.status = options[:status] || 200
self.content_type = options[:content_type] if options.key?(:content_type)
self.response_body = File.open(path, "rb")
@@ -105,10 +101,6 @@ module ActionController #:nodoc:
# send_data image.data, :type => image.content_type, :disposition => 'inline'
#
# See +send_file+ for more information on HTTP Content-* headers and caching.
- #
- # <b>Tip:</b> if you want to stream large amounts of on-the-fly generated
- # data to the browser, then use <tt>render :text => proc { ... }</tt>
- # instead. See ActionController::Base#render for more information.
def send_data(data, options = {}) #:doc:
send_file_headers! options.dup
render options.slice(:status, :content_type).merge(:text => data)
@@ -121,10 +113,6 @@ module ActionController #:nodoc:
raise ArgumentError, ":#{arg} option required" if options[arg].nil?
end
- if options.key?(:length)
- ActiveSupport::Deprecation.warn("You do not need to provide the file's length", caller)
- end
-
disposition = options[:disposition]
disposition += %(; filename="#{options[:filename]}") if options[:filename]
diff --git a/actionpack/lib/action_controller/metal/testing.rb b/actionpack/lib/action_controller/metal/testing.rb
index 4b8c452d50..f4efeb33ba 100644
--- a/actionpack/lib/action_controller/metal/testing.rb
+++ b/actionpack/lib/action_controller/metal/testing.rb
@@ -14,18 +14,9 @@ module ActionController
cookies.write(@_response)
end
@_response.prepare!
- set_test_assigns
ret
end
- def set_test_assigns
- @assigns = {}
- (instance_variable_names - self.class.protected_instance_variables).each do |var|
- name, value = var[1..-1], instance_variable_get(var)
- @assigns[name] = value
- end
- end
-
# TODO : Rewrite tests using controller.headers= to use Rack env
def headers=(new_headers)
@_response ||= ActionDispatch::Response.new
diff --git a/actionpack/lib/action_controller/metal/url_for.rb b/actionpack/lib/action_controller/metal/url_for.rb
index a51fc5b8e4..6fc0cf1fb8 100644
--- a/actionpack/lib/action_controller/metal/url_for.rb
+++ b/actionpack/lib/action_controller/metal/url_for.rb
@@ -2,27 +2,25 @@ module ActionController
module UrlFor
extend ActiveSupport::Concern
- include ActionDispatch::Routing::UrlFor
+ include AbstractController::UrlFor
def url_options
- super.reverse_merge(
- :host => request.host_with_port,
+ @_url_options ||= super.reverse_merge(
+ :host => request.host,
+ :port => request.optional_port,
:protocol => request.protocol,
:_path_segments => request.symbolized_path_parameters
- ).merge(:script_name => request.script_name)
- end
-
- def _routes
- raise "In order to use #url_for, you must include routing helpers explicitly. " \
- "For instance, `include Rails.application.routes.url_helpers"
- end
+ ).freeze
- module ClassMethods
- def action_methods
- @action_methods ||= begin
- super - _routes.named_routes.helper_names
+ if _routes.equal?(env["action_dispatch.routes"])
+ @_url_options.dup.tap do |options|
+ options[:script_name] = request.script_name.dup
+ options.freeze
end
+ else
+ @_url_options
end
end
+
end
end
diff --git a/actionpack/lib/action_controller/middleware.rb b/actionpack/lib/action_controller/middleware.rb
index 2115b07b3e..437fec3dc6 100644
--- a/actionpack/lib/action_controller/middleware.rb
+++ b/actionpack/lib/action_controller/middleware.rb
@@ -31,7 +31,7 @@ module ActionController
super()
@_app = app
end
-
+
def index
call(env)
end
diff --git a/actionpack/lib/action_controller/railtie.rb b/actionpack/lib/action_controller/railtie.rb
index cd2dfafbe6..f0c29825ba 100644
--- a/actionpack/lib/action_controller/railtie.rb
+++ b/actionpack/lib/action_controller/railtie.rb
@@ -2,35 +2,13 @@ require "rails"
require "action_controller"
require "action_dispatch/railtie"
require "action_view/railtie"
-require "active_support/deprecation/proxy_wrappers"
-require "active_support/deprecation"
+require "abstract_controller/railties/routes_helpers"
+require "action_controller/railties/paths"
module ActionController
class Railtie < Rails::Railtie
config.action_controller = ActiveSupport::OrderedOptions.new
- config.action_controller.singleton_class.tap do |d|
- d.send(:define_method, :session) do
- ActiveSupport::Deprecation.warn "config.action_controller.session has been deprecated. " <<
- "Please use Rails.application.config.session_store instead.", caller
- end
-
- d.send(:define_method, :session=) do |val|
- ActiveSupport::Deprecation.warn "config.action_controller.session= has been deprecated. " <<
- "Please use config.session_store(name, options) instead.", caller
- end
-
- d.send(:define_method, :session_store) do
- ActiveSupport::Deprecation.warn "config.action_controller.session_store has been deprecated. " <<
- "Please use Rails.application.config.session_store instead.", caller
- end
-
- d.send(:define_method, :session_store=) do |val|
- ActiveSupport::Deprecation.warn "config.action_controller.session_store= has been deprecated. " <<
- "Please use config.session_store(name, options) instead.", caller
- end
- end
-
initializer "action_controller.logger" do
ActiveSupport.on_load(:action_controller) { self.logger ||= Rails.logger }
end
@@ -43,24 +21,27 @@ module ActionController
paths = app.config.paths
options = app.config.action_controller
- options.assets_dir ||= paths.public.to_a.first
- options.javascripts_dir ||= paths.public.javascripts.to_a.first
- options.stylesheets_dir ||= paths.public.stylesheets.to_a.first
- options.page_cache_directory ||= paths.public.to_a.first
- options.helpers_path ||= paths.app.helpers.to_a
+ options.assets_dir ||= paths["public"].first
+ options.javascripts_dir ||= paths["public/javascripts"].first
+ options.stylesheets_dir ||= paths["public/stylesheets"].first
+ options.page_cache_directory ||= paths["public"].first
+
+ # make sure readers methods get compiled
+ options.asset_path ||= app.config.asset_path
+ options.asset_host ||= app.config.asset_host
ActiveSupport.on_load(:action_controller) do
- include app.routes.url_helpers
+ include app.routes.mounted_helpers
+ extend ::AbstractController::Railties::RoutesHelpers.with(app.routes)
+ extend ::ActionController::Railties::Paths.with(app)
options.each { |k,v| send("#{k}=", v) }
end
end
- initializer "action_controller.deprecated_routes" do |app|
- message = "ActionController::Routing::Routes is deprecated. " \
- "Instead, use Rails.application.routes"
-
- proxy = ActiveSupport::Deprecation::DeprecatedObjectProxy.new(app.routes, message)
- ActionController::Routing::Routes = proxy
+ initializer "action_controller.compile_config_methods" do
+ ActiveSupport.on_load(:action_controller) do
+ config.compile_methods! if config.respond_to?(:compile_methods!)
+ end
end
end
-end \ No newline at end of file
+end
diff --git a/actionpack/lib/action_controller/railties/paths.rb b/actionpack/lib/action_controller/railties/paths.rb
new file mode 100644
index 0000000000..dce3c2fe88
--- /dev/null
+++ b/actionpack/lib/action_controller/railties/paths.rb
@@ -0,0 +1,32 @@
+module ActionController
+ module Railties
+ module Paths
+ def self.with(app)
+ Module.new do
+ define_method(:inherited) do |klass|
+ super(klass)
+
+ if namespace = klass.parents.detect {|m| m.respond_to?(:_railtie) }
+ paths = namespace._railtie.paths["app/helpers"].existent
+ else
+ paths = app.config.helpers_paths
+ end
+
+ klass.helpers_path = paths
+ if klass.superclass == ActionController::Base && ActionController::Base.include_all_helpers
+ klass.helper :all
+ end
+
+ if app.config.serve_static_assets && namespace
+ paths = namespace._railtie.config.paths
+
+ klass.config.assets_dir = paths["public"].first
+ klass.config.javascripts_dir = paths["public/javascripts"].first
+ klass.config.stylesheets_dir = paths["public/stylesheets"].first
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb
index e306697f4b..bc4f8bb9ce 100644
--- a/actionpack/lib/action_controller/test_case.rb
+++ b/actionpack/lib/action_controller/test_case.rb
@@ -1,6 +1,7 @@
require 'rack/session/abstract/id'
require 'active_support/core_ext/object/blank'
require 'active_support/core_ext/object/to_query'
+require 'active_support/core_ext/class/attribute'
module ActionController
module TemplateAssertions
@@ -40,6 +41,13 @@ module ActionController
ActiveSupport::Notifications.unsubscribe("!render_template.action_view")
end
+ def process(*args)
+ @partials = Hash.new(0)
+ @templates = Hash.new(0)
+ @layouts = Hash.new(0)
+ super
+ end
+
# Asserts that the request was rendered with the appropriate template file or partials.
#
# ==== Examples
@@ -127,7 +135,7 @@ module ActionController
class Result < ::Array #:nodoc:
def to_s() join '/' end
def self.new_escaped(strings)
- new strings.collect {|str| URI.unescape str}
+ new strings.collect {|str| uri_parser.unescape str}
end
end
@@ -164,9 +172,14 @@ module ActionController
end
def recycle!
+ write_cookies!
+ @env.delete('HTTP_COOKIE') if @cookies.blank?
+ @env.delete('action_dispatch.cookies')
+ @cookies = nil
@formats = nil
@env.delete_if { |k, v| k =~ /^(action_dispatch|rack)\.request/ }
@env.delete_if { |k, v| k =~ /^action_dispatch\.rescue/ }
+ @symbolized_path_params = nil
@method = @request_method = nil
@fullpath = @ip = @remote_ip = nil
@env['action_dispatch.request.query_parameters'] = {}
@@ -186,20 +199,23 @@ module ActionController
end
end
- class TestSession < ActionDispatch::Session::AbstractStore::SessionHash #:nodoc:
- DEFAULT_OPTIONS = ActionDispatch::Session::AbstractStore::DEFAULT_OPTIONS
+ class TestSession < Rack::Session::Abstract::SessionHash #:nodoc:
+ DEFAULT_OPTIONS = Rack::Session::Abstract::ID::DEFAULT_OPTIONS
def initialize(session = {})
+ @env, @by = nil, nil
replace(session.stringify_keys)
@loaded = true
end
- def exists?; true; end
+ def exists?
+ true
+ end
end
# Superclass for ActionController functional tests. Functional tests allow you to
# test a single controller action per test method. This should not be confused with
- # integration tests (see ActionController::IntegrationTest), which are more like
+ # integration tests (see ActionDispatch::IntegrationTest), which are more like
# "stories" that can involve multiple controllers and multiple actions (i.e. multiple
# different HTTP requests).
#
@@ -244,7 +260,7 @@ module ActionController
# after calling +post+. If the various assert methods are not sufficient, then you
# may use this object to inspect the HTTP response in detail.
#
- # (Earlier versions of Rails required each functional test to subclass
+ # (Earlier versions of \Rails required each functional test to subclass
# Test::Unit::TestCase and define @controller, @request, @response in +setup+.)
#
# == Controller is automatically inferred
@@ -257,7 +273,7 @@ module ActionController
# tests WidgetController
# end
#
- # == Testing controller internals
+ # == \Testing controller internals
#
# In addition to these specific assertions, you also have easy access to various collections that the regular test/unit assertions
# can be used against. These collections are:
@@ -265,7 +281,7 @@ module ActionController
# * 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.
+ # * cookies: \Cookies being sent to the user on this request.
#
# These collections can be used just like any other hash:
#
@@ -289,9 +305,13 @@ module ActionController
# and cookies, though. For sessions, you just do:
#
# @request.session[:key] = "value"
- # @request.cookies["key"] = "value"
+ # @request.cookies[:key] = "value"
+ #
+ # To clear the cookies for a test just clear the request's cookies hash:
+ #
+ # @request.cookies.clear
#
- # == Testing named routes
+ # == \Testing named routes
#
# If you're using named routes, they can be easily tested using the original named routes' methods straight in the test case.
# Example:
@@ -311,14 +331,14 @@ module ActionController
def tests(controller_class)
self.controller_class = controller_class
end
-
+
def controller_class=(new_class)
prepare_controller_class(new_class) if new_class
- write_inheritable_attribute(:controller_class, new_class)
+ self._controller_class = new_class
end
def controller_class
- if current_controller_class = read_inheritable_attribute(:controller_class)
+ if current_controller_class = self._controller_class
current_controller_class
else
self.controller_class = determine_default_controller_class(name)
@@ -393,16 +413,18 @@ module ActionController
parameters ||= {}
@request.assign_parameters(@routes, @controller.class.name.underscore.sub(/_controller$/, ''), action.to_s, parameters)
- @request.session = ActionController::TestSession.new(session) unless session.nil?
+ @request.session = ActionController::TestSession.new(session) if session
@request.session["flash"] = @request.flash.update(flash || {})
@request.session["flash"].sweep
@controller.request = @request
@controller.params.merge!(parameters)
build_request_uri(action, parameters)
- Base.class_eval { include Testing }
+ @controller.class.class_eval { include Testing }
@controller.process_with_new_base_test(@request, @response)
+ @assigns = @controller.respond_to?(:view_assigns) ? @controller.view_assigns : {}
@request.session.delete('flash') if @request.session['flash'].blank?
+ @request.cookies.merge!(@response.cookies)
@response
end
@@ -416,7 +438,7 @@ module ActionController
@request.env.delete('PATH_INFO')
- if @controller
+ if defined?(@controller) && @controller
@controller.request = @request
@controller.params = {}
end
@@ -430,6 +452,7 @@ module ActionController
included do
include ActionController::TemplateAssertions
include ActionDispatch::Assertions
+ class_attribute :_controller_class
setup :setup_controller_request_and_response
end
@@ -437,7 +460,7 @@ module ActionController
def build_request_uri(action, parameters)
unless @request.env["PATH_INFO"]
- options = @controller.__send__(:url_options).merge(parameters)
+ options = @controller.respond_to?(:url_options) ? @controller.__send__(:url_options).merge(parameters) : parameters
options.update(
:only_path => true,
:action => action,
@@ -461,9 +484,11 @@ module ActionController
# The exception is stored in the exception accessor for further inspection.
module RaiseActionExceptions
def self.included(base)
- base.class_eval do
- attr_accessor :exception
- protected :exception, :exception=
+ unless base.method_defined?(:exception) && base.method_defined?(:exception=)
+ base.class_eval do
+ attr_accessor :exception
+ protected :exception, :exception=
+ end
end
end
diff --git a/actionpack/lib/action_controller/vendor/html-scanner/html/document.rb b/actionpack/lib/action_controller/vendor/html-scanner/html/document.rb
index b8d73c350d..7fa3aead82 100644
--- a/actionpack/lib/action_controller/vendor/html-scanner/html/document.rb
+++ b/actionpack/lib/action_controller/vendor/html-scanner/html/document.rb
@@ -48,7 +48,7 @@ EOF
end
end
end
-
+
# Search the tree for (and return) the first node that matches the given
# conditions. The conditions are interpreted differently for different node
# types, see HTML::Text#find and HTML::Tag#find.
@@ -62,7 +62,7 @@ EOF
def find_all(conditions)
@root.find_all(conditions)
end
-
+
end
end
diff --git a/actionpack/lib/action_controller/vendor/html-scanner/html/node.rb b/actionpack/lib/action_controller/vendor/html-scanner/html/node.rb
index a874519978..22b3243104 100644
--- a/actionpack/lib/action_controller/vendor/html-scanner/html/node.rb
+++ b/actionpack/lib/action_controller/vendor/html-scanner/html/node.rb
@@ -1,7 +1,7 @@
require 'strscan'
module HTML #:nodoc:
-
+
class Conditions < Hash #:nodoc:
def initialize(hash)
super()
@@ -18,14 +18,14 @@ module HTML #:nodoc:
hash[k] = Conditions.new(v)
when :children
hash[k] = v = keys_to_symbols(v)
- v.each do |k,v2|
- case k
+ v.each do |key,value|
+ case key
when :count, :greater_than, :less_than
# keys are valid, and require no further processing
when :only
- v[k] = Conditions.new(v2)
+ v[key] = Conditions.new(value)
else
- raise "illegal key #{k.inspect} => #{v2.inspect}"
+ raise "illegal key #{key.inspect} => #{value.inspect}"
end
end
else
@@ -38,18 +38,14 @@ module HTML #:nodoc:
private
def keys_to_strings(hash)
- hash.keys.inject({}) do |h,k|
- h[k.to_s] = hash[k]
- h
- end
+ Hash[hash.keys.map {|k| [k.to_s, hash[k]]}]
end
def keys_to_symbols(hash)
- hash.keys.inject({}) do |h,k|
+ Hash[hash.keys.map do |k|
raise "illegal key #{k.inspect}" unless k.respond_to?(:to_sym)
- h[k.to_sym] = hash[k]
- h
- end
+ [k.to_sym, hash[k]]
+ end]
end
end
@@ -57,17 +53,17 @@ module HTML #:nodoc:
class Node #:nodoc:
# The array of children of this node. Not all nodes have children.
attr_reader :children
-
+
# The parent node of this node. All nodes have a parent, except for the
# root node.
attr_reader :parent
-
+
# The line number of the input where this node was begun
attr_reader :line
-
+
# The byte position in the input where this node was begun
attr_reader :position
-
+
# Create a new node as a child of the given parent.
def initialize(parent, line=0, pos=0)
@parent = parent
@@ -77,9 +73,7 @@ module HTML #:nodoc:
# Return a textual representation of the node.
def to_s
- s = ""
- @children.each { |child| s << child.to_s }
- s
+ @children.join()
end
# Return false (subclasses must override this to provide specific matching
@@ -92,7 +86,7 @@ module HTML #:nodoc:
# returns non +nil+. Returns the result of the #find call that succeeded.
def find(conditions)
conditions = validate_conditions(conditions)
- @children.each do |child|
+ @children.each do |child|
node = child.find(conditions)
return node if node
end
@@ -133,7 +127,7 @@ module HTML #:nodoc:
equivalent
end
-
+
class <<self
def parse(parent, line, pos, content, strict=true)
if content !~ /^<\S/
@@ -160,11 +154,11 @@ module HTML #:nodoc:
return CDATA.new(parent, line, pos, scanner.pre_match.gsub(/<!\[CDATA\[/, ''))
end
-
+
closing = ( scanner.scan(/\//) ? :close : nil )
return Text.new(parent, line, pos, content) unless name = scanner.scan(/[\w:-]+/)
name.downcase!
-
+
unless closing
scanner.skip(/\s*/)
attributes = {}
@@ -191,13 +185,13 @@ module HTML #:nodoc:
attributes[attr.downcase] = value
scanner.skip(/\s*/)
end
-
+
closing = ( scanner.scan(/\//) ? :self : nil )
end
-
+
unless scanner.scan(/\s*>/)
if strict
- raise "expected > (got #{scanner.rest.inspect} for #{content}, #{attributes.inspect})"
+ raise "expected > (got #{scanner.rest.inspect} for #{content}, #{attributes.inspect})"
else
# throw away all text until we find what we're looking for
scanner.skip_until(/>/) or scanner.terminate
@@ -212,9 +206,9 @@ module HTML #:nodoc:
# A node that represents text, rather than markup.
class Text < Node #:nodoc:
-
+
attr_reader :content
-
+
# Creates a new text node as a child of the given parent, with the given
# content.
def initialize(parent, line, pos, content)
@@ -240,7 +234,7 @@ module HTML #:nodoc:
def find(conditions)
match(conditions) && self
end
-
+
# Returns non-+nil+ if this node meets the given conditions, or +nil+
# otherwise. See the discussion of #find for the valid conditions.
def match(conditions)
@@ -268,7 +262,7 @@ module HTML #:nodoc:
content == node.content
end
end
-
+
# A CDATA node is simply a text node with a specialized way of displaying
# itself.
class CDATA < Text #:nodoc:
@@ -281,16 +275,16 @@ module HTML #:nodoc:
# closing tag, or a self-closing tag. It has a name, and may have a hash of
# attributes.
class Tag < Node #:nodoc:
-
+
# Either +nil+, <tt>:close</tt>, or <tt>:self</tt>
attr_reader :closing
-
+
# Either +nil+, or a hash of attributes for this node.
attr_reader :attributes
# The name of this tag.
attr_reader :name
-
+
# Create a new node as a child of the given parent, using the given content
# to describe the node. It will be parsed and the node name, attributes and
# closing status extracted.
@@ -344,7 +338,7 @@ module HTML #:nodoc:
def tag?
true
end
-
+
# Returns +true+ if the node meets any of the given conditions. The
# +conditions+ parameter must be a hash of any of the following keys
# (all are optional):
@@ -404,7 +398,7 @@ module HTML #:nodoc:
# node.match :descendant => { :tag => "strong" }
#
# # test if the node has between 2 and 4 span tags as immediate children
- # node.match :children => { :count => 2..4, :only => { :tag => "span" } }
+ # node.match :children => { :count => 2..4, :only => { :tag => "span" } }
#
# # get funky: test to see if the node is a "div", has a "ul" ancestor
# # and an "li" parent (with "class" = "enum"), and whether or not it has
@@ -439,7 +433,7 @@ module HTML #:nodoc:
# test children
return false unless children.find { |child| child.match(conditions[:child]) } if conditions[:child]
-
+
# test ancestors
if conditions[:ancestor]
return false unless catch :found do
@@ -457,13 +451,13 @@ module HTML #:nodoc:
child.match(:descendant => conditions[:descendant])
end
end
-
+
# count children
if opts = conditions[:children]
matches = children.select do |c|
(c.kind_of?(HTML::Tag) and (c.closing == :self or ! c.childless?))
end
-
+
matches = matches.select { |c| c.match(opts[:only]) } if opts[:only]
opts.each do |key, value|
next if key == :only
@@ -489,24 +483,24 @@ module HTML #:nodoc:
self_index = siblings.index(self)
if conditions[:sibling]
- return false unless siblings.detect do |s|
+ return false unless siblings.detect do |s|
s != self && s.match(conditions[:sibling])
end
end
if conditions[:before]
- return false unless siblings[self_index+1..-1].detect do |s|
+ return false unless siblings[self_index+1..-1].detect do |s|
s != self && s.match(conditions[:before])
end
end
if conditions[:after]
- return false unless siblings[0,self_index].detect do |s|
+ return false unless siblings[0,self_index].detect do |s|
s != self && s.match(conditions[:after])
end
end
end
-
+
true
end
@@ -515,7 +509,7 @@ module HTML #:nodoc:
return false unless closing == node.closing && self.name == node.name
attributes == node.attributes
end
-
+
private
# Match the given value to the given condition.
def match_condition(value, condition)
diff --git a/actionpack/lib/action_controller/vendor/html-scanner/html/sanitizer.rb b/actionpack/lib/action_controller/vendor/html-scanner/html/sanitizer.rb
index 51e0868995..09dd08898c 100644
--- a/actionpack/lib/action_controller/vendor/html-scanner/html/sanitizer.rb
+++ b/actionpack/lib/action_controller/vendor/html-scanner/html/sanitizer.rb
@@ -1,5 +1,5 @@
require 'set'
-require 'active_support/core_ext/class/inheritable_attributes'
+require 'active_support/core_ext/class/attribute'
module HTML
class Sanitizer
@@ -7,11 +7,11 @@ module HTML
return text unless sanitizeable?(text)
tokenize(text, options).join
end
-
+
def sanitizeable?(text)
!(text.nil? || text.empty? || !text.index("<"))
end
-
+
protected
def tokenize(text, options)
tokenizer = HTML::Tokenizer.new(text)
@@ -22,12 +22,12 @@ module HTML
end
result
end
-
+
def process_node(node, result, options)
result << node.to_s
end
end
-
+
class FullSanitizer < Sanitizer
def sanitize(text, options = {})
result = super
@@ -37,12 +37,12 @@ module HTML
# Recurse - handle all dirty nested tags
result == text ? result : sanitize(result, options)
end
-
+
def process_node(node, result, options)
result << node.to_s if node.class == HTML::Text
end
end
-
+
class LinkSanitizer < FullSanitizer
cattr_accessor :included_tags, :instance_writer => false
self.included_tags = Set.new(%w(a href))
@@ -50,51 +50,51 @@ module HTML
def sanitizeable?(text)
!(text.nil? || text.empty? || !((text.index("<a") || text.index("<href")) && text.index(">")))
end
-
+
protected
def process_node(node, result, options)
- result << node.to_s unless node.is_a?(HTML::Tag) && included_tags.include?(node.name)
+ result << node.to_s unless node.is_a?(HTML::Tag) && included_tags.include?(node.name)
end
end
-
+
class WhiteListSanitizer < Sanitizer
[:protocol_separator, :uri_attributes, :allowed_attributes, :allowed_tags, :allowed_protocols, :bad_tags,
:allowed_css_properties, :allowed_css_keywords, :shorthand_css_properties].each do |attr|
- class_inheritable_accessor attr, :instance_writer => false
+ class_attribute attr, :instance_writer => false
end
# A regular expression of the valid characters used to separate protocols like
# the ':' in 'http://foo.com'
self.protocol_separator = /:|(&#0*58)|(&#x70)|(%|&#37;)3A/
-
+
# Specifies a Set of HTML attributes that can have URIs.
self.uri_attributes = Set.new(%w(href src cite action longdesc xlink:href lowsrc))
# Specifies a Set of 'bad' tags that the #sanitize helper will remove completely, as opposed
# to just escaping harmless tags like &lt;font&gt;
self.bad_tags = Set.new(%w(script))
-
+
# Specifies the default Set of tags that the #sanitize helper will allow unscathed.
- self.allowed_tags = Set.new(%w(strong em b i p code pre tt samp kbd var sub
- sup dfn cite big small address hr br div span h1 h2 h3 h4 h5 h6 ul ol li dl dt dd abbr
+ self.allowed_tags = Set.new(%w(strong em b i p code pre tt samp kbd var sub
+ sup dfn cite big small address hr br div span h1 h2 h3 h4 h5 h6 ul ol li dl dt dd abbr
acronym a img blockquote del ins))
- # Specifies the default Set of html attributes that the #sanitize helper will leave
+ # Specifies the default Set of html attributes that the #sanitize helper will leave
# in the allowed tag.
self.allowed_attributes = Set.new(%w(href src width height alt cite datetime title class name xml:lang abbr))
-
+
# Specifies the default Set of acceptable css properties that #sanitize and #sanitize_css will accept.
- self.allowed_protocols = Set.new(%w(ed2k ftp http https irc mailto news gopher nntp telnet webcal xmpp callto
+ self.allowed_protocols = Set.new(%w(ed2k ftp http https irc mailto news gopher nntp telnet webcal xmpp callto
feed svn urn aim rsync tag ssh sftp rtsp afs))
-
+
# Specifies the default Set of acceptable css keywords that #sanitize and #sanitize_css will accept.
- self.allowed_css_properties = Set.new(%w(azimuth background-color border-bottom-color border-collapse
- border-color border-left-color border-right-color border-top-color clear color cursor direction display
+ self.allowed_css_properties = Set.new(%w(azimuth background-color border-bottom-color border-collapse
+ border-color border-left-color border-right-color border-top-color clear color cursor direction display
elevation float font font-family font-size font-style font-variant font-weight height letter-spacing line-height
overflow pause pause-after pause-before pitch pitch-range richness speak speak-header speak-numeral speak-punctuation
speech-rate stress text-align text-decoration text-indent unicode-bidi vertical-align voice-family volume white-space
width))
-
+
# Specifies the default Set of acceptable css keywords that #sanitize and #sanitize_css will accept.
self.allowed_css_keywords = Set.new(%w(auto aqua black block blue bold both bottom brown center
collapse dashed dotted fuchsia gray green !important italic left lime maroon medium none navy normal
@@ -118,9 +118,9 @@ module HTML
style.scan(/([-\w]+)\s*:\s*([^:;]*)/) do |prop,val|
if allowed_css_properties.include?(prop.downcase)
clean << prop + ': ' + val + ';'
- elsif shorthand_css_properties.include?(prop.split('-')[0].downcase)
+ elsif shorthand_css_properties.include?(prop.split('-')[0].downcase)
unless val.split().any? do |keyword|
- !allowed_css_keywords.include?(keyword) &&
+ !allowed_css_keywords.include?(keyword) &&
keyword !~ /^(#[0-9a-f]+|rgb\(\d+%?,\d*%?,?\d*%?\)?|\d{0,2}\.?\d{0,2}(cm|em|ex|in|mm|pc|pt|px|%|,|\))?)$/
end
clean << prop + ': ' + val + ';'
@@ -146,7 +146,7 @@ module HTML
else
options[:parent].unshift node.name
end
-
+
process_attributes_for node, options
options[:tags].include?(node.name) ? node : nil
@@ -154,7 +154,7 @@ module HTML
bad_tags.include?(options[:parent].first) ? nil : node.to_s.gsub(/</, "&lt;")
end
end
-
+
def process_attributes_for(node, options)
return unless node.attributes
node.attributes.keys.each do |attr_name|
@@ -169,8 +169,8 @@ module HTML
end
def contains_bad_protocols?(attr_name, value)
- uri_attributes.include?(attr_name) &&
- (value =~ /(^[^\/:]*):|(&#0*58)|(&#x70)|(%|&#37;)3A/ && !allowed_protocols.include?(value.split(protocol_separator).first))
+ uri_attributes.include?(attr_name) &&
+ (value =~ /(^[^\/:]*):|(&#0*58)|(&#x70)|(%|&#37;)3A/ && !allowed_protocols.include?(value.split(protocol_separator).first.downcase))
end
end
end
diff --git a/actionpack/lib/action_controller/vendor/html-scanner/html/selector.rb b/actionpack/lib/action_controller/vendor/html-scanner/html/selector.rb
index e2c49c284f..0fe2e6d1a6 100644
--- a/actionpack/lib/action_controller/vendor/html-scanner/html/selector.rb
+++ b/actionpack/lib/action_controller/vendor/html-scanner/html/selector.rb
@@ -182,7 +182,7 @@ module HTML
# not another using <tt>:not</tt>. For example:
# p:not(.post)
# Matches all paragraphs that do not have the class <tt>.post</tt>.
- #
+ #
# === Substitution Values
#
# You can use substitution with identifiers, class names and element values.
diff --git a/actionpack/lib/action_controller/vendor/html-scanner/html/tokenizer.rb b/actionpack/lib/action_controller/vendor/html-scanner/html/tokenizer.rb
index 240dc1890f..c252e01cf5 100644
--- a/actionpack/lib/action_controller/vendor/html-scanner/html/tokenizer.rb
+++ b/actionpack/lib/action_controller/vendor/html-scanner/html/tokenizer.rb
@@ -1,7 +1,7 @@
require 'strscan'
module HTML #:nodoc:
-
+
# A simple HTML tokenizer. It simply breaks a stream of text into tokens, where each
# token is a string. Each string represents either "text", or an HTML element.
#
@@ -14,13 +14,13 @@ module HTML #:nodoc:
# p token
# end
class Tokenizer #:nodoc:
-
+
# The current (byte) position in the text
attr_reader :position
-
+
# The current line number
attr_reader :line
-
+
# Create a new Tokenizer for the given text.
def initialize(text)
text.encode! if text.encoding_aware?
@@ -42,7 +42,7 @@ module HTML #:nodoc:
update_current_line(scan_text)
end
end
-
+
private
# Treat the text at the current position as a tag, and scan it. Supports
@@ -69,13 +69,13 @@ module HTML #:nodoc:
def scan_text
"#{@scanner.getch}#{@scanner.scan(/[^<]*/)}"
end
-
+
# Counts the number of newlines in the text and updates the current line
# accordingly.
def update_current_line(text)
text.scan(/\r?\n/) { @current_line += 1 }
end
-
+
# Skips over quoted strings, so that less-than and greater-than characters
# within the strings are ignored.
def consume_quoted_regions
@@ -103,5 +103,5 @@ module HTML #:nodoc:
text
end
end
-
+
end
diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb
index aeec934be8..49971fc9f8 100644
--- a/actionpack/lib/action_dispatch.rb
+++ b/actionpack/lib/action_dispatch.rb
@@ -1,5 +1,5 @@
#--
-# Copyright (c) 2004-2010 David Heinemeier Hansson
+# Copyright (c) 2004-2011 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@@ -53,6 +53,7 @@ module ActionDispatch
autoload :Flash
autoload :Head
autoload :ParamsParser
+ autoload :Reloader
autoload :RemoteIp
autoload :Rescue
autoload :ShowExceptions
@@ -85,6 +86,7 @@ module ActionDispatch
autoload_under 'testing' do
autoload :Assertions
autoload :Integration
+ autoload :IntegrationTest, 'action_dispatch/testing/integration'
autoload :PerformanceTest
autoload :TestProcess
autoload :TestRequest
diff --git a/actionpack/lib/action_dispatch/http/cache.rb b/actionpack/lib/action_dispatch/http/cache.rb
index e9fdf75cc8..4f4cb96a74 100644
--- a/actionpack/lib/action_dispatch/http/cache.rb
+++ b/actionpack/lib/action_dispatch/http/cache.rb
@@ -39,10 +39,11 @@ module ActionDispatch
end
module Response
- attr_reader :cache_control
+ attr_reader :cache_control, :etag
+ alias :etag? :etag
def initialize(*)
- status, header, body = super
+ super
@cache_control = {}
@etag = self["ETag"]
@@ -50,8 +51,7 @@ module ActionDispatch
if cache_control = self["Cache-Control"]
cache_control.split(/,\s*/).each do |segment|
first, last = segment.split("=")
- last ||= true
- @cache_control[first.to_sym] = last
+ @cache_control[first.to_sym] = last || true
end
end
end
@@ -70,14 +70,6 @@ module ActionDispatch
headers['Last-Modified'] = utc_time.httpdate
end
- def etag
- @etag
- end
-
- def etag?
- @etag
- end
-
def etag=(etag)
key = ActiveSupport::Cache.expand_cache_key(etag)
@etag = self["ETag"] = %("#{Digest::MD5.hexdigest(key)}")
@@ -88,38 +80,19 @@ module ActionDispatch
def handle_conditional_get!
if etag? || last_modified? || !@cache_control.empty?
set_conditional_cache_control!
- elsif nonempty_ok_response?
- self.etag = body
-
- if request && request.etag_matches?(etag)
- self.status = 304
- self.body = []
- end
-
- set_conditional_cache_control!
- else
- headers["Cache-Control"] = "no-cache"
end
end
- def nonempty_ok_response?
- @status == 200 && string_body?
- end
-
- def string_body?
- !@blank && @body.respond_to?(:all?) && @body.all? { |part| part.is_a?(String) }
- end
-
DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate"
def set_conditional_cache_control!
- control = @cache_control
-
return if self["Cache-Control"].present?
+ control = @cache_control
+
if control.empty?
headers["Cache-Control"] = DEFAULT_CACHE_CONTROL
- elsif @cache_control[:no_cache]
+ elsif control[:no_cache]
headers["Cache-Control"] = "no-cache"
else
extras = control[:extras]
diff --git a/actionpack/lib/action_dispatch/http/filter_parameters.rb b/actionpack/lib/action_dispatch/http/filter_parameters.rb
index 1ab48ae04d..8dd1af7f3d 100644
--- a/actionpack/lib/action_dispatch/http/filter_parameters.rb
+++ b/actionpack/lib/action_dispatch/http/filter_parameters.rb
@@ -5,10 +5,10 @@ require 'active_support/core_ext/object/duplicable'
module ActionDispatch
module Http
# Allows you to specify sensitive parameters which will be replaced from
- # the request log by looking in all subhashes of the param hash for keys
- # to filter. If a block is given, each key and value of the parameter
- # hash and all subhashes is passed to it, the value or key can be replaced
- # using String#replace or similar method.
+ # the request log by looking in the query string of the request and all
+ # subhashes of the params hash to filter. If a block is given, each key and
+ # value of the params hash and all subhashes is passed to it, the value
+ # or key can be replaced using String#replace or similar method.
#
# Examples:
#
@@ -38,6 +38,11 @@ module ActionDispatch
@filtered_env ||= env_filter.filter(@env)
end
+ # Reconstructed a path with all sensitive GET parameters replaced.
+ def filtered_path
+ @filtered_path ||= query_string.empty? ? path : "#{path}?#{filtered_query_string}"
+ end
+
protected
def parameter_filter
@@ -52,6 +57,14 @@ module ActionDispatch
@@parameter_filter_for[filters] ||= ParameterFilter.new(filters)
end
+ KV_RE = '[^&;=]+'
+ PAIR_RE = %r{(#{KV_RE})=(#{KV_RE})}
+ def filtered_query_string
+ query_string.gsub(PAIR_RE) do |_|
+ parameter_filter.filter([[$1, $2]]).first.join("=")
+ end
+ end
+
end
end
end
diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb
index 4082770b85..68ba1a81b5 100644
--- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb
+++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb
@@ -32,23 +32,25 @@ module ActionDispatch
end
end
- # Returns the Mime type for the \format used in the request.
+ # Returns the MIME type for the \format used in the request.
#
# GET /posts/5.xml | request.format => Mime::XML
# GET /posts/5.xhtml | request.format => Mime::HTML
- # GET /posts/5 | request.format => Mime::HTML or MIME::JS, or request.accepts.first depending on the value of <tt>ActionController::Base.use_accept_header</tt>
+ # GET /posts/5 | request.format => Mime::HTML or MIME::JS, or request.accepts.first
#
def format(view_path = [])
formats.first
end
+ BROWSER_LIKE_ACCEPTS = /,\s*\*\/\*|\*\/\*\s*,/
+
def formats
accept = @env['HTTP_ACCEPT']
@env["action_dispatch.request.formats"] ||=
if parameters[:format]
Array(Mime[parameters[:format]])
- elsif xhr? || (accept && accept !~ /,\s*\*\/\*/)
+ elsif xhr? || (accept && accept !~ BROWSER_LIKE_ACCEPTS)
accepts
else
[Mime::HTML]
diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb
index c6fc582851..7c9ebe7c7b 100644
--- a/actionpack/lib/action_dispatch/http/mime_type.rb
+++ b/actionpack/lib/action_dispatch/http/mime_type.rb
@@ -80,6 +80,9 @@ module Mime
end
class << self
+
+ TRAILING_STAR_REGEXP = /(text|application)\/\*/
+
def lookup(string)
LOOKUP[string]
end
@@ -105,15 +108,28 @@ module Mime
def parse(accept_header)
if accept_header !~ /,/
- [Mime::Type.lookup(accept_header)]
+ if accept_header =~ TRAILING_STAR_REGEXP
+ parse_data_with_trailing_star($1)
+ else
+ [Mime::Type.lookup(accept_header)]
+ end
else
# keep track of creation order to keep the subsequent sort stable
- list = []
- accept_header.split(/,/).each_with_index do |header, index|
- params, q = header.split(/;\s*q=/)
- if params
- params.strip!
- list << AcceptItem.new(index, params, q) unless params.empty?
+ list, index = [], 0
+ accept_header.split(/,/).each do |header|
+ params, q = header.split(/;\s*q=/)
+ if params.present?
+ params.strip!
+
+ if params =~ TRAILING_STAR_REGEXP
+ parse_data_with_trailing_star($1).each do |m|
+ list << AcceptItem.new(index, m.to_s, q)
+ index += 1
+ end
+ else
+ list << AcceptItem.new(index, params, q)
+ index += 1
+ end
end
end
list.sort!
@@ -160,23 +176,51 @@ module Mime
list
end
end
+
+ # input: 'text'
+ # returned value: [Mime::JSON, Mime::XML, Mime::ICS, Mime::HTML, Mime::CSS, Mime::CSV, Mime::JS, Mime::YAML, Mime::TEXT]
+ #
+ # input: 'application'
+ # returned value: [Mime::HTML, Mime::JS, Mime::XML, Mime::YAML, Mime::ATOM, Mime::JSON, Mime::RSS, Mime::URL_ENCODED_FORM]
+ def parse_data_with_trailing_star(input)
+ Mime::SET.select { |m| m =~ input }
+ end
+
+ # This method is opposite of register method.
+ #
+ # Usage:
+ #
+ # Mime::Type.unregister(:mobile)
+ def unregister(symbol)
+ symbol = symbol.to_s.upcase
+ mime = Mime.const_get(symbol)
+ Mime.instance_eval { remove_const(symbol) }
+
+ SET.delete_if { |v| v.eql?(mime) }
+ LOOKUP.delete_if { |k,v| v.eql?(mime) }
+ EXTENSION_LOOKUP.delete_if { |k,v| v.eql?(mime) }
+ end
end
-
+
def initialize(string, symbol = nil, synonyms = [])
@symbol, @synonyms = symbol, synonyms
@string = string
end
-
+
def to_s
@string
end
-
+
def to_str
to_s
end
-
+
def to_sym
- @symbol || @string.to_sym
+ @symbol
+ end
+
+ def ref
+ to_sym || to_s
end
def ===(list)
@@ -186,11 +230,11 @@ module Mime
super
end
end
-
+
def ==(mime_type)
return false if mime_type.blank?
- (@synonyms + [ self ]).any? do |synonym|
- synonym.to_s == mime_type.to_s || synonym.to_sym == mime_type.to_sym
+ (@synonyms + [ self ]).any? do |synonym|
+ synonym.to_s == mime_type.to_s || synonym.to_sym == mime_type.to_sym
end
end
diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb
index add8cab2ab..ef5d207b26 100644
--- a/actionpack/lib/action_dispatch/http/parameters.rb
+++ b/actionpack/lib/action_dispatch/http/parameters.rb
@@ -15,14 +15,14 @@ module ActionDispatch
alias :params :parameters
def path_parameters=(parameters) #:nodoc:
- @env.delete("action_dispatch.request.symbolized_path_parameters")
+ @symbolized_path_params = nil
@env.delete("action_dispatch.request.parameters")
@env["action_dispatch.request.path_parameters"] = parameters
end
# The same as <tt>path_parameters</tt> with explicitly symbolized keys.
def symbolized_path_parameters
- @env["action_dispatch.request.symbolized_path_parameters"] ||= path_parameters.symbolize_keys
+ @symbolized_path_params ||= path_parameters.symbolize_keys
end
# Returns a hash with the \parameters used to form the \path of the request.
diff --git a/actionpack/lib/action_dispatch/http/rack_cache.rb b/actionpack/lib/action_dispatch/http/rack_cache.rb
new file mode 100644
index 0000000000..b5c1435903
--- /dev/null
+++ b/actionpack/lib/action_dispatch/http/rack_cache.rb
@@ -0,0 +1,58 @@
+require "rack/cache"
+require "rack/cache/context"
+require "active_support/cache"
+
+module ActionDispatch
+ class RailsMetaStore < Rack::Cache::MetaStore
+ def self.resolve(uri)
+ new
+ end
+
+ # TODO: Finally deal with the RAILS_CACHE global
+ def initialize(store = RAILS_CACHE)
+ @store = store
+ end
+
+ def read(key)
+ @store.read(key) || []
+ end
+
+ def write(key, value)
+ @store.write(key, value)
+ end
+
+ ::Rack::Cache::MetaStore::RAILS = self
+ end
+
+ class RailsEntityStore < Rack::Cache::EntityStore
+ def self.resolve(uri)
+ new
+ end
+
+ def initialize(store = RAILS_CACHE)
+ @store = store
+ end
+
+ def exist?(key)
+ @store.exist?(key)
+ end
+
+ def open(key)
+ @store.read(key)
+ end
+
+ def read(key)
+ body = open(key)
+ body.join if body
+ end
+
+ def write(body)
+ buf = []
+ key, size = slurp(body) { |part| buf << part }
+ @store.write(key, buf)
+ [key, size]
+ end
+
+ ::Rack::Cache::EntityStore::RAILS = self
+ end
+end
diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb
index fd23b1df79..f07ac44f7a 100644
--- a/actionpack/lib/action_dispatch/http/request.rb
+++ b/actionpack/lib/action_dispatch/http/request.rb
@@ -2,8 +2,10 @@ require 'tempfile'
require 'stringio'
require 'strscan'
+require 'active_support/core_ext/module/deprecation'
require 'active_support/core_ext/hash/indifferent_access'
require 'active_support/core_ext/string/access'
+require 'active_support/inflector'
require 'action_dispatch/http/headers'
module ActionDispatch
@@ -15,6 +17,8 @@ module ActionDispatch
include ActionDispatch::Http::Upload
include ActionDispatch::Http::URL
+ LOCALHOST = [/^127\.0\.0\.\d{1,3}$/, "::1", /^0:0:0:0:0:0:0:1(%.*)?$/].freeze
+
%w[ AUTH_TYPE GATEWAY_INTERFACE
PATH_TRANSLATED REMOTE_HOST
REMOTE_IDENT REMOTE_USER REMOTE_ADDR
@@ -42,8 +46,24 @@ module ActionDispatch
@env.key?(key)
end
- HTTP_METHODS = %w(get head put post delete options)
- HTTP_METHOD_LOOKUP = HTTP_METHODS.inject({}) { |h, m| h[m] = h[m.upcase] = m.to_sym; h }
+ # List of HTTP request methods from the following RFCs:
+ # Hypertext Transfer Protocol -- HTTP/1.1 (http://www.ietf.org/rfc/rfc2616.txt)
+ # HTTP Extensions for Distributed Authoring -- WEBDAV (http://www.ietf.org/rfc/rfc2518.txt)
+ # Versioning Extensions to WebDAV (http://www.ietf.org/rfc/rfc3253.txt)
+ # Ordered Collections Protocol (WebDAV) (http://www.ietf.org/rfc/rfc3648.txt)
+ # Web Distributed Authoring and Versioning (WebDAV) Access Control Protocol (http://www.ietf.org/rfc/rfc3744.txt)
+ # Web Distributed Authoring and Versioning (WebDAV) SEARCH (http://www.ietf.org/rfc/rfc5323.txt)
+ # PATCH Method for HTTP (http://www.ietf.org/rfc/rfc5789.txt)
+ RFC2616 = %w(OPTIONS GET HEAD POST PUT DELETE TRACE CONNECT)
+ RFC2518 = %w(PROPFIND PROPPATCH MKCOL COPY MOVE LOCK UNLOCK)
+ RFC3253 = %w(VERSION-CONTROL REPORT CHECKOUT CHECKIN UNCHECKOUT MKWORKSPACE UPDATE LABEL MERGE BASELINE-CONTROL MKACTIVITY)
+ RFC3648 = %w(ORDERPATCH)
+ RFC3744 = %w(ACL)
+ RFC5323 = %w(SEARCH)
+ RFC5789 = %w(PATCH)
+
+ HTTP_METHODS = RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC5789
+ HTTP_METHOD_LOOKUP = Hash.new { |h, m| h[m] = m.underscore.to_sym if HTTP_METHODS.include?(m) }
# Returns the HTTP \method that the application should see.
# In the case where the \method was overridden by a middleware
@@ -52,11 +72,7 @@ module ActionDispatch
# the application should use), this \method returns the overridden
# value, not the original.
def request_method
- @request_method ||= begin
- method = env["REQUEST_METHOD"]
- HTTP_METHOD_LOOKUP[method] || raise(ActionController::UnknownHttpMethod, "#{method}, accepted HTTP methods are #{HTTP_METHODS.to_sentence(:locale => :en)}")
- method
- end
+ @request_method ||= check_method(env["REQUEST_METHOD"])
end
# Returns a symbol form of the #request_method
@@ -68,11 +84,7 @@ module ActionDispatch
# even if it was overridden by middleware. See #request_method for
# more information.
def method
- @method ||= begin
- method = env["rack.methodoverride.original_method"] || env['REQUEST_METHOD']
- HTTP_METHOD_LOOKUP[method] || raise(ActionController::UnknownHttpMethod, "#{method}, accepted HTTP methods are #{HTTP_METHODS.to_sentence(:locale => :en)}")
- method
- end
+ @method ||= check_method(env["rack.methodoverride.original_method"] || env['REQUEST_METHOD'])
end
# Returns a symbol form of the #method
@@ -122,8 +134,9 @@ module ActionDispatch
end
def forgery_whitelisted?
- get? || xhr? || content_mime_type.nil? || !content_mime_type.verify_request?
+ get?
end
+ deprecate :forgery_whitelisted? => "it is just an alias for 'get?' now, update your code"
def media_type
content_mime_type.to_s
@@ -134,11 +147,11 @@ module ActionDispatch
super.to_i
end
- # Returns true if the request's "X-Requested-With" header contains
- # "XMLHttpRequest". (The Prototype Javascript library sends this header with
- # every Ajax request.)
+ # Returns true if the "X-Requested-With" header contains "XMLHttpRequest"
+ # (case-insensitive). All major JavaScript libraries send this header with
+ # every Ajax request.
def xml_http_request?
- !(@env['HTTP_X_REQUESTED_WITH'] !~ /XMLHttpRequest/i)
+ @env['HTTP_X_REQUESTED_WITH'] =~ /XMLHttpRequest/i
end
alias :xhr? :xml_http_request?
@@ -147,8 +160,16 @@ module ActionDispatch
end
# Which IP addresses are "trusted proxies" that can be stripped from
- # the right-hand-side of X-Forwarded-For
- TRUSTED_PROXIES = /^127\.0\.0\.1$|^(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\./i
+ # the right-hand-side of X-Forwarded-For.
+ #
+ # http://en.wikipedia.org/wiki/Private_network#Private_IPv4_address_spaces.
+ TRUSTED_PROXIES = %r{
+ ^127\.0\.0\.1$ | # localhost
+ ^(10 | # private IP 10.x.x.x
+ 172\.(1[6-9]|2[0-9]|3[0-1]) | # private IP in the range 172.16.0.0 .. 172.31.255.255
+ 192\.168 # private IP 192.168.x.x
+ )\.
+ }x
# Determines originating IP address. REMOTE_ADDR is the standard
# but will fail if the user is behind a proxy. HTTP_CLIENT_IP and/or
@@ -197,7 +218,7 @@ module ActionDispatch
# TODO This should be broken apart into AD::Request::Session and probably
# be included by the session middleware.
def reset_session
- session.destroy if session
+ session.destroy if session && session.respond_to?(:destroy)
self.session = {}
@env['action_dispatch.request.flash_hash'] = nil
end
@@ -212,13 +233,13 @@ module ActionDispatch
# Override Rack's GET method to support indifferent access
def GET
- @env["action_dispatch.request.query_parameters"] ||= normalize_parameters(super)
+ @env["action_dispatch.request.query_parameters"] ||= (normalize_parameters(super) || {})
end
alias :query_parameters :GET
# Override Rack's POST method to support indifferent access
def POST
- @env["action_dispatch.request.request_parameters"] ||= normalize_parameters(super)
+ @env["action_dispatch.request.request_parameters"] ||= (normalize_parameters(super) || {})
end
alias :request_parameters :POST
@@ -231,5 +252,17 @@ module ActionDispatch
@env['X_HTTP_AUTHORIZATION'] ||
@env['REDIRECT_X_HTTP_AUTHORIZATION']
end
+
+ # True if the request came from localhost, 127.0.0.1.
+ def local?
+ LOCALHOST.any? { |local_ip| local_ip === remote_addr && local_ip === remote_ip }
+ end
+
+ private
+
+ def check_method(name)
+ HTTP_METHOD_LOOKUP[name] || raise(ActionController::UnknownHttpMethod, "#{name}, accepted HTTP methods are #{HTTP_METHODS.to_sentence(:locale => :en)}")
+ name
+ end
end
end
diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb
index 3b85a98576..8e03a7879f 100644
--- a/actionpack/lib/action_dispatch/http/response.rb
+++ b/actionpack/lib/action_dispatch/http/response.rb
@@ -4,27 +4,26 @@ require 'active_support/core_ext/object/blank'
require 'active_support/core_ext/class/attribute_accessors'
module ActionDispatch # :nodoc:
- # Represents an HTTP response generated by a controller action. One can use
- # an ActionDispatch::Response object to retrieve the current state
- # of the response, or customize the response. An Response object can
- # either represent a "real" HTTP response (i.e. one that is meant to be sent
- # back to the web browser) or a test response (i.e. one that is generated
- # from integration tests). See CgiResponse and TestResponse, respectively.
+ # Represents an HTTP response generated by a controller action. Use it to
+ # retrieve the current state of the response, or customize the response. It can
+ # either represent a real HTTP response (i.e. one that is meant to be sent
+ # back to the web browser) or a TestResponse (i.e. one that is generated
+ # from integration tests).
#
- # Response is mostly a Ruby on Rails framework implement detail, and
+ # \Response is mostly a Ruby on \Rails framework implementation detail, and
# should never be used directly in controllers. Controllers should use the
# methods defined in ActionController::Base instead. For example, if you want
# to set the HTTP response's content MIME type, then use
# ActionControllerBase#headers instead of Response#headers.
#
# Nevertheless, integration tests may want to inspect controller responses in
- # more detail, and that's when Response can be useful for application
+ # more detail, and that's when \Response can be useful for application
# developers. Integration test methods such as
# ActionDispatch::Integration::Session#get and
# ActionDispatch::Integration::Session#post return objects of type
- # TestResponse (which are of course also of type Response).
+ # TestResponse (which are of course also of type \Response).
#
- # For example, the following demo integration "test" prints the body of the
+ # For example, the following demo integration test prints the body of the
# controller response to the console:
#
# class DemoControllerTest < ActionDispatch::IntegrationTest
@@ -45,8 +44,8 @@ module ActionDispatch # :nodoc:
@block = nil
@length = 0
- @status, @header = status, header
- self.body = body
+ @header = header
+ self.body, self.status = body, status
@cookie = []
@sending_file = false
@@ -133,7 +132,7 @@ module ActionDispatch # :nodoc:
# information.
attr_accessor :charset, :content_type
- CONTENT_TYPE = "Content-Type"
+ CONTENT_TYPE = "Content-Type"
cattr_accessor(:default_charset) { "utf-8" }
@@ -141,7 +140,6 @@ module ActionDispatch # :nodoc:
assign_default_content_type_and_charset!
handle_conditional_get!
self["Set-Cookie"] = self["Set-Cookie"].join("\n") if self["Set-Cookie"].respond_to?(:join)
- self["ETag"] = @_etag if @_etag
super
end
diff --git a/actionpack/lib/action_dispatch/http/upload.rb b/actionpack/lib/action_dispatch/http/upload.rb
index 8ee4b81cdd..37effade4f 100644
--- a/actionpack/lib/action_dispatch/http/upload.rb
+++ b/actionpack/lib/action_dispatch/http/upload.rb
@@ -1,32 +1,34 @@
-require 'active_support/core_ext/object/blank'
-
module ActionDispatch
module Http
- module UploadedFile
- def self.extended(object)
- object.class_eval do
- attr_accessor :original_path, :content_type
- alias_method :local_path, :path if method_defined?(:path)
- end
+ class UploadedFile
+ attr_accessor :original_filename, :content_type, :tempfile, :headers
+
+ def initialize(hash)
+ @original_filename = hash[:filename]
+ @content_type = hash[:type]
+ @headers = hash[:head]
+ @tempfile = hash[:tempfile]
+ raise(ArgumentError, ':tempfile is required') unless @tempfile
end
- # Take the basename of the upload's original filename.
- # This handles the full Windows paths given by Internet Explorer
- # (and perhaps other broken user agents) without affecting
- # those which give the lone filename.
- # The Windows regexp is adapted from Perl's File::Basename.
- def original_filename
- unless defined? @original_filename
- @original_filename =
- unless original_path.blank?
- if original_path =~ /^(?:.*[:\\\/])?(.*)/m
- $1
- else
- File.basename original_path
- end
- end
- end
- @original_filename
+ def open
+ @tempfile.open
+ end
+
+ def path
+ @tempfile.path
+ end
+
+ def read(*args)
+ @tempfile.read(*args)
+ end
+
+ def rewind
+ @tempfile.rewind
+ end
+
+ def size
+ @tempfile.size
end
end
@@ -35,11 +37,7 @@ module ActionDispatch
# file upload hash with UploadedFile objects
def normalize_parameters(value)
if Hash === value && value.has_key?(:tempfile)
- upload = value[:tempfile]
- upload.extend(UploadedFile)
- upload.original_path = value[:filename]
- upload.content_type = value[:type]
- upload
+ UploadedFile.new(value)
else
super
end
@@ -47,4 +45,4 @@ module ActionDispatch
private :normalize_parameters
end
end
-end \ No newline at end of file
+end
diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb
index b64a83c62e..ac0fd9607d 100644
--- a/actionpack/lib/action_dispatch/http/url.rb
+++ b/actionpack/lib/action_dispatch/http/url.rb
@@ -1,6 +1,81 @@
module ActionDispatch
module Http
module URL
+ mattr_accessor :tld_length
+ self.tld_length = 1
+
+ class << self
+ def extract_domain(host, tld_length = @@tld_length)
+ return nil unless named_host?(host)
+ host.split('.').last(1 + tld_length).join('.')
+ end
+
+ def extract_subdomains(host, tld_length = @@tld_length)
+ return [] unless named_host?(host)
+ parts = host.split('.')
+ parts[0..-(tld_length+2)]
+ end
+
+ def extract_subdomain(host, tld_length = @@tld_length)
+ extract_subdomains(host, tld_length).join('.')
+ end
+
+ def url_for(options = {})
+ unless options[:host].present? || options[:only_path].present?
+ raise ArgumentError, 'Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true'
+ end
+
+ rewritten_url = ""
+
+ unless options[:only_path]
+ unless options[:protocol] == false
+ rewritten_url << (options[:protocol] || "http")
+ rewritten_url << ":" unless rewritten_url.match(%r{:|//})
+ end
+ rewritten_url << "//" unless rewritten_url.match("//")
+ rewritten_url << rewrite_authentication(options)
+ rewritten_url << host_or_subdomain_and_domain(options)
+ rewritten_url << ":#{options.delete(:port)}" if options[:port]
+ end
+
+ path = options.delete(:path) || ''
+
+ params = options[:params] || {}
+ params.reject! {|k,v| v.to_param.nil? }
+
+ rewritten_url << (options[:trailing_slash] ? path.sub(/\?|\z/) { "/" + $& } : path)
+ rewritten_url << "?#{params.to_query}" unless params.empty?
+ rewritten_url << "##{Rack::Mount::Utils.escape_uri(options[:anchor].to_param.to_s)}" if options[:anchor]
+ rewritten_url
+ end
+
+ private
+
+ def named_host?(host)
+ !(host.nil? || /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.match(host))
+ end
+
+ def rewrite_authentication(options)
+ if options[:user] && options[:password]
+ "#{Rack::Utils.escape(options[:user])}:#{Rack::Utils.escape(options[:password])}@"
+ else
+ ""
+ end
+ end
+
+ def host_or_subdomain_and_domain(options)
+ return options[:host] unless options[:subdomain] || options[:domain]
+
+ tld_length = options[:tld_length] || @@tld_length
+
+ host = ""
+ host << (options[:subdomain] || extract_subdomain(options[:host], tld_length))
+ host << "."
+ host << (options[:domain] || extract_domain(options[:host], tld_length))
+ host
+ end
+ end
+
# Returns the complete URL used for this request.
def url
protocol + host_with_port + fullpath
@@ -8,12 +83,7 @@ module ActionDispatch
# Returns 'https://' if this is an SSL request and 'http://' otherwise.
def protocol
- ssl? ? 'https://' : 'http://'
- end
-
- # Is this an SSL request?
- def ssl?
- @env['HTTPS'] == 'on' || @env['HTTP_X_FORWARDED_PROTO'] == 'https'
+ @protocol ||= ssl? ? 'https://' : 'http://'
end
# Returns the \host for this request, such as "example.com".
@@ -38,10 +108,12 @@ module ActionDispatch
# Returns the port number of this request as an integer.
def port
- if raw_host_with_port =~ /:(\d+)$/
- $1.to_i
- else
- standard_port
+ @port ||= begin
+ if raw_host_with_port =~ /:(\d+)$/
+ $1.to_i
+ else
+ standard_port
+ end
end
end
@@ -53,10 +125,21 @@ module ActionDispatch
end
end
- # Returns a \port suffix like ":8080" if the \port number of this request
+ # Returns whether this request is using the standard port
+ 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.
+ 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.
def port_string
- port == standard_port ? '' : ":#{port}"
+ standard_port? ? '' : ":#{port}"
end
def server_port
@@ -65,38 +148,25 @@ module ActionDispatch
# Returns the \domain part of a \host, such as "rubyonrails.org" in "www.rubyonrails.org". You can specify
# a different <tt>tld_length</tt>, such as 2 to catch rubyonrails.co.uk in "www.rubyonrails.co.uk".
- def domain(tld_length = 1)
- return nil unless named_host?(host)
-
- host.split('.').last(1 + tld_length).join('.')
+ def domain(tld_length = @@tld_length)
+ ActionDispatch::Http::URL.extract_domain(host, tld_length)
end
# Returns all the \subdomains as an array, so <tt>["dev", "www"]</tt> would be
# returned for "dev.www.rubyonrails.org". You can specify a different <tt>tld_length</tt>,
# such as 2 to catch <tt>["www"]</tt> instead of <tt>["www", "rubyonrails"]</tt>
# in "www.rubyonrails.co.uk".
- def subdomains(tld_length = 1)
- return [] unless named_host?(host)
- parts = host.split('.')
- parts[0..-(tld_length+2)]
- end
-
- def subdomain(tld_length = 1)
- subdomains(tld_length).join('.')
- end
-
- # Returns the request URI, accounting for server idiosyncrasies.
- # WEBrick includes the full URL. IIS leaves REQUEST_URI blank.
- def request_uri
- ActiveSupport::Deprecation.warn "Using #request_uri is deprecated. Use fullpath instead.", caller
- fullpath
+ def subdomains(tld_length = @@tld_length)
+ ActionDispatch::Http::URL.extract_subdomains(host, tld_length)
end
- private
-
- def named_host?(host)
- !(host.nil? || /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.match(host))
+ # Returns all the \subdomains as a string, so <tt>"dev.www"</tt> would be
+ # returned for "dev.www.rubyonrails.org". You can specify a different <tt>tld_length</tt>,
+ # such as 2 to catch <tt>["www"]</tt> instead of <tt>"www.rubyonrails"</tt>
+ # in "www.rubyonrails.co.uk".
+ def subdomain(tld_length = @@tld_length)
+ subdomains(tld_length)
end
end
end
-end \ No newline at end of file
+end
diff --git a/actionpack/lib/action_dispatch/middleware/callbacks.rb b/actionpack/lib/action_dispatch/middleware/callbacks.rb
index e4ae480bfb..1bb2ad7f67 100644
--- a/actionpack/lib/action_dispatch/middleware/callbacks.rb
+++ b/actionpack/lib/action_dispatch/middleware/callbacks.rb
@@ -1,30 +1,14 @@
+require 'active_support/core_ext/module/delegation'
+
module ActionDispatch
# Provide callbacks to be executed before and after the request dispatch.
- #
- # It also provides a to_prepare callback, which is performed in all requests
- # in development by only once in production and notification callback for async
- # operations.
- #
class Callbacks
include ActiveSupport::Callbacks
define_callbacks :call, :rescuable => true
- define_callbacks :prepare, :scope => :name
- # Add a preparation callback. Preparation callbacks are run before every
- # request in development mode, and before the first request in production mode.
- #
- # If a symbol with a block is given, the symbol is used as an identifier.
- # That allows to_prepare to be called again with the same identifier to
- # replace the existing callback. Passing an identifier is a suggested
- # practice if the code adding a preparation block may be reloaded.
- def self.to_prepare(*args, &block)
- if args.first.is_a?(Symbol) && block_given?
- define_method :"__#{args.first}", &block
- set_callback(:prepare, :"__#{args.first}")
- else
- set_callback(:prepare, *args, &block)
- end
+ class << self
+ delegate :to_prepare, :to_cleanup, :to => "ActionDispatch::Reloader"
end
def self.before(*args, &block)
@@ -35,14 +19,13 @@ module ActionDispatch
set_callback(:call, :after, *args, &block)
end
- def initialize(app, prepare_each_request = false)
- @app, @prepare_each_request = app, prepare_each_request
- _run_prepare_callbacks
+ def initialize(app, unused = nil)
+ ActiveSupport::Deprecation.warn "Passing a second argument to ActionDispatch::Callbacks.new is deprecated." unless unused.nil?
+ @app = app
end
def call(env)
- _run_call_callbacks do
- _run_prepare_callbacks if @prepare_each_request
+ run_callbacks :call do
@app.call(env)
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb
index 4d33cd3b0c..7ac608f0a8 100644
--- a/actionpack/lib/action_dispatch/middleware/cookies.rb
+++ b/actionpack/lib/action_dispatch/middleware/cookies.rb
@@ -7,7 +7,7 @@ module ActionDispatch
end
end
- # Cookies are read and written through ActionController#cookies.
+ # \Cookies are read and written through ActionController#cookies.
#
# The cookies being read are the ones received along with the request, the cookies
# being written will be sent out with the response. Reading a cookie does not get
@@ -16,15 +16,31 @@ module ActionDispatch
# Examples for writing:
#
# # Sets a simple session cookie.
+ # # This cookie will be deleted when the user's browser is closed.
# cookies[:user_name] = "david"
#
+ # # Assign an array of values to a cookie.
+ # cookies[:lat_lon] = [47.68, -122.37]
+ #
# # Sets a cookie that expires in 1 hour.
# cookies[:login] = { :value => "XJ-122", :expires => 1.hour.from_now }
#
+ # # Sets a signed cookie, which prevents a user from tampering with its value.
+ # # The cookie is signed by your app's <tt>config.secret_token</tt> value.
+ # # Rails generates this value by default when you create a new Rails app.
+ # cookies.signed[:user_id] = current_user.id
+ #
+ # # Sets a "permanent" cookie (which expires in 20 years from now).
+ # cookies.permanent[:login] = "XJ-122"
+ #
+ # # You can also chain these methods:
+ # cookies.permanent.signed[:login] = "XJ-122"
+ #
# Examples for reading:
#
# cookies[:user_name] # => "david"
# cookies.size # => 2
+ # cookies[:lat_lon] # => [47.68, -122.37]
#
# Example for deleting:
#
@@ -55,7 +71,7 @@ module ActionDispatch
# :domain => :all # Allow the cookie for the top most level
# domain and subdomains.
#
- # * <tt>:expires</tt> - The time at which this cookie expires, as a Time object.
+ # * <tt>:expires</tt> - The time at which this cookie expires, as a \Time object.
# * <tt>:secure</tt> - Whether this cookie is a only transmitted to HTTPS servers.
# Default is +false+.
# * <tt>:httponly</tt> - Whether this cookie is accessible via scripting or
@@ -69,27 +85,36 @@ module ActionDispatch
class CookieJar < Hash #:nodoc:
- # This regular expression is used to split the levels of a domain
- # So www.example.co.uk gives:
- # $1 => www.
- # $2 => example
- # $3 => co.uk
- DOMAIN_REGEXP = /^(.*\.)*(.*)\.(...|...\...|....|..\...|..)$/
+ # This regular expression is used to split the levels of a domain.
+ # The top level domain can be any string without a period or
+ # **.**, ***.** style TLDs like co.uk or com.au
+ #
+ # www.example.co.uk gives:
+ # $& => example.co.uk
+ #
+ # example.com gives:
+ # $& => example.com
+ #
+ # lots.of.subdomains.example.local gives:
+ # $& => example.local
+ DOMAIN_REGEXP = /[^.]*\.([^.]*|..\...|...\...)$/
def self.build(request)
secret = request.env[TOKEN_KEY]
- host = request.env["HTTP_HOST"]
+ host = request.host
+ secure = request.ssl?
- new(secret, host).tap do |hash|
+ new(secret, host, secure).tap do |hash|
hash.update(request.cookies)
end
end
- def initialize(secret = nil, host = nil)
+ def initialize(secret = nil, host = nil, secure = false)
@secret = secret
@set_cookies = {}
@delete_cookies = {}
@host = host
+ @secure = secure
super()
end
@@ -103,8 +128,17 @@ module ActionDispatch
options[:path] ||= "/"
if options[:domain] == :all
- @host =~ DOMAIN_REGEXP
- options[:domain] = ".#{$2}.#{$3}"
+ # 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)
+ ".#{$&}"
+ 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[/^\.?(.*)$/, 1] }
end
end
@@ -174,9 +208,15 @@ module ActionDispatch
end
def write(headers)
- @set_cookies.each { |k, v| ::Rack::Utils.set_cookie_header!(headers, k, v) }
+ @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
+
+ private
+
+ def write_cookie?(cookie)
+ @secure || !cookie[:secure] || defined?(Rails.env) && Rails.env.development?
+ end
end
class PermanentCookieJar < CookieJar #:nodoc:
@@ -248,7 +288,7 @@ module ActionDispatch
"integrity hash for cookie session data. Use " +
"config.secret_token = \"some secret phrase of at " +
"least #{SECRET_MIN_LENGTH} characters\"" +
- "in config/application.rb"
+ "in config/initializers/secret_token.rb"
end
if secret.length < SECRET_MIN_LENGTH
diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb
index bfa30cf5af..21aeeb217a 100644
--- a/actionpack/lib/action_dispatch/middleware/flash.rb
+++ b/actionpack/lib/action_dispatch/middleware/flash.rb
@@ -10,13 +10,13 @@ module ActionDispatch
# The flash provides a way to pass temporary objects 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] = "Successfully created"</tt> before redirecting to a display action that can
+ # action that sets <tt>flash[:notice] = "Post successfully created"</tt> before redirecting to a display action that can
# then expose the flash to its template. Actually, that exposure is automatically done. Example:
#
# class PostsController < ActionController::Base
# def create
# # save post
- # flash[:notice] = "Successfully created post"
+ # flash[:notice] = "Post successfully created"
# redirect_to posts_path(@post)
# end
#
@@ -30,6 +30,11 @@ module ActionDispatch
# <div class="notice"><%= flash[:notice] %></div>
# <% end %>
#
+ # Since the +notice+ and +alert+ keys are a common idiom, convenience accessors are available:
+ #
+ # flash.alert = "You must be logged in"
+ # flash.notice = "Post successfully created"
+ #
# This example just places a string in the flash, but you can put any object in there. And of course, you can put as
# many as you like at a time too. Just remember: They'll be gone by the time the next action has been performed.
#
diff --git a/actionpack/lib/action_dispatch/middleware/reloader.rb b/actionpack/lib/action_dispatch/middleware/reloader.rb
new file mode 100644
index 0000000000..29289a76b4
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/reloader.rb
@@ -0,0 +1,76 @@
+module ActionDispatch
+ # ActionDispatch::Reloader provides prepare and cleanup callbacks,
+ # intended to assist with code reloading during development.
+ #
+ # Prepare callbacks are run before each request, and cleanup callbacks
+ # after each request. In this respect they are analogs of ActionDispatch::Callback's
+ # before and after callbacks. However, cleanup callbacks are not called until the
+ # request is fully complete -- that is, after #close has been called on
+ # the response body. This is important for streaming responses such as the
+ # following:
+ #
+ # self.response_body = lambda { |response, output|
+ # # code here which refers to application models
+ # }
+ #
+ # Cleanup callbacks will not be called until after the response_body lambda
+ # is evaluated, ensuring that it can refer to application models and other
+ # classes before they are unloaded.
+ #
+ # By default, ActionDispatch::Reloader is included in the middleware stack
+ # only in the development environment; specifically, when config.cache_classes
+ # is false. Callbacks may be registered even when it is not included in the
+ # middleware stack, but are executed only when +ActionDispatch::Reloader.prepare!+
+ # or +ActionDispatch::Reloader.cleanup!+ are called manually.
+ #
+ class Reloader
+ include ActiveSupport::Callbacks
+
+ define_callbacks :prepare, :scope => :name
+ define_callbacks :cleanup, :scope => :name
+
+ # Add a prepare callback. Prepare callbacks are run before each request, prior
+ # to ActionDispatch::Callback's before callbacks.
+ def self.to_prepare(*args, &block)
+ set_callback(:prepare, *args, &block)
+ end
+
+ # Add a cleanup callback. Cleanup callbacks are run after each request is
+ # complete (after #close is called on the response body).
+ def self.to_cleanup(*args, &block)
+ set_callback(:cleanup, *args, &block)
+ end
+
+ # Execute all prepare callbacks.
+ def self.prepare!
+ new(nil).run_callbacks :prepare
+ end
+
+ # Execute all cleanup callbacks.
+ def self.cleanup!
+ new(nil).run_callbacks :cleanup
+ end
+
+ def initialize(app)
+ @app = app
+ end
+
+ module CleanupOnClose
+ def close
+ super if defined?(super)
+ ensure
+ ActionDispatch::Reloader.cleanup!
+ end
+ end
+
+ def call(env)
+ run_callbacks :prepare
+ response = @app.call(env)
+ response[2].extend(CleanupOnClose)
+ response
+ rescue Exception
+ run_callbacks :cleanup
+ raise
+ end
+ 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 dd82294644..64d3a87fd0 100644
--- a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb
+++ b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb
@@ -1,5 +1,6 @@
require 'rack/utils'
require 'rack/request'
+require 'rack/session/abstract/id'
require 'action_dispatch/middleware/cookies'
require 'active_support/core_ext/object/blank'
@@ -8,249 +9,76 @@ module ActionDispatch
class SessionRestoreError < StandardError #:nodoc:
end
- class AbstractStore
- ENV_SESSION_KEY = 'rack.session'.freeze
- ENV_SESSION_OPTIONS_KEY = 'rack.session.options'.freeze
-
- # thin wrapper around Hash that allows us to lazily
- # load session id into session_options
- class OptionsHash < Hash
- def initialize(by, env, default_options)
- @by = by
- @env = env
- @session_id_loaded = false
- merge!(default_options)
- end
-
- def [](key)
- if key == :id
- load_session_id! unless key?(:id) || has_session_id?
- end
- super
- end
-
- private
-
- def has_session_id?
- @session_id_loaded
- end
-
- def load_session_id!
- self[:id] = @by.send(:extract_session_id, @env)
- @session_id_loaded = true
- end
- end
-
- class SessionHash < Hash
- def initialize(by, env)
- super()
- @by = by
- @env = env
- @loaded = false
- end
-
- def [](key)
- load_for_read!
- super(key.to_s)
- end
-
- def has_key?(key)
- load_for_read!
- super(key.to_s)
- end
-
- def []=(key, value)
- load_for_write!
- super(key.to_s, value)
- end
-
- def clear
- load_for_write!
- super
- end
-
- def to_hash
- load_for_read!
- h = {}.replace(self)
- h.delete_if { |k,v| v.nil? }
- h
- end
-
- def update(hash)
- load_for_write!
- super(hash.stringify_keys)
- end
-
- def delete(key)
- load_for_write!
- super(key.to_s)
- end
-
- def inspect
- load_for_read!
- super
- end
-
- def exists?
- return @exists if instance_variable_defined?(:@exists)
- @exists = @by.send(:exists?, @env)
- end
-
- def loaded?
- @loaded
- end
-
- def destroy
- clear
- @by.send(:destroy, @env) if @by
- @env[ENV_SESSION_OPTIONS_KEY][:id] = nil if @env && @env[ENV_SESSION_OPTIONS_KEY]
- @loaded = false
- end
-
- private
-
- def load_for_read!
- load! if !loaded? && exists?
- end
-
- def load_for_write!
- load! unless loaded?
- end
-
- def load!
- id, session = @by.send(:load_session, @env)
- @env[ENV_SESSION_OPTIONS_KEY][:id] = id
- replace(session.stringify_keys)
- @loaded = true
- end
-
+ module DestroyableSession
+ def destroy
+ clear
+ options = @env[Rack::Session::Abstract::ENV_SESSION_OPTIONS_KEY] if @env
+ options ||= {}
+ @by.send(:destroy_session, @env, options[:id], options) if @by
+ options[:id] = nil
+ @loaded = false
end
+ end
- DEFAULT_OPTIONS = {
- :key => '_session_id',
- :path => '/',
- :domain => nil,
- :expire_after => nil,
- :secure => false,
- :httponly => true,
- :cookie_only => true
- }
+ ::Rack::Session::Abstract::SessionHash.send :include, DestroyableSession
+ module Compatibility
def initialize(app, options = {})
- @app = app
- @default_options = DEFAULT_OPTIONS.merge(options)
- @key = @default_options.delete(:key).freeze
- @cookie_only = @default_options.delete(:cookie_only)
- ensure_session_key!
+ options[:key] ||= '_session_id'
+ super
end
- def call(env)
- prepare!(env)
- response = @app.call(env)
-
- session_data = env[ENV_SESSION_KEY]
- options = env[ENV_SESSION_OPTIONS_KEY]
-
- if !session_data.is_a?(AbstractStore::SessionHash) || session_data.loaded? || options[:expire_after]
- session_data.send(:load!) if session_data.is_a?(AbstractStore::SessionHash) && !session_data.loaded?
-
- sid = options[:id] || generate_sid
- session_data = session_data.to_hash
-
- value = set_session(env, sid, session_data)
- return response unless value
-
- cookie = { :value => value }
- unless options[:expire_after].nil?
- cookie[:expires] = Time.now + options.delete(:expire_after)
- end
-
- request = ActionDispatch::Request.new(env)
- set_cookie(request, cookie.merge!(options))
- end
-
- response
+ def generate_sid
+ ActiveSupport::SecureRandom.hex(16)
end
- private
-
- def prepare!(env)
- env[ENV_SESSION_KEY] = SessionHash.new(self, env)
- env[ENV_SESSION_OPTIONS_KEY] = OptionsHash.new(self, env, @default_options)
- end
-
- def generate_sid
- ActiveSupport::SecureRandom.hex(16)
- end
-
- def set_cookie(request, options)
- if request.cookie_jar[@key] != options[:value] || !options[:expires].nil?
- request.cookie_jar[@key] = options
- end
- end
-
- def load_session(env)
- stale_session_check! do
- sid = current_session_id(env)
- sid, session = get_session(env, sid)
- [sid, session]
- end
- end
+ protected
- def extract_session_id(env)
- stale_session_check! do
- request = ActionDispatch::Request.new(env)
- sid = request.cookies[@key]
- sid ||= request.params[@key] unless @cookie_only
- sid
- end
- end
+ def initialize_sid
+ @default_options.delete(:sidbits)
+ @default_options.delete(:secure_random)
+ end
+ end
- def current_session_id(env)
- env[ENV_SESSION_OPTIONS_KEY][:id]
- end
+ module StaleSessionCheck
+ def load_session(env)
+ stale_session_check! { super }
+ end
- def ensure_session_key!
- if @key.blank?
- raise ArgumentError, 'A key is required to write a ' +
- 'cookie containing the session data. Use ' +
- 'config.session_store SESSION_STORE, { :key => ' +
- '"_myapp_session" } in config/application.rb'
- end
- end
+ def extract_session_id(env)
+ stale_session_check! { super }
+ end
- def stale_session_check!
- yield
- rescue ArgumentError => argument_error
- if argument_error.message =~ %r{undefined class/module ([\w:]*\w)}
- begin
- # Note that the regexp does not allow $1 to end with a ':'
- $1.constantize
- rescue LoadError, NameError => const_error
- raise ActionDispatch::Session::SessionRestoreError, "Session contains objects whose class definition isn't available.\nRemember to require the classes for all objects kept in the session.\n(Original exception: #{const_error.message} [#{const_error.class}])\n"
- end
- retry
- else
- raise
+ def stale_session_check!
+ yield
+ rescue ArgumentError => argument_error
+ if argument_error.message =~ %r{undefined class/module ([\w:]*\w)}
+ begin
+ # Note that the regexp does not allow $1 to end with a ':'
+ $1.constantize
+ rescue LoadError, NameError => const_error
+ raise ActionDispatch::Session::SessionRestoreError, "Session contains objects whose class definition isn't available.\nRemember to require the classes for all objects kept in the session.\n(Original exception: #{const_error.message} [#{const_error.class}])\n"
end
+ retry
+ else
+ raise
end
+ end
+ end
- def exists?(env)
- current_session_id(env).present?
- end
-
- def get_session(env, sid)
- raise '#get_session needs to be implemented.'
- end
+ class AbstractStore < Rack::Session::Abstract::ID
+ include Compatibility
+ include StaleSessionCheck
- def set_session(env, sid, session_data)
- raise '#set_session needs to be implemented and should return ' <<
- 'the value to be stored in the cookie (usually the sid)'
- end
+ def destroy_session(env, sid, options)
+ ActiveSupport::Deprecation.warn "Implementing #destroy in session stores is deprecated. " <<
+ "Please implement destroy_session(env, session_id, options) instead."
+ destroy(env)
+ end
- def destroy(env)
- raise '#destroy needs to be implemented.'
- end
+ def destroy(env)
+ raise '#destroy needs to be implemented.'
+ end
end
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
index ca1494425f..9c9ccc62f5 100644
--- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
+++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
@@ -1,5 +1,7 @@
require 'active_support/core_ext/hash/keys'
require 'active_support/core_ext/object/blank'
+require 'action_dispatch/middleware/session/abstract_store'
+require 'rack/session/cookie'
module ActionDispatch
module Session
@@ -38,58 +40,32 @@ module ActionDispatch
# "rake secret" and set the key in config/initializers/secret_token.rb.
#
# Note that changing digest or secret invalidates all existing sessions!
- class CookieStore < AbstractStore
-
- def initialize(app, options = {})
- super(app, options.merge!(:cookie_only => true))
- freeze
- end
+ class CookieStore < Rack::Session::Cookie
+ include Compatibility
+ include StaleSessionCheck
private
- def load_session(env)
- data = unpacked_cookie_data(env)
- data = persistent_session_id!(data)
- [data["session_id"], data]
- end
-
- def extract_session_id(env)
- if data = unpacked_cookie_data(env)
- data["session_id"]
- else
- nil
- end
- end
-
- def unpacked_cookie_data(env)
- env["action_dispatch.request.unsigned_session_cookie"] ||= begin
- stale_session_check! do
- request = ActionDispatch::Request.new(env)
- if data = request.cookie_jar.signed[@key]
- data.stringify_keys!
- end
- data || {}
+ def unpacked_cookie_data(env)
+ env["action_dispatch.request.unsigned_session_cookie"] ||= begin
+ stale_session_check! do
+ request = ActionDispatch::Request.new(env)
+ if data = request.cookie_jar.signed[@key]
+ data.stringify_keys!
end
+ data || {}
end
end
+ end
- def set_cookie(request, options)
- request.cookie_jar.signed[@key] = options
- end
-
- def set_session(env, sid, session_data)
- persistent_session_id!(session_data, sid)
- end
-
- def destroy(env)
- # session data is stored on client; nothing to do here
- end
+ def set_session(env, sid, session_data, options)
+ persistent_session_id!(session_data, sid)
+ end
- def persistent_session_id!(data, sid=nil)
- data ||= {}
- data["session_id"] ||= sid || generate_sid
- data
- end
+ def set_cookie(env, session_id, cookie)
+ request = ActionDispatch::Request.new(env)
+ request.cookie_jar.signed[@key] = cookie
+ end
end
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb b/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb
index 28e3dbd732..4dd9a946c2 100644
--- a/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb
+++ b/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb
@@ -1,56 +1,17 @@
+require 'action_dispatch/middleware/session/abstract_store'
+require 'rack/session/memcache'
+
module ActionDispatch
module Session
- class MemCacheStore < AbstractStore
+ class MemCacheStore < Rack::Session::Memcache
+ include Compatibility
+ include StaleSessionCheck
+
def initialize(app, options = {})
require 'memcache'
-
- # Support old :expires option
options[:expire_after] ||= options[:expires]
-
- super
-
- @default_options = {
- :namespace => 'rack:session',
- :memcache_server => 'localhost:11211'
- }.merge(@default_options)
-
- @pool = options[:cache] || MemCache.new(@default_options[:memcache_server], @default_options)
- unless @pool.servers.any? { |s| s.alive? }
- raise "#{self} unable to find server during initialization."
- end
- @mutex = Mutex.new
-
super
end
-
- private
- def get_session(env, sid)
- sid ||= generate_sid
- begin
- session = @pool.get(sid) || {}
- rescue MemCache::MemCacheError, Errno::ECONNREFUSED
- session = {}
- end
- [sid, session]
- end
-
- def set_session(env, sid, session_data)
- options = env['rack.session.options']
- expiry = options[:expire_after] || 0
- @pool.set(sid, session_data, expiry)
- sid
- rescue MemCache::MemCacheError, Errno::ECONNREFUSED
- false
- end
-
- def destroy(env)
- if sid = current_session_id(env)
- @pool.delete(sid)
- end
- rescue MemCache::MemCacheError, Errno::ECONNREFUSED
- false
- end
-
end
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb
index e095b51342..dbe3206808 100644
--- a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb
+++ b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb
@@ -6,8 +6,6 @@ module ActionDispatch
# This middleware rescues any exception returned by the application and renders
# nice exception pages if it's being rescued locally.
class ShowExceptions
- LOCALHOST = [/^127\.0\.0\.\d{1,3}$/, "::1", /^0:0:0:0:0:0:0:1(%.*)?$/].freeze
-
RESCUES_TEMPLATE_PATH = File.join(File.dirname(__FILE__), 'templates')
cattr_accessor :rescue_responses
@@ -45,28 +43,29 @@ module ActionDispatch
end
def call(env)
- status, headers, body = @app.call(env)
-
- # Only this middleware cares about RoutingError. So, let's just raise
- # it here.
- # TODO: refactor this middleware to handle the X-Cascade scenario without
- # having to raise an exception.
- if headers['X-Cascade'] == 'pass'
- raise ActionController::RoutingError, "No route matches #{env['PATH_INFO'].inspect}"
+ begin
+ status, headers, body = @app.call(env)
+ exception = nil
+
+ # Only this middleware cares about RoutingError. So, let's just raise
+ # it here.
+ if headers['X-Cascade'] == 'pass'
+ raise ActionController::RoutingError, "No route matches #{env['PATH_INFO'].inspect}"
+ end
+ rescue Exception => exception
+ raise exception if env['action_dispatch.show_exceptions'] == false
end
- [status, headers, body]
- rescue Exception => exception
- raise exception if env['action_dispatch.show_exceptions'] == false
- render_exception(env, exception)
+ exception ? render_exception(env, exception) : [status, headers, body]
end
private
def render_exception(env, exception)
log_error(exception)
+ exception = original_exception(exception)
request = Request.new(env)
- if @consider_all_requests_local || local_request?(request)
+ if @consider_all_requests_local || request.local?
rescue_action_locally(request, exception)
else
rescue_action_in_public(exception)
@@ -112,11 +111,6 @@ module ActionDispatch
end
end
- # True if the request came from localhost, 127.0.0.1.
- def local_request?(request)
- LOCALHOST.any? { |local_ip| local_ip === request.remote_addr && local_ip === request.remote_ip }
- end
-
def status_code(exception)
Rack::Utils.status_code(@@rescue_responses[exception.class.name])
end
@@ -134,7 +128,7 @@ module ActionDispatch
ActiveSupport::Deprecation.silence do
message = "\n#{exception.class} (#{exception.message}):\n"
- message << exception.annoted_source_code if exception.respond_to?(:annoted_source_code)
+ message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code)
message << " " << application_trace(exception).join("\n ")
logger.fatal("#{message}\n\n")
end
@@ -161,5 +155,17 @@ module ActionDispatch
def logger
defined?(Rails.logger) ? Rails.logger : Logger.new($stderr)
end
+
+ def original_exception(exception)
+ if registered_original_exception?(exception)
+ exception.original_exception
+ else
+ exception
+ end
+ end
+
+ def registered_original_exception?(exception)
+ exception.respond_to?(:original_exception) && @@rescue_responses.has_key?(exception.original_exception.class.name)
+ end
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/stack.rb b/actionpack/lib/action_dispatch/middleware/stack.rb
index 41078eced7..a4308f528c 100644
--- a/actionpack/lib/action_dispatch/middleware/stack.rb
+++ b/actionpack/lib/action_dispatch/middleware/stack.rb
@@ -1,17 +1,27 @@
require "active_support/inflector/methods"
+require "active_support/dependencies"
module ActionDispatch
- class MiddlewareStack < Array
+ class MiddlewareStack
class Middleware
- attr_reader :args, :block
+ attr_reader :args, :block, :name, :classcache
def initialize(klass_or_name, *args, &block)
- @ref = ActiveSupport::Dependencies::Reference.new(klass_or_name)
+ @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
end
def klass
- @ref.get
+ @klass || classcache[@name]
end
def ==(middleware)
@@ -21,7 +31,7 @@ module ActionDispatch
when Class
klass == middleware
else
- normalize(@ref.name) == normalize(middleware)
+ normalize(@name) == normalize(middleware)
end
end
@@ -40,15 +50,39 @@ module ActionDispatch
end
end
- def initialize(*args, &block)
- super(*args)
- block.call(self) if block_given?
+ include Enumerable
+
+ attr_accessor :middlewares
+
+ def initialize(*args)
+ @middlewares = []
+ yield(self) if block_given?
+ end
+
+ def each
+ @middlewares.each { |x| yield x }
+ end
+
+ def size
+ middlewares.size
+ end
+
+ def last
+ middlewares.last
+ end
+
+ def [](i)
+ middlewares[i]
+ end
+
+ def initialize_copy(other)
+ self.middlewares = other.middlewares.dup
end
def insert(index, *args, &block)
index = assert_index(index, :before)
middleware = self.class::Middleware.new(*args, &block)
- super(index, middleware)
+ middlewares.insert(index, middleware)
end
alias_method :insert_before, :insert
@@ -63,26 +97,25 @@ module ActionDispatch
delete(target)
end
- def use(*args, &block)
- middleware = self.class::Middleware.new(*args, &block)
- push(middleware)
+ def delete(target)
+ middlewares.delete target
end
- def active
- ActiveSupport::Deprecation.warn "All middlewares in the chain are active since the laziness " <<
- "was removed from the middleware stack", caller
+ def use(*args, &block)
+ middleware = self.class::Middleware.new(*args, &block)
+ middlewares.push(middleware)
end
def build(app = nil, &block)
app ||= block
raise "MiddlewareStack#build requires an app" unless app
- reverse.inject(app) { |a, e| e.build(a) }
+ middlewares.reverse.inject(app) { |a, e| e.build(a) }
end
protected
def assert_index(index, where)
- i = index.is_a?(Integer) ? index : self.index(index)
+ i = index.is_a?(Integer) ? index : middlewares.index(index)
raise "No such middleware to insert #{where}: #{index.inspect}" unless i
i
end
diff --git a/actionpack/lib/action_dispatch/middleware/static.rb b/actionpack/lib/action_dispatch/middleware/static.rb
index d7e88a54e4..c57f694c4d 100644
--- a/actionpack/lib/action_dispatch/middleware/static.rb
+++ b/actionpack/lib/action_dispatch/middleware/static.rb
@@ -1,12 +1,47 @@
require 'rack/utils'
module ActionDispatch
+ class FileHandler
+ def initialize(at, root)
+ @at, @root = at.chomp('/'), root.chomp('/')
+ @compiled_at = @at.blank? ? nil : /^#{Regexp.escape(at)}/
+ @compiled_root = /^#{Regexp.escape(root)}/
+ @file_server = ::Rack::File.new(@root)
+ end
+
+ def match?(path)
+ path = path.dup
+ if !@compiled_at || path.sub!(@compiled_at, '')
+ full_path = path.empty? ? @root : File.join(@root, ::Rack::Utils.unescape(path))
+ paths = "#{full_path}#{ext}"
+
+ matches = Dir[paths]
+ match = matches.detect { |m| File.file?(m) }
+ if match
+ match.sub!(@compiled_root, '')
+ match
+ end
+ end
+ end
+
+ def call(env)
+ @file_server.call(env)
+ end
+
+ def ext
+ @ext ||= begin
+ ext = ::ActionController::Base.page_cache_extension
+ "{,#{ext},/index#{ext}}"
+ end
+ end
+ end
+
class Static
FILE_METHODS = %w(GET HEAD).freeze
- def initialize(app, root)
+ def initialize(app, roots)
@app = app
- @file_server = ::Rack::File.new(root)
+ @file_handlers = create_file_handlers(roots)
end
def call(env)
@@ -14,15 +49,10 @@ module ActionDispatch
method = env['REQUEST_METHOD']
if FILE_METHODS.include?(method)
- if file_exist?(path)
- return @file_server.call(env)
- else
- cached_path = directory_exist?(path) ? "#{path}/index" : path
- cached_path += ::ActionController::Base.page_cache_extension
-
- if file_exist?(cached_path)
- env['PATH_INFO'] = cached_path
- return @file_server.call(env)
+ @file_handlers.each do |file_handler|
+ if match = file_handler.match?(path)
+ env["PATH_INFO"] = match
+ return file_handler.call(env)
end
end
end
@@ -31,14 +61,12 @@ module ActionDispatch
end
private
- def file_exist?(path)
- full_path = File.join(@file_server.root, ::Rack::Utils.unescape(path))
- File.file?(full_path) && File.readable?(full_path)
- end
+ def create_file_handlers(roots)
+ roots = { '' => roots } unless roots.is_a?(Hash)
- def directory_exist?(path)
- full_path = File.join(@file_server.root, ::Rack::Utils.unescape(path))
- File.directory?(full_path) && File.readable?(full_path)
+ roots.map do |at, root|
+ FileHandler.new(at, root) if File.exist?(root)
+ end.compact
end
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb
index e963b04524..97f7cf0bbe 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb
@@ -14,7 +14,7 @@
def debug_hash(hash)
hash.sort_by { |k, v| k.to_s }.map { |k, v| "#{k}: #{v.inspect rescue $!.message}" }.join("\n")
- end
+ end unless self.class.method_defined?(:debug_hash)
%>
<h2 style="margin-top: 30px">Request</h2>
@@ -28,4 +28,4 @@
<h2 style="margin-top: 30px">Response</h2>
-<p><b>Headers</b>: <pre><%=h @response ? @response.headers.inspect.gsub(',', ",\n") : 'None' %></pre></p>
+<p><b>Headers</b>: <pre><%=h defined?(@response) ? @response.headers.inspect.gsub(',', ",\n") : 'None' %></pre></p>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.erb
index d18b162a93..8771b5fd6d 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.erb
@@ -12,14 +12,14 @@
<div id="traces">
<% names.each do |name| %>
<%
- show = "document.getElementById('#{name.gsub /\s/, '-'}').style.display='block';"
- hide = (names - [name]).collect {|hide_name| "document.getElementById('#{hide_name.gsub /\s/, '-'}').style.display='none';"}
+ show = "document.getElementById('#{name.gsub(/\s/, '-')}').style.display='block';"
+ hide = (names - [name]).collect {|hide_name| "document.getElementById('#{hide_name.gsub(/\s/, '-')}').style.display='none';"}
%>
<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' %>;">
+ <div id="<%= name.gsub(/\s/, '-') %>" style="display: <%= (name == "Application Trace") ? 'block' : 'none' %>;">
<pre><code><%=h trace.join "\n" %></code></pre>
</div>
<% end %>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.erb
index bd6ffbab5d..50d8ca9484 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.erb
@@ -6,5 +6,5 @@
</h1>
<pre><%=h @exception.message %></pre>
-<%= render :file => "rescues/_trace.erb" %>
-<%= render :file => "rescues/_request_and_response.erb" %>
+<%= render :template => "rescues/_trace" %>
+<%= render :template => "rescues/_request_and_response" %>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.erb
index 02fa18211d..c658559be9 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.erb
@@ -13,9 +13,5 @@
<p><%=h @exception.sub_template_message %></p>
-<% @real_exception = @exception
- @exception = @exception.original_exception || @exception %>
-<%= render :file => "rescues/_trace.erb" %>
-<% @exception = @real_exception %>
-
-<%= render :file => "rescues/_request_and_response.erb" %>
+<%= render :template => "rescues/_trace" %>
+<%= render :template => "rescues/_request_and_response" %>
diff --git a/actionpack/lib/action_dispatch/railtie.rb b/actionpack/lib/action_dispatch/railtie.rb
index a3af37947a..0a3bd5fe40 100644
--- a/actionpack/lib/action_dispatch/railtie.rb
+++ b/actionpack/lib/action_dispatch/railtie.rb
@@ -8,10 +8,11 @@ module ActionDispatch
config.action_dispatch.ip_spoofing_check = true
config.action_dispatch.show_exceptions = true
config.action_dispatch.best_standards_support = true
+ config.action_dispatch.tld_length = 1
+ config.action_dispatch.rack_cache = {:metastore => "rails:/", :entitystore => "rails:/", :verbose => true}
- # Prepare dispatcher callbacks and run 'prepare' callbacks
- initializer "action_dispatch.prepare_dispatcher" do |app|
- ActionDispatch::Callbacks.to_prepare { app.routes_reloader.execute_if_updated }
+ initializer "action_dispatch.configure" do |app|
+ ActionDispatch::Http::URL.tld_length = app.config.action_dispatch.tld_length
end
end
end
diff --git a/actionpack/lib/action_dispatch/routing.rb b/actionpack/lib/action_dispatch/routing.rb
index 683dd72555..43fd93adf6 100644
--- a/actionpack/lib/action_dispatch/routing.rb
+++ b/actionpack/lib/action_dispatch/routing.rb
@@ -2,31 +2,11 @@ require 'active_support/core_ext/object/to_param'
require 'active_support/core_ext/regexp'
module ActionDispatch
- # = Routing
- #
# The routing module provides URL rewriting in native Ruby. It's a way to
# redirect incoming requests to controllers and actions. This replaces
- # mod_rewrite rules. Best of all, Rails' Routing works with any web server.
+ # mod_rewrite rules. Best of all, Rails' \Routing works with any web server.
# Routes are defined in <tt>config/routes.rb</tt>.
#
- # Consider the following route, which you will find commented out at the
- # bottom of your generated <tt>config/routes.rb</tt>:
- #
- # match ':controller(/:action(/:id(.:format)))'
- #
- # This route states that it expects requests to consist of a
- # <tt>:controller</tt> followed optionally by an <tt>:action</tt> that in
- # turn is followed optionally by an <tt>:id</tt>, which in turn is followed
- # optionally by a <tt>:format</tt>
- #
- # Suppose you get an incoming request for <tt>/blog/edit/22</tt>, you'll end
- # up with:
- #
- # params = { :controller => 'blog',
- # :action => 'edit',
- # :id => '22'
- # }
- #
# Think of creating routes as drawing a map for your requests. The map tells
# them where to go based on some predefined pattern:
#
@@ -43,6 +23,51 @@ module ActionDispatch
#
# Other names simply map to a parameter as in the case of <tt>:id</tt>.
#
+ # == Resources
+ #
+ # Resource routing allows you to quickly declare all of the common routes
+ # for a given resourceful controller. Instead of declaring separate routes
+ # for your +index+, +show+, +new+, +edit+, +create+, +update+ and +destroy+
+ # actions, a resourceful route declares them in a single line of code:
+ #
+ # resources :photos
+ #
+ # Sometimes, you have a resource that clients always look up without
+ # referencing an ID. A common example, /profile always shows the profile of
+ # the currently logged in user. In this case, you can use a singular resource
+ # to map /profile (rather than /profile/:id) to the show action.
+ #
+ # resource :profile
+ #
+ # It's common to have resources that are logically children of other
+ # resources:
+ #
+ # resources :magazines do
+ # resources :ads
+ # end
+ #
+ # You may wish to organize groups of controllers under a namespace. Most
+ # commonly, you might group a number of administrative controllers under
+ # an +admin+ namespace. You would place these controllers under the
+ # app/controllers/admin directory, and you can group them together in your
+ # router:
+ #
+ # namespace "admin" do
+ # resources :posts, :comments
+ # end
+ #
+ # Alternately, you can add prefixes to your path without using a separate
+ # directory by using +scope+. +scope+ takes additional options which
+ # apply to all enclosed routes.
+ #
+ # scope :path => "/cpanel", :as => 'admin' do
+ # resources :posts, :comments
+ # end
+ #
+ # For more, see <tt>Routing::Mapper::Resources#resources</tt>,
+ # <tt>Routing::Mapper::Scoping#namespace</tt>, and
+ # <tt>Routing::Mapper::Scoping#scope</tt>.
+ #
# == Named routes
#
# Routes can be named by passing an <tt>:as</tt> option,
@@ -131,11 +156,35 @@ module ActionDispatch
# Encoding regular expression modifiers are silently ignored. The
# match will always use the default encoding or ASCII.
#
+ # == Default route
+ #
+ # Consider the following route, which you will find commented out at the
+ # bottom of your generated <tt>config/routes.rb</tt>:
+ #
+ # match ':controller(/:action(/:id(.:format)))'
+ #
+ # This route states that it expects requests to consist of a
+ # <tt>:controller</tt> followed optionally by an <tt>:action</tt> that in
+ # turn is followed optionally by an <tt>:id</tt>, which in turn is followed
+ # optionally by a <tt>:format</tt>.
+ #
+ # Suppose you get an incoming request for <tt>/blog/edit/22</tt>, you'll end
+ # up with:
+ #
+ # params = { :controller => 'blog',
+ # :action => 'edit',
+ # :id => '22'
+ # }
+ #
+ # By not relying on default routes, you improve the security of your
+ # application since not all controller actions, which includes actions you
+ # might add at a later time, are exposed by default.
+ #
# == HTTP Methods
#
# Using the <tt>:via</tt> option when specifying a route allows you to restrict it to a specific HTTP method.
- # Possible values are <tt>:post</tt>, <tt>:get</tt>, <tt>:put</tt>, <tt>:delete</tt> and <tt>:any</tt>.
- # If your route needs to respond to more than one method you can use an array, e.g. <tt>[ :get, :post ]</tt>.
+ # Possible values are <tt>:post</tt>, <tt>:get</tt>, <tt>:put</tt>, <tt>:delete</tt> and <tt>:any</tt>.
+ # If your route needs to respond to more than one method you can use an array, e.g. <tt>[ :get, :post ]</tt>.
# The default value is <tt>:any</tt> which means that the route will respond to any of the HTTP methods.
#
# Examples:
@@ -144,7 +193,7 @@ module ActionDispatch
# match 'post/:id' => "posts#create_comment', :via => :post
#
# Now, if you POST to <tt>/posts/:id</tt>, it will route to the <tt>create_comment</tt> action. A GET on the same
- # URL will route to the <tt>show</tt> action.
+ # URL will route to the <tt>show</tt> action.
#
# === HTTP helper methods
#
@@ -160,6 +209,20 @@ module ActionDispatch
# however if your route needs to respond to more than one HTTP method (or all methods) then using the
# <tt>:via</tt> option on <tt>match</tt> is preferable.
#
+ # == External redirects
+ #
+ # You can redirect any path to another path using the redirect helper in your router:
+ #
+ # match "/stories" => redirect("/posts")
+ #
+ # == Routing to Rack Applications
+ #
+ # Instead of a String, like <tt>posts#index</tt>, which corresponds to the
+ # index action in the PostsController, you can specify any Rack application
+ # as the endpoint for a matcher:
+ #
+ # match "/application.js" => Sprockets
+ #
# == Reloading routes
#
# You can reload routes if you feel you must:
@@ -208,13 +271,15 @@ module ActionDispatch
#
# == View a list of all your routes
#
- # Run <tt>rake routes</tt>.
+ # rake routes
+ #
+ # Target specific controllers by prefixing the command with <tt>CONTROLLER=x</tt>.
#
module Routing
- autoload :DeprecatedMapper, 'action_dispatch/routing/deprecated_mapper'
autoload :Mapper, 'action_dispatch/routing/mapper'
autoload :Route, 'action_dispatch/routing/route'
autoload :RouteSet, 'action_dispatch/routing/route_set'
+ autoload :RoutesProxy, 'action_dispatch/routing/routes_proxy'
autoload :UrlFor, 'action_dispatch/routing/url_for'
autoload :PolymorphicRoutes, 'action_dispatch/routing/polymorphic_routes'
diff --git a/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb b/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb
deleted file mode 100644
index e04062ce8b..0000000000
--- a/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb
+++ /dev/null
@@ -1,525 +0,0 @@
-require 'active_support/core_ext/object/blank'
-require 'active_support/core_ext/object/with_options'
-require 'active_support/core_ext/object/try'
-
-module ActionDispatch
- module Routing
- class RouteSet
- attr_accessor :controller_namespaces
-
- CONTROLLER_REGEXP = /[_a-zA-Z0-9]+/
-
- def controller_constraints
- @controller_constraints ||= begin
- namespaces = controller_namespaces + in_memory_controller_namespaces
- source = namespaces.map { |ns| "#{Regexp.escape(ns)}/#{CONTROLLER_REGEXP.source}" }
- source << CONTROLLER_REGEXP.source
- Regexp.compile(source.sort.reverse.join('|'))
- end
- end
-
- def in_memory_controller_namespaces
- namespaces = Set.new
- ActionController::Base.descendants.each do |klass|
- next if klass.anonymous?
- namespaces << klass.name.underscore.split('/')[0...-1].join('/')
- end
- namespaces.delete('')
- namespaces
- end
- end
-
- class DeprecatedMapper #:nodoc:
- def initialize(set) #:nodoc:
- ActiveSupport::Deprecation.warn "You are using the old router DSL which will be removed in Rails 3.1. " <<
- "Please check how to update your routes file at: http://www.engineyard.com/blog/2010/the-lowdown-on-routes-in-rails-3/"
- @set = set
- end
-
- def connect(path, options = {})
- options = options.dup
-
- if conditions = options.delete(:conditions)
- conditions = conditions.dup
- method = [conditions.delete(:method)].flatten.compact
- method.map! { |m|
- m = m.to_s.upcase
-
- if m == "HEAD"
- raise ArgumentError, "HTTP method HEAD is invalid in route conditions. Rails processes HEAD requests the same as GETs, returning just the response headers"
- end
-
- unless HTTP_METHODS.include?(m.downcase.to_sym)
- raise ArgumentError, "Invalid HTTP method specified in route conditions"
- end
-
- m
- }
-
- if method.length > 1
- method = Regexp.union(*method)
- elsif method.length == 1
- method = method.first
- else
- method = nil
- end
- end
-
- path_prefix = options.delete(:path_prefix)
- name_prefix = options.delete(:name_prefix)
- namespace = options.delete(:namespace)
-
- name = options.delete(:_name)
- name = "#{name_prefix}#{name}" if name_prefix
-
- requirements = options.delete(:requirements) || {}
- defaults = options.delete(:defaults) || {}
- options.each do |k, v|
- if v.is_a?(Regexp)
- if value = options.delete(k)
- requirements[k.to_sym] = value
- end
- else
- value = options.delete(k)
- defaults[k.to_sym] = value.is_a?(Symbol) ? value : value.to_param
- end
- end
-
- requirements.each do |_, requirement|
- if requirement.source =~ %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z}
- raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}"
- end
- if requirement.multiline?
- raise ArgumentError, "Regexp multiline option not allowed in routing requirements: #{requirement.inspect}"
- end
- end
-
- requirements[:controller] ||= @set.controller_constraints
-
- if defaults[:controller]
- defaults[:action] ||= 'index'
- defaults[:controller] = defaults[:controller].to_s
- defaults[:controller] = "#{namespace}#{defaults[:controller]}" if namespace
- end
-
- if defaults[:action]
- defaults[:action] = defaults[:action].to_s
- end
-
- if path.is_a?(String)
- path = "#{path_prefix}/#{path}" if path_prefix
- path = path.gsub('.:format', '(.:format)')
- path = optionalize_trailing_dynamic_segments(path, requirements, defaults)
- glob = $1.to_sym if path =~ /\/\*(\w+)$/
- path = ::Rack::Mount::Utils.normalize_path(path)
-
- if glob && !defaults[glob].blank?
- raise ActionController::RoutingError, "paths cannot have non-empty default values"
- end
- end
-
- app = Routing::RouteSet::Dispatcher.new(:defaults => defaults, :glob => glob)
-
- conditions = {}
- conditions[:request_method] = method if method
- conditions[:path_info] = path if path
-
- @set.add_route(app, conditions, requirements, defaults, name)
- end
-
- def optionalize_trailing_dynamic_segments(path, requirements, defaults) #:nodoc:
- path = (path =~ /^\//) ? path.dup : "/#{path}"
- optional, segments = true, []
-
- required_segments = requirements.keys
- required_segments -= defaults.keys.compact
-
- old_segments = path.split('/')
- old_segments.shift
- length = old_segments.length
-
- old_segments.reverse.each_with_index do |segment, index|
- required_segments.each do |required|
- if segment =~ /#{required}/
- optional = false
- break
- end
- end
-
- if optional
- if segment == ":id" && segments.include?(":action")
- optional = false
- elsif segment == ":controller" || segment == ":action" || segment == ":id"
- # Ignore
- elsif !(segment =~ /^:\w+$/) &&
- !(segment =~ /^:\w+\(\.:format\)$/)
- optional = false
- elsif segment =~ /^:(\w+)$/
- if defaults.has_key?($1.to_sym)
- defaults.delete($1.to_sym) if defaults[$1.to_sym].nil?
- else
- optional = false
- end
- end
- end
-
- if optional && index < length - 1
- segments.unshift('(/', segment)
- segments.push(')')
- elsif optional
- segments.unshift('/(', segment)
- segments.push(')')
- else
- segments.unshift('/', segment)
- end
- end
-
- segments.join
- end
- private :optionalize_trailing_dynamic_segments
-
- # Creates a named route called "root" for matching the root level request.
- def root(options = {})
- if options.is_a?(Symbol)
- if source_route = @set.named_routes.routes[options]
- options = source_route.defaults.merge({ :conditions => source_route.conditions })
- end
- end
- named_route("root", '', options)
- end
-
- def named_route(name, path, options = {}) #:nodoc:
- options[:_name] = name
- connect(path, options)
- end
-
- def namespace(name, options = {}, &block)
- if options[:namespace]
- with_options({:path_prefix => "#{options.delete(:path_prefix)}/#{name}", :name_prefix => "#{options.delete(:name_prefix)}#{name}_", :namespace => "#{options.delete(:namespace)}#{name}/" }.merge(options), &block)
- else
- with_options({:path_prefix => name, :name_prefix => "#{name}_", :namespace => "#{name}/" }.merge(options), &block)
- end
- end
-
- def method_missing(route_name, *args, &proc) #:nodoc:
- super unless args.length >= 1 && proc.nil?
- named_route(route_name, *args)
- end
-
- INHERITABLE_OPTIONS = :namespace, :shallow
-
- class Resource #:nodoc:
- DEFAULT_ACTIONS = :index, :create, :new, :edit, :show, :update, :destroy
-
- attr_reader :collection_methods, :member_methods, :new_methods
- attr_reader :path_prefix, :name_prefix, :path_segment
- attr_reader :plural, :singular
- attr_reader :options, :defaults
-
- def initialize(entities, options, defaults)
- @plural ||= entities
- @singular ||= options[:singular] || plural.to_s.singularize
- @path_segment = options.delete(:as) || @plural
-
- @options = options
- @defaults = defaults
-
- arrange_actions
- add_default_actions
- set_allowed_actions
- set_prefixes
- end
-
- def controller
- @controller ||= "#{options[:namespace]}#{(options[:controller] || plural).to_s}"
- end
-
- def requirements(with_id = false)
- @requirements ||= @options[:requirements] || {}
- @id_requirement ||= { :id => @requirements.delete(:id) || /[^#{Routing::SEPARATORS.join}]+/ }
-
- with_id ? @requirements.merge(@id_requirement) : @requirements
- end
-
- def conditions
- @conditions ||= @options[:conditions] || {}
- end
-
- def path
- @path ||= "#{path_prefix}/#{path_segment}"
- end
-
- def new_path
- new_action = self.options[:path_names][:new] if self.options[:path_names]
- new_action ||= self.defaults[:path_names][:new]
- @new_path ||= "#{path}/#{new_action}"
- end
-
- def shallow_path_prefix
- @shallow_path_prefix ||= @options[:shallow] ? @options[:namespace].try(:sub, /\/$/, '') : path_prefix
- end
-
- def member_path
- @member_path ||= "#{shallow_path_prefix}/#{path_segment}/:id"
- end
-
- def nesting_path_prefix
- @nesting_path_prefix ||= "#{shallow_path_prefix}/#{path_segment}/:#{singular}_id"
- end
-
- def shallow_name_prefix
- @shallow_name_prefix ||= @options[:shallow] ? @options[:namespace].try(:gsub, /\//, '_') : name_prefix
- end
-
- def nesting_name_prefix
- "#{shallow_name_prefix}#{singular}_"
- end
-
- def action_separator
- @action_separator ||= ActionController::Base.resource_action_separator
- end
-
- def uncountable?
- @singular.to_s == @plural.to_s
- end
-
- def has_action?(action)
- !DEFAULT_ACTIONS.include?(action) || action_allowed?(action)
- end
-
- protected
- def arrange_actions
- @collection_methods = arrange_actions_by_methods(options.delete(:collection))
- @member_methods = arrange_actions_by_methods(options.delete(:member))
- @new_methods = arrange_actions_by_methods(options.delete(:new))
- end
-
- def add_default_actions
- add_default_action(member_methods, :get, :edit)
- add_default_action(new_methods, :get, :new)
- end
-
- def set_allowed_actions
- only, except = @options.values_at(:only, :except)
- @allowed_actions ||= {}
-
- if only == :all || except == :none
- only = nil
- except = []
- elsif only == :none || except == :all
- only = []
- except = nil
- end
-
- if only
- @allowed_actions[:only] = Array(only).map {|a| a.to_sym }
- elsif except
- @allowed_actions[:except] = Array(except).map {|a| a.to_sym }
- end
- end
-
- def action_allowed?(action)
- only, except = @allowed_actions.values_at(:only, :except)
- (!only || only.include?(action)) && (!except || !except.include?(action))
- end
-
- def set_prefixes
- @path_prefix = options.delete(:path_prefix)
- @name_prefix = options.delete(:name_prefix)
- end
-
- def arrange_actions_by_methods(actions)
- (actions || {}).inject({}) do |flipped_hash, (key, value)|
- (flipped_hash[value] ||= []) << key
- flipped_hash
- end
- end
-
- def add_default_action(collection, method, action)
- (collection[method] ||= []).unshift(action)
- end
- end
-
- class SingletonResource < Resource #:nodoc:
- def initialize(entity, options, defaults)
- @singular = @plural = entity
- options[:controller] ||= @singular.to_s.pluralize
- super
- end
-
- alias_method :shallow_path_prefix, :path_prefix
- alias_method :shallow_name_prefix, :name_prefix
- alias_method :member_path, :path
- alias_method :nesting_path_prefix, :path
- end
-
- def resources(*entities, &block)
- options = entities.extract_options!
- entities.each { |entity| map_resource(entity, options.dup, &block) }
- end
-
- def resource(*entities, &block)
- options = entities.extract_options!
- entities.each { |entity| map_singleton_resource(entity, options.dup, &block) }
- end
-
- private
- def map_resource(entities, options = {}, &block)
- resource = Resource.new(entities, options, :path_names => @set.resources_path_names)
-
- with_options :controller => resource.controller do |map|
- map_associations(resource, options)
-
- if block_given?
- with_options(options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => resource.nesting_path_prefix, :name_prefix => resource.nesting_name_prefix), &block)
- end
-
- map_collection_actions(map, resource)
- map_default_collection_actions(map, resource)
- map_new_actions(map, resource)
- map_member_actions(map, resource)
- end
- end
-
- def map_singleton_resource(entities, options = {}, &block)
- resource = SingletonResource.new(entities, options, :path_names => @set.resources_path_names)
-
- with_options :controller => resource.controller do |map|
- map_associations(resource, options)
-
- if block_given?
- with_options(options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => resource.nesting_path_prefix, :name_prefix => resource.nesting_name_prefix), &block)
- end
-
- map_collection_actions(map, resource)
- map_new_actions(map, resource)
- map_member_actions(map, resource)
- map_default_singleton_actions(map, resource)
- end
- end
-
- def map_associations(resource, options)
- map_has_many_associations(resource, options.delete(:has_many), options) if options[:has_many]
-
- path_prefix = "#{options.delete(:path_prefix)}#{resource.nesting_path_prefix}"
- name_prefix = "#{options.delete(:name_prefix)}#{resource.nesting_name_prefix}"
-
- Array(options[:has_one]).each do |association|
- resource(association, options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => path_prefix, :name_prefix => name_prefix))
- end
- end
-
- def map_has_many_associations(resource, associations, options)
- case associations
- when Hash
- associations.each do |association,has_many|
- map_has_many_associations(resource, association, options.merge(:has_many => has_many))
- end
- when Array
- associations.each do |association|
- map_has_many_associations(resource, association, options)
- end
- when Symbol, String
- resources(associations, options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => resource.nesting_path_prefix, :name_prefix => resource.nesting_name_prefix, :has_many => options[:has_many]))
- else
- end
- end
-
- def map_collection_actions(map, resource)
- resource.collection_methods.each do |method, actions|
- actions.each do |action|
- [method].flatten.each do |m|
- action_path = resource.options[:path_names][action] if resource.options[:path_names].is_a?(Hash)
- action_path ||= action
-
- map_resource_routes(map, resource, action, "#{resource.path}#{resource.action_separator}#{action_path}", "#{action}_#{resource.name_prefix}#{resource.plural}", m)
- end
- end
- end
- end
-
- def map_default_collection_actions(map, resource)
- index_route_name = "#{resource.name_prefix}#{resource.plural}"
-
- if resource.uncountable?
- index_route_name << "_index"
- end
-
- map_resource_routes(map, resource, :index, resource.path, index_route_name)
- map_resource_routes(map, resource, :create, resource.path, index_route_name)
- end
-
- def map_default_singleton_actions(map, resource)
- map_resource_routes(map, resource, :create, resource.path, "#{resource.shallow_name_prefix}#{resource.singular}")
- end
-
- def map_new_actions(map, resource)
- resource.new_methods.each do |method, actions|
- actions.each do |action|
- route_path = resource.new_path
- route_name = "new_#{resource.name_prefix}#{resource.singular}"
-
- unless action == :new
- route_path = "#{route_path}#{resource.action_separator}#{action}"
- route_name = "#{action}_#{route_name}"
- end
-
- map_resource_routes(map, resource, action, route_path, route_name, method)
- end
- end
- end
-
- def map_member_actions(map, resource)
- resource.member_methods.each do |method, actions|
- actions.each do |action|
- [method].flatten.each do |m|
- action_path = resource.options[:path_names][action] if resource.options[:path_names].is_a?(Hash)
- action_path ||= @set.resources_path_names[action] || action
-
- map_resource_routes(map, resource, action, "#{resource.member_path}#{resource.action_separator}#{action_path}", "#{action}_#{resource.shallow_name_prefix}#{resource.singular}", m, { :force_id => true })
- end
- end
- end
-
- route_path = "#{resource.shallow_name_prefix}#{resource.singular}"
- map_resource_routes(map, resource, :show, resource.member_path, route_path)
- map_resource_routes(map, resource, :update, resource.member_path, route_path)
- map_resource_routes(map, resource, :destroy, resource.member_path, route_path)
- end
-
- def map_resource_routes(map, resource, action, route_path, route_name = nil, method = nil, resource_options = {} )
- if resource.has_action?(action)
- action_options = action_options_for(action, resource, method, resource_options)
- formatted_route_path = "#{route_path}.:format"
-
- if route_name && @set.named_routes[route_name.to_sym].nil?
- map.named_route(route_name, formatted_route_path, action_options)
- else
- map.connect(formatted_route_path, action_options)
- end
- end
- end
-
- def add_conditions_for(conditions, method)
- {:conditions => conditions.dup}.tap do |options|
- options[:conditions][:method] = method unless method == :any
- end
- end
-
- def action_options_for(action, resource, method = nil, resource_options = {})
- default_options = { :action => action.to_s }
- require_id = !resource.kind_of?(SingletonResource)
- force_id = resource_options[:force_id] && !resource.kind_of?(SingletonResource)
-
- case default_options[:action]
- when "index", "new"; default_options.merge(add_conditions_for(resource.conditions, method || :get)).merge(resource.requirements)
- when "create"; default_options.merge(add_conditions_for(resource.conditions, method || :post)).merge(resource.requirements)
- when "show", "edit"; default_options.merge(add_conditions_for(resource.conditions, method || :get)).merge(resource.requirements(require_id))
- when "update"; default_options.merge(add_conditions_for(resource.conditions, method || :put)).merge(resource.requirements(require_id))
- when "destroy"; default_options.merge(add_conditions_for(resource.conditions, method || :delete)).merge(resource.requirements(require_id))
- else default_options.merge(add_conditions_for(resource.conditions, method)).merge(resource.requirements(force_id))
- end
- end
- end
- end
-end
diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb
index c118c72440..14c424f24b 100644
--- a/actionpack/lib/action_dispatch/routing/mapper.rb
+++ b/actionpack/lib/action_dispatch/routing/mapper.rb
@@ -1,6 +1,8 @@
require 'erb'
require 'active_support/core_ext/hash/except'
require 'active_support/core_ext/object/blank'
+require 'active_support/inflector'
+require 'action_dispatch/routing/redirection'
module ActionDispatch
module Routing
@@ -20,27 +22,37 @@ module ActionDispatch
@app, @constraints, @request = app, constraints, request
end
- def call(env)
+ def matches?(env)
req = @request.new(env)
@constraints.each { |constraint|
if constraint.respond_to?(:matches?) && !constraint.matches?(req)
- return [ 404, {'X-Cascade' => 'pass'}, [] ]
- elsif constraint.respond_to?(:call) && !constraint.call(req)
- return [ 404, {'X-Cascade' => 'pass'}, [] ]
+ return false
+ elsif constraint.respond_to?(:call) && !constraint.call(*constraint_args(constraint, req))
+ return false
end
}
- @app.call(env)
+ return true
+ end
+
+ def call(env)
+ matches?(env) ? @app.call(env) : [ 404, {'X-Cascade' => 'pass'}, [] ]
end
+
+ private
+ def constraint_args(constraint, request)
+ constraint.arity == 1 ? [request] : [request.symbolized_path_parameters, request]
+ end
end
class Mapping #:nodoc:
IGNORE_OPTIONS = [:to, :as, :via, :on, :constraints, :defaults, :only, :except, :anchor, :shallow, :shallow_path, :shallow_prefix]
- def initialize(set, scope, args)
- @set, @scope = set, scope
- @path, @options = extract_path_and_options(args)
+ def initialize(set, scope, path, options)
+ @set, @scope = set, scope
+ @options = (@scope[:options] || {}).merge(options)
+ @path = normalize_path(path)
normalize_options!
end
@@ -49,28 +61,6 @@ module ActionDispatch
end
private
- def extract_path_and_options(args)
- options = args.extract_options!
-
- if using_to_shorthand?(args, options)
- path, to = options.find { |name, value| name.is_a?(String) }
- options.merge!(:to => to).delete(path) if path
- else
- path = args.first
- end
-
- if path.match(':controller')
- raise ArgumentError, ":controller segment is not allowed within a namespace block" if @scope[:module]
-
- # Add a default constraint for :controller path segments that matches namespaced
- # controllers with default routes like :controller/:action/:id(.:format), e.g:
- # GET /admin/products/show/1
- # => { :controller => 'admin/products', :action => 'show', :id => '1' }
- options.reverse_merge!(:controller => /.+?/)
- end
-
- [ normalize_path(path), options ]
- end
def normalize_options!
path_without_format = @path.sub(/\(\.:format\)$/, '')
@@ -78,15 +68,21 @@ module ActionDispatch
if using_match_shorthand?(path_without_format, @options)
to_shorthand = @options[:to].blank?
@options[:to] ||= path_without_format[1..-1].sub(%r{/([^/]*)$}, '#\1')
- @options[:as] ||= path_without_format[1..-1].gsub("/", "_")
end
@options.merge!(default_controller_and_action(to_shorthand))
- end
- # match "account" => "account#index"
- def using_to_shorthand?(args, options)
- args.empty? && options.present?
+ requirements.each do |name, requirement|
+ # segment_keys.include?(k.to_s) || k == :controller
+ next unless Regexp === requirement && !constraints[name]
+
+ if requirement.source =~ %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z}
+ raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}"
+ end
+ if requirement.multiline?
+ raise ArgumentError, "Regexp multiline option not allowed in routing requirements: #{requirement.inspect}"
+ end
+ end
end
# match "account/overview"
@@ -95,8 +91,27 @@ module ActionDispatch
end
def normalize_path(path)
- raise ArgumentError, "path is required" if @scope[:path].blank? && path.blank?
- Mapper.normalize_path("#{@scope[:path]}/#{path}")
+ raise ArgumentError, "path is required" if path.blank?
+ path = Mapper.normalize_path(path)
+
+ if path.match(':controller')
+ raise ArgumentError, ":controller segment is not allowed within a namespace block" if @scope[:module]
+
+ # Add a default constraint for :controller path segments that matches namespaced
+ # controllers with default routes like :controller/:action/:id(.:format), e.g:
+ # GET /admin/products/show/1
+ # => { :controller => 'admin/products', :action => 'show', :id => '1' }
+ @options.reverse_merge!(:controller => /.+?/)
+ end
+
+ if @options[:format] == false
+ @options.delete(:format)
+ path
+ elsif path.include?(":format") || path.end_with?('/') || path.match(/^\/?\*/)
+ path
+ else
+ "#{path}(.:format)"
+ end
end
def app
@@ -142,6 +157,10 @@ module ActionDispatch
controller = [@scope[:module], controller].compact.join("/").presence
end
+ if controller.is_a?(String) && controller =~ %r{\A/}
+ raise ArgumentError, "controller name should not start with a slash"
+ end
+
controller = controller.to_s unless controller.is_a?(Regexp)
action = action.to_s unless action.is_a?(Regexp)
@@ -153,21 +172,21 @@ module ActionDispatch
raise ArgumentError, "missing :action"
end
- { :controller => controller, :action => action }.tap do |hash|
- hash.delete(:controller) if hash[:controller].blank?
- hash.delete(:action) if hash[:action].blank?
- end
+ hash = {}
+ hash[:controller] = controller unless controller.blank?
+ hash[:action] = action unless action.blank?
+ hash
end
end
def blocks
+ block = @scope[:blocks] || []
+
if @options[:constraints].present? && !@options[:constraints].is_a?(Hash)
- block = @options[:constraints]
- else
- block = nil
+ block << @options[:constraints]
end
- ((@scope[:blocks] || []) + [ block ]).compact
+ block
end
def constraints
@@ -176,8 +195,8 @@ module ActionDispatch
def request_method_condition
if via = @options[:via]
- via = Array(via).map { |m| m.to_s.upcase }
- { :request_method => Regexp.union(*via) }
+ list = Array(via).map { |m| m.to_s.dasherize.upcase }
+ { :request_method => list }
else
{ }
end
@@ -219,21 +238,165 @@ module ActionDispatch
path
end
- module Base
- def initialize(set) #:nodoc:
- @set = set
- end
+ def self.normalize_name(name)
+ normalize_path(name)[1..-1].gsub("/", "_")
+ end
+ module Base
+ # You can specify what Rails should route "/" to with the root method:
+ #
+ # root :to => 'pages#main'
+ #
+ # For options, see +match+, as +root+ uses it internally.
+ #
+ # You should put the root route at the top of <tt>config/routes.rb</tt>,
+ # because this means it will be matched first. As this is the most popular route
+ # of most Rails applications, this is beneficial.
def root(options = {})
match '/', options.reverse_merge(:as => :root)
end
- def match(*args)
- mapping = Mapping.new(@set, @scope, args).to_route
- @set.add_route(*mapping)
+ # Matches a url pattern to one or more routes. Any symbols in a pattern
+ # are interpreted as url query parameters and thus available as +params+
+ # in an action:
+ #
+ # # sets :controller, :action and :id in params
+ # match ':controller/:action/:id'
+ #
+ # Two of these symbols are special, +:controller+ maps to the controller
+ # and +:action+ to the controller's action. A pattern can also map
+ # wildcard segments (globs) to params:
+ #
+ # match 'songs/*category/:title' => 'songs#show'
+ #
+ # # 'songs/rock/classic/stairway-to-heaven' sets
+ # # params[:category] = 'rock/classic'
+ # # params[:title] = 'stairway-to-heaven'
+ #
+ # When a pattern points to an internal route, the route's +:action+ and
+ # +:controller+ should be set in options or hash shorthand. Examples:
+ #
+ # match 'photos/:id' => 'photos#show'
+ # match 'photos/:id', :to => 'photos#show'
+ # match 'photos/:id', :controller => 'photos', :action => 'show'
+ #
+ # A pattern can also point to a +Rack+ endpoint i.e. anything that
+ # responds to +call+:
+ #
+ # match 'photos/:id' => lambda {|hash| [200, {}, "Coming soon" }
+ # match 'photos/:id' => PhotoRackApp
+ # # Yes, controller actions are just rack endpoints
+ # match 'photos/:id' => PhotosController.action(:show)
+ #
+ # === Options
+ #
+ # Any options not seen here are passed on as params with the url.
+ #
+ # [:controller]
+ # The route's controller.
+ #
+ # [:action]
+ # The route's action.
+ #
+ # [:path]
+ # The path prefix for the routes.
+ #
+ # [:module]
+ # The namespace for :controller.
+ #
+ # match 'path' => 'c#a', :module => 'sekret', :controller => 'posts'
+ # #=> Sekret::PostsController
+ #
+ # See <tt>Scoping#namespace</tt> for its scope equivalent.
+ #
+ # [:as]
+ # The name used to generate routing helpers.
+ #
+ # [:via]
+ # Allowed HTTP verb(s) for route.
+ #
+ # match 'path' => 'c#a', :via => :get
+ # match 'path' => 'c#a', :via => [:get, :post]
+ #
+ # [:to]
+ # Points to a +Rack+ endpoint. Can be an object that responds to
+ # +call+ or a string representing a controller's action.
+ #
+ # match 'path', :to => 'controller#action'
+ # match 'path', :to => lambda { [200, {}, "Success!"] }
+ # match 'path', :to => RackApp
+ #
+ # [:on]
+ # Shorthand for wrapping routes in a specific RESTful context. Valid
+ # values are :member, :collection, and :new. Only use within
+ # <tt>resource(s)</tt> block. For example:
+ #
+ # resource :bar do
+ # match 'foo' => 'c#a', :on => :member, :via => [:get, :post]
+ # end
+ #
+ # Is equivalent to:
+ #
+ # resource :bar do
+ # member do
+ # match 'foo' => 'c#a', :via => [:get, :post]
+ # end
+ # end
+ #
+ # [:constraints]
+ # Constrains parameters with a hash of regular expressions or an
+ # object that responds to #matches?
+ #
+ # match 'path/:id', :constraints => { :id => /[A-Z]\d{5}/ }
+ #
+ # class Blacklist
+ # def matches?(request) request.remote_ip == '1.2.3.4' end
+ # end
+ # match 'path' => 'c#a', :constraints => Blacklist.new
+ #
+ # See <tt>Scoping#constraints</tt> for more examples with its scope
+ # equivalent.
+ #
+ # [:defaults]
+ # Sets defaults for parameters
+ #
+ # # Sets params[:format] to 'jpg' by default
+ # match 'path' => 'c#a', :defaults => { :format => 'jpg' }
+ #
+ # See <tt>Scoping#defaults</tt> for its scope equivalent.
+ #
+ # [:anchor]
+ # Boolean to anchor a #match pattern. Default is true. When set to
+ # false, the pattern matches any request prefixed with the given path.
+ #
+ # # Matches any request starting with 'path'
+ # match 'path' => 'c#a', :anchor => false
+ def match(path, options=nil)
+ mapping = Mapping.new(@set, @scope, path, options || {})
+ app, conditions, requirements, defaults, as, anchor = mapping.to_route
+ @set.add_route(app, conditions, requirements, defaults, as, anchor)
self
end
+ # Mount a Rack-based application to be used within the application.
+ #
+ # mount SomeRackApp, :at => "some_route"
+ #
+ # Alternatively:
+ #
+ # mount(SomeRackApp => "some_route")
+ #
+ # For options, see +match+, as +mount+ uses it internally.
+ #
+ # All mounted applications come with routing helpers to access them.
+ # These are named after the class specified, so for the above example
+ # the helper is either +some_rack_app_path+ or +some_rack_app_url+.
+ # To customize this helper's name, use the +:as+ option:
+ #
+ # mount(SomeRackApp => "some_route", :as => "exciting")
+ #
+ # This will generate the +exciting_path+ and +exciting_url+ helpers
+ # which can be used to navigate to this mounted app.
def mount(app, options = nil)
if options
path = options.delete(:at)
@@ -245,7 +408,11 @@ module ActionDispatch
raise "A rack application must be specified" unless path
- match(path, options.merge(:to => app, :anchor => false))
+ options[:as] ||= app_name(app)
+
+ match(path, options.merge(:to => app, :anchor => false, :format => false))
+
+ define_generate_prefix(app, options[:as])
self
end
@@ -253,55 +420,83 @@ module ActionDispatch
@set.default_url_options = options
end
alias_method :default_url_options, :default_url_options=
+
+ def with_default_scope(scope, &block)
+ scope(scope) do
+ instance_exec(&block)
+ end
+ end
+
+ private
+ def app_name(app)
+ return unless app.respond_to?(:routes)
+
+ if app.respond_to?(:railtie_name)
+ app.railtie_name
+ else
+ class_name = app.class.is_a?(Class) ? app.name : app.class.name
+ ActiveSupport::Inflector.underscore(class_name).gsub("/", "_")
+ end
+ end
+
+ def define_generate_prefix(app, name)
+ return unless app.respond_to?(:routes) && app.routes.respond_to?(:define_mounted_helper)
+
+ _route = @set.named_routes.routes[name.to_sym]
+ _routes = @set
+ app.routes.define_mounted_helper(name)
+ app.routes.class_eval do
+ define_method :_generate_prefix do |options|
+ 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)
+ end
+ end
+ end
end
module HttpHelpers
+ # Define a route that only recognizes HTTP GET.
+ # For supported arguments, see <tt>Base#match</tt>.
+ #
+ # Example:
+ #
+ # get 'bacon', :to => 'food#bacon'
def get(*args, &block)
map_method(:get, *args, &block)
end
+ # Define a route that only recognizes HTTP POST.
+ # For supported arguments, see <tt>Base#match</tt>.
+ #
+ # Example:
+ #
+ # post 'bacon', :to => 'food#bacon'
def post(*args, &block)
map_method(:post, *args, &block)
end
+ # Define a route that only recognizes HTTP PUT.
+ # For supported arguments, see <tt>Base#match</tt>.
+ #
+ # Example:
+ #
+ # put 'bacon', :to => 'food#bacon'
def put(*args, &block)
map_method(:put, *args, &block)
end
+ # Define a route that only recognizes HTTP PUT.
+ # For supported arguments, see <tt>Base#match</tt>.
+ #
+ # Example:
+ #
+ # delete 'broccoli', :to => 'food#broccoli'
def delete(*args, &block)
map_method(:delete, *args, &block)
end
- def redirect(*args, &block)
- options = args.last.is_a?(Hash) ? args.pop : {}
-
- path = args.shift || block
- path_proc = path.is_a?(Proc) ? path : proc { |params| path % params }
- status = options[:status] || 301
-
- lambda do |env|
- req = Request.new(env)
-
- params = [req.symbolized_path_parameters]
- params << req if path_proc.arity > 1
-
- uri = URI.parse(path_proc.call(*params))
- uri.scheme ||= req.scheme
- uri.host ||= req.host
- uri.port ||= req.port unless req.port == 80
-
- body = %(<html><body>You are being <a href="#{ERB::Util.h(uri.to_s)}">redirected</a>.</body></html>)
-
- headers = {
- 'Location' => uri.to_s,
- 'Content-Type' => 'text/html',
- 'Content-Length' => body.length.to_s
- }
-
- [ status, headers, [body] ]
- end
- end
-
private
def map_method(method, *args, &block)
options = args.extract_options!
@@ -312,28 +507,98 @@ module ActionDispatch
end
end
+ # You may wish to organize groups of controllers under a namespace.
+ # Most commonly, you might group a number of administrative controllers
+ # under an +admin+ namespace. You would place these controllers under
+ # the app/controllers/admin directory, and you can group them together
+ # in your router:
+ #
+ # namespace "admin" do
+ # resources :posts, :comments
+ # end
+ #
+ # This will create a number of routes for each of the posts and comments
+ # controller. For Admin::PostsController, Rails will create:
+ #
+ # GET /admin/posts
+ # GET /admin/posts/new
+ # POST /admin/posts
+ # GET /admin/posts/1
+ # GET /admin/posts/1/edit
+ # PUT /admin/posts/1
+ # DELETE /admin/posts/1
+ #
+ # If you want to route /posts (without the prefix /admin) to
+ # Admin::PostsController, you could use
+ #
+ # scope :module => "admin" do
+ # resources :posts
+ # end
+ #
+ # or, for a single case
+ #
+ # resources :posts, :module => "admin"
+ #
+ # If you want to route /admin/posts to PostsController
+ # (without the Admin:: module prefix), you could use
+ #
+ # scope "/admin" do
+ # resources :posts
+ # end
+ #
+ # or, for a single case
+ #
+ # resources :posts, :path => "/admin"
+ #
+ # In each of these cases, the named routes remain the same as if you did
+ # not use scope. In the last case, the following paths map to
+ # PostsController:
+ #
+ # GET /admin/posts
+ # GET /admin/posts/new
+ # POST /admin/posts
+ # GET /admin/posts/1
+ # GET /admin/posts/1/edit
+ # PUT /admin/posts/1
+ # DELETE /admin/posts/1
module Scoping
- def initialize(*args) #:nodoc:
- @scope = {}
- super
- end
-
+ # Scopes a set of routes to the given default options.
+ #
+ # Take the following route definition as an example:
+ #
+ # scope :path => ":account_id", :as => "account" do
+ # resources :projects
+ # end
+ #
+ # This generates helpers such as +account_projects_path+, just like +resources+ does.
+ # The difference here being that the routes generated are like /rails/projects/2,
+ # rather than /accounts/rails/projects/2.
+ #
+ # === Options
+ #
+ # Takes same options as <tt>Base#match</tt> and <tt>Resources#resources</tt>.
+ #
+ # === Examples
+ #
+ # # route /posts (without the prefix /admin) to Admin::PostsController
+ # scope :module => "admin" do
+ # resources :posts
+ # end
+ #
+ # # prefix the posts resource's requests with '/admin'
+ # scope :path => "/admin" do
+ # resources :posts
+ # end
+ #
+ # # prefix the routing helper name: sekret_posts_path instead of posts_path
+ # scope :as => "sekret" do
+ # resources :posts
+ # end
def scope(*args)
options = args.extract_options!
options = options.dup
- if name_prefix = options.delete(:name_prefix)
- options[:as] ||= name_prefix
- ActiveSupport::Deprecation.warn ":name_prefix was deprecated in the new router syntax. Use :as instead.", caller
- end
-
- case args.first
- when String
- options[:path] = args.first
- when Symbol
- options[:controller] = args.first
- end
-
+ options[:path] = args.first if args.first.is_a?(String)
recover = {}
options[:constraints] ||= {}
@@ -365,10 +630,57 @@ module ActionDispatch
@scope[:blocks] = recover[:block]
end
- def controller(controller)
- scope(controller.to_sym) { yield }
+ # Scopes routes to a specific controller
+ #
+ # Example:
+ # controller "food" do
+ # match "bacon", :action => "bacon"
+ # end
+ def controller(controller, options={})
+ options[:controller] = controller
+ scope(options) { yield }
end
+ # Scopes routes to a specific namespace. For example:
+ #
+ # namespace :admin do
+ # resources :posts
+ # end
+ #
+ # This generates the following routes:
+ #
+ # admin_posts GET /admin/posts(.:format) {:action=>"index", :controller=>"admin/posts"}
+ # admin_posts POST /admin/posts(.:format) {:action=>"create", :controller=>"admin/posts"}
+ # new_admin_post GET /admin/posts/new(.:format) {:action=>"new", :controller=>"admin/posts"}
+ # edit_admin_post GET /admin/posts/:id/edit(.:format) {:action=>"edit", :controller=>"admin/posts"}
+ # admin_post GET /admin/posts/:id(.:format) {:action=>"show", :controller=>"admin/posts"}
+ # admin_post PUT /admin/posts/:id(.:format) {:action=>"update", :controller=>"admin/posts"}
+ # admin_post DELETE /admin/posts/:id(.:format) {:action=>"destroy", :controller=>"admin/posts"}
+ #
+ # === Options
+ #
+ # The +:path+, +:as+, +:module+, +:shallow_path+ and +:shallow_prefix+
+ # options all default to the name of the namespace.
+ #
+ # For options, see <tt>Base#match</tt>. For +:shallow_path+ option, see
+ # <tt>Resources#resources</tt>.
+ #
+ # === Examples
+ #
+ # # accessible through /sekret/posts rather than /admin/posts
+ # namespace :admin, :path => "sekret" do
+ # resources :posts
+ # end
+ #
+ # # maps to Sekret::PostsController rather than Admin::PostsController
+ # namespace :admin, :module => "sekret" do
+ # resources :posts
+ # end
+ #
+ # # generates sekret_posts_path rather than admin_posts_path
+ # namespace :admin, :as => "sekret" do
+ # resources :posts
+ # end
def namespace(path, options = {})
path = path.to_s
options = { :path => path, :as => path, :module => path,
@@ -376,95 +688,178 @@ module ActionDispatch
scope(options) { yield }
end
+ # === Parameter Restriction
+ # Allows you to constrain the nested routes based on a set of rules.
+ # For instance, in order to change the routes to allow for a dot character in the +id+ parameter:
+ #
+ # constraints(:id => /\d+\.\d+) do
+ # resources :posts
+ # end
+ #
+ # Now routes such as +/posts/1+ will no longer be valid, but +/posts/1.1+ will be.
+ # The +id+ parameter must match the constraint passed in for this example.
+ #
+ # You may use this to also restrict other parameters:
+ #
+ # resources :posts do
+ # constraints(:post_id => /\d+\.\d+) do
+ # resources :comments
+ # end
+ #
+ # === Restricting based on IP
+ #
+ # Routes can also be constrained to an IP or a certain range of IP addresses:
+ #
+ # constraints(:ip => /192.168.\d+.\d+/) do
+ # resources :posts
+ # end
+ #
+ # Any user connecting from the 192.168.* range will be able to see this resource,
+ # where as any user connecting outside of this range will be told there is no such route.
+ #
+ # === Dynamic request matching
+ #
+ # Requests to routes can be constrained based on specific criteria:
+ #
+ # constraints(lambda { |req| req.env["HTTP_USER_AGENT"] =~ /iPhone/ }) do
+ # resources :iphones
+ # end
+ #
+ # You are able to move this logic out into a class if it is too complex for routes.
+ # This class must have a +matches?+ method defined on it which either returns +true+
+ # if the user should be given access to that route, or +false+ if the user should not.
+ #
+ # class Iphone
+ # def self.matches(request)
+ # request.env["HTTP_USER_AGENT"] =~ /iPhone/
+ # end
+ # end
+ #
+ # An expected place for this code would be +lib/constraints+.
+ #
+ # This class is then used like this:
+ #
+ # constraints(Iphone) do
+ # resources :iphones
+ # end
def constraints(constraints = {})
scope(:constraints => constraints) { yield }
end
+ # Allows you to set default parameters for a route, such as this:
+ # defaults :id => 'home' do
+ # match 'scoped_pages/(:id)', :to => 'pages#show'
+ # end
+ # Using this, the +:id+ parameter here will default to 'home'.
def defaults(defaults = {})
scope(:defaults => defaults) { yield }
end
- def match(*args)
- options = args.extract_options!
-
- options = (@scope[:options] || {}).merge(options)
-
- if @scope[:as] && !options[:as].blank?
- options[:as] = "#{@scope[:as]}_#{options[:as]}"
- elsif @scope[:as] && options[:as] == ""
- options[:as] = @scope[:as].to_s
- end
-
- args.push(options)
- super(*args)
- end
-
private
- def scope_options
+ def scope_options #:nodoc:
@scope_options ||= private_methods.grep(/^merge_(.+)_scope$/) { $1.to_sym }
end
- def merge_path_scope(parent, child)
+ def merge_path_scope(parent, child) #:nodoc:
Mapper.normalize_path("#{parent}/#{child}")
end
- def merge_shallow_path_scope(parent, child)
+ def merge_shallow_path_scope(parent, child) #:nodoc:
Mapper.normalize_path("#{parent}/#{child}")
end
- def merge_as_scope(parent, child)
+ def merge_as_scope(parent, child) #:nodoc:
parent ? "#{parent}_#{child}" : child
end
- def merge_shallow_prefix_scope(parent, child)
+ def merge_shallow_prefix_scope(parent, child) #:nodoc:
parent ? "#{parent}_#{child}" : child
end
- def merge_module_scope(parent, child)
+ def merge_module_scope(parent, child) #:nodoc:
parent ? "#{parent}/#{child}" : child
end
- def merge_controller_scope(parent, child)
+ def merge_controller_scope(parent, child) #:nodoc:
child
end
- def merge_path_names_scope(parent, child)
+ def merge_path_names_scope(parent, child) #:nodoc:
merge_options_scope(parent, child)
end
- def merge_constraints_scope(parent, child)
+ def merge_constraints_scope(parent, child) #:nodoc:
merge_options_scope(parent, child)
end
- def merge_defaults_scope(parent, child)
+ def merge_defaults_scope(parent, child) #:nodoc:
merge_options_scope(parent, child)
end
- def merge_blocks_scope(parent, child)
+ def merge_blocks_scope(parent, child) #:nodoc:
merged = parent ? parent.dup : []
merged << child if child
merged
end
- def merge_options_scope(parent, child)
+ def merge_options_scope(parent, child) #:nodoc:
(parent || {}).except(*override_keys(child)).merge(child)
end
- def merge_shallow_scope(parent, child)
+ def merge_shallow_scope(parent, child) #:nodoc:
child ? true : false
end
- def override_keys(child)
+ 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
+ # for a given resourceful controller. Instead of declaring separate routes
+ # for your +index+, +show+, +new+, +edit+, +create+, +update+ and +destroy+
+ # actions, a resourceful route declares them in a single line of code:
+ #
+ # resources :photos
+ #
+ # Sometimes, you have a resource that clients always look up without
+ # referencing an ID. A common example, /profile always shows the profile of
+ # the currently logged in user. In this case, you can use a singular resource
+ # to map /profile (rather than /profile/:id) to the show action.
+ #
+ # resource :profile
+ #
+ # It's common to have resources that are logically children of other
+ # resources:
+ #
+ # resources :magazines do
+ # resources :ads
+ # end
+ #
+ # You may wish to organize groups of controllers under a namespace. Most
+ # commonly, you might group a number of administrative controllers under
+ # an +admin+ namespace. You would place these controllers under the
+ # app/controllers/admin directory, and you can group them together in your
+ # router:
+ #
+ # namespace "admin" do
+ # resources :posts, :comments
+ # end
+ #
+ # By default the :id parameter doesn't accept dots. If you need to
+ # use dots as part of the :id parameter add a constraint which
+ # overrides this restriction, e.g:
+ #
+ # resources :articles, :id => /[^\/]+/
+ #
+ # This allows any character other than a slash as part of your :id.
+ #
module Resources
# CANONICAL_ACTIONS holds all actions that does not need a prefix or
# a path appended since they fit properly in their scope level.
- VALID_ON_OPTIONS = [:new, :collection, :member]
- CANONICAL_ACTIONS = [:index, :create, :new, :show, :update, :destroy]
- RESOURCE_OPTIONS = [:as, :controller, :path, :only, :except]
+ VALID_ON_OPTIONS = [:new, :collection, :member]
+ RESOURCE_OPTIONS = [:as, :controller, :path, :only, :except]
+ CANONICAL_ACTIONS = %w(index create new show update destroy)
class Resource #:nodoc:
DEFAULT_ACTIONS = [:index, :create, :new, :show, :update, :destroy, :edit]
@@ -473,7 +868,7 @@ module ActionDispatch
def initialize(entities, options = {})
@name = entities.to_s
- @path = options.delete(:path) || @name
+ @path = (options.delete(:path) || @name).to_s
@controller = (options.delete(:controller) || @name).to_s
@as = options.delete(:as)
@options = options
@@ -498,18 +893,17 @@ module ActionDispatch
end
def plural
- name.to_s.pluralize
+ @plural ||= name.to_s
end
def singular
- name.to_s.singularize
+ @singular ||= name.to_s.singularize
end
- def member_name
- singular
- end
+ alias :member_name :singular
- # Checks for uncountable plurals, and appends "_index" if they're.
+ # Checks for uncountable plurals, and appends "_index" if the plural
+ # and singular form are the same.
def collection_name
singular == plural ? "#{plural}_index" : plural
end
@@ -518,9 +912,7 @@ module ActionDispatch
{ :controller => controller }
end
- def collection_scope
- path
- end
+ alias :collection_scope :path
def member_scope
"#{path}/:id"
@@ -540,33 +932,54 @@ module ActionDispatch
DEFAULT_ACTIONS = [:show, :create, :update, :destroy, :new, :edit]
def initialize(entities, options)
+ @as = nil
@name = entities.to_s
- @path = options.delete(:path) || @name
+ @path = (options.delete(:path) || @name).to_s
@controller = (options.delete(:controller) || plural).to_s
@as = options.delete(:as)
@options = options
end
- def member_name
- name
+ def plural
+ @plural ||= name.to_s.pluralize
end
- alias :collection_name :member_name
- def member_scope
- path
+ def singular
+ @singular ||= name.to_s
end
- alias :nested_scope :member_scope
- end
- def initialize(*args) #:nodoc:
- super
- @scope[:path_names] = @set.resources_path_names
+ alias :member_name :singular
+ alias :collection_name :singular
+
+ alias :member_scope :path
+ alias :nested_scope :path
end
def resources_path_names(options)
@scope[:path_names].merge!(options)
end
+ # Sometimes, you have a resource that clients always look up without
+ # referencing an ID. A common example, /profile always shows the
+ # profile of the currently logged in user. In this case, you can use
+ # a singular resource to map /profile (rather than /profile/:id) to
+ # the show action:
+ #
+ # resource :geocoder
+ #
+ # creates six different routes in your application, all mapping to
+ # the GeoCoders controller (note that the controller is named after
+ # the plural):
+ #
+ # GET /geocoder/new
+ # POST /geocoder
+ # GET /geocoder
+ # GET /geocoder/edit
+ # PUT /geocoder
+ # DELETE /geocoder
+ #
+ # === Options
+ # Takes same options as +resources+.
def resource(*resources, &block)
options = resources.extract_options!
@@ -577,25 +990,121 @@ module ActionDispatch
resource_scope(SingletonResource.new(resources.pop, options)) do
yield if block_given?
- collection_scope do
+ collection do
post :create
end if parent_resource.actions.include?(:create)
- new_scope do
+ new do
get :new
end if parent_resource.actions.include?(:new)
- member_scope do
+ member do
+ get :edit if parent_resource.actions.include?(:edit)
get :show if parent_resource.actions.include?(:show)
put :update if parent_resource.actions.include?(:update)
delete :destroy if parent_resource.actions.include?(:destroy)
- get :edit if parent_resource.actions.include?(:edit)
end
end
self
end
+ # In Rails, a resourceful route provides a mapping between HTTP verbs
+ # and URLs and controller actions. By convention, each action also maps
+ # to particular CRUD operations in a database. A single entry in the
+ # routing file, such as
+ #
+ # resources :photos
+ #
+ # creates seven different routes in your application, all mapping to
+ # the Photos controller:
+ #
+ # GET /photos/new
+ # POST /photos
+ # GET /photos/:id
+ # GET /photos/:id/edit
+ # PUT /photos/:id
+ # DELETE /photos/:id
+ #
+ # Resources can also be nested infinitely by using this block syntax:
+ #
+ # resources :photos do
+ # resources :comments
+ # end
+ #
+ # This generates the following comments routes:
+ #
+ # GET /photos/:id/comments/new
+ # POST /photos/:id/comments
+ # GET /photos/:id/comments/:id
+ # GET /photos/:id/comments/:id/edit
+ # PUT /photos/:id/comments/:id
+ # DELETE /photos/:id/comments/:id
+ #
+ # === Options
+ # Takes same options as <tt>Base#match</tt> as well as:
+ #
+ # [:path_names]
+ # Allows you to change the paths of the seven default actions.
+ # Paths not specified are not changed.
+ #
+ # resources :posts, :path_names => { :new => "brand_new" }
+ #
+ # The above example will now change /posts/new to /posts/brand_new
+ #
+ # [:only]
+ # Only generate routes for the given actions.
+ #
+ # resources :cows, :only => :show
+ # resources :cows, :only => [:show, :index]
+ #
+ # [:except]
+ # Generate all routes except for the given actions.
+ #
+ # resources :cows, :except => :show
+ # resources :cows, :except => [:show, :index]
+ #
+ # [:shallow]
+ # Generates shallow routes for nested resource(s). When placed on a parent resource,
+ # generates shallow routes for all nested resources.
+ #
+ # resources :posts, :shallow => true do
+ # resources :comments
+ # end
+ #
+ # Is the same as:
+ #
+ # resources :posts do
+ # resources :comments
+ # end
+ # resources :comments
+ #
+ # [:shallow_path]
+ # Prefixes nested shallow routes with the specified path.
+ #
+ # scope :shallow_path => "sekret" do
+ # resources :posts do
+ # resources :comments, :shallow => true
+ # end
+ # end
+ #
+ # The +comments+ resource here will have the following routes generated for it:
+ #
+ # post_comments GET /sekret/posts/:post_id/comments(.:format)
+ # post_comments POST /sekret/posts/:post_id/comments(.:format)
+ # new_post_comment GET /sekret/posts/:post_id/comments/new(.:format)
+ # edit_comment GET /sekret/comments/:id/edit(.:format)
+ # comment GET /sekret/comments/:id(.:format)
+ # comment PUT /sekret/comments/:id(.:format)
+ # comment DELETE /sekret/comments/:id(.:format)
+ #
+ # === Examples
+ #
+ # # routes call Admin::PostsController
+ # resources :posts, :module => "admin"
+ #
+ # # resource actions are at /admin/posts.
+ # resources :posts, :path => "admin"
def resources(*resources, &block)
options = resources.extract_options!
@@ -606,43 +1115,70 @@ module ActionDispatch
resource_scope(Resource.new(resources.pop, options)) do
yield if block_given?
- collection_scope do
+ collection do
get :index if parent_resource.actions.include?(:index)
post :create if parent_resource.actions.include?(:create)
end
- new_scope do
+ new do
get :new
end if parent_resource.actions.include?(:new)
- member_scope do
+ member do
+ get :edit if parent_resource.actions.include?(:edit)
get :show if parent_resource.actions.include?(:show)
put :update if parent_resource.actions.include?(:update)
delete :destroy if parent_resource.actions.include?(:destroy)
- get :edit if parent_resource.actions.include?(:edit)
end
end
self
end
+ # To add a route to the collection:
+ #
+ # resources :photos do
+ # collection do
+ # get 'search'
+ # end
+ # end
+ #
+ # This will enable Rails to recognize paths such as <tt>/photos/search</tt>
+ # with GET, and route to the search action of PhotosController. It will also
+ # create the <tt>search_photos_url</tt> and <tt>search_photos_path</tt>
+ # route helpers.
def collection
- unless @scope[:scope_level] == :resources
- raise ArgumentError, "can't use collection outside resources scope"
+ unless resource_scope?
+ raise ArgumentError, "can't use collection outside resource(s) scope"
end
- collection_scope do
- yield
+ with_scope_level(:collection) do
+ scope(parent_resource.collection_scope) do
+ yield
+ end
end
end
+ # To add a member route, add a member block into the resource block:
+ #
+ # resources :photos do
+ # member do
+ # get 'preview'
+ # end
+ # end
+ #
+ # This will recognize <tt>/photos/1/preview</tt> with GET, and route to the
+ # preview action of PhotosController. It will also create the
+ # <tt>preview_photo_url</tt> and <tt>preview_photo_path</tt> helpers.
def member
unless resource_scope?
raise ArgumentError, "can't use member outside resource(s) scope"
end
- member_scope do
- yield
+ with_scope_level(:member) do
+ scope(parent_resource.member_scope) do
+ yield
+ end
end
end
@@ -651,8 +1187,10 @@ module ActionDispatch
raise ArgumentError, "can't use new outside resource(s) scope"
end
- new_scope do
- yield
+ with_scope_level(:new) do
+ scope(parent_resource.new_scope(action_path(:new))) do
+ yield
+ end
end
end
@@ -678,6 +1216,7 @@ module ActionDispatch
end
end
+ # See ActionDispatch::Routing::Mapper::Scoping#namespace
def namespace(path, options = {})
if resource_scope?
nested { super }
@@ -687,7 +1226,7 @@ module ActionDispatch
end
def shallow
- scope(:shallow => true) do
+ scope(:shallow => true, :shallow_path => @scope[:path]) do
yield
end
end
@@ -713,46 +1252,36 @@ module ActionDispatch
raise ArgumentError, "Unknown scope #{on.inspect} given to :on"
end
- if @scope[:scope_level] == :resource
+ if @scope[:scope_level] == :resources
+ args.push(options)
+ return nested { match(*args) }
+ elsif @scope[:scope_level] == :resource
args.push(options)
return member { match(*args) }
end
- path = options.delete(:path)
action = args.first
+ path = path_for_action(action, options.delete(:path))
- if action.is_a?(Symbol)
- path = path_for_action(action, path)
- options[:to] ||= action
- options[:as] = name_for_action(action, options[:as])
-
- with_exclusive_scope do
- return super(path, options)
- end
- elsif resource_method_scope?
- path = path_for_custom_action
- options[:as] = name_for_action(options[:as]) if options[:as]
- args.push(options)
-
- with_exclusive_scope do
- scope(path) do
- return super
- end
- end
+ if action.to_s =~ /^[\w\/]+$/
+ options[:action] ||= action unless action.to_s.include?("/")
+ else
+ action = nil
end
- if resource_scope?
- raise ArgumentError, "can't define route directly in resource(s) scope"
+ if options.key?(:as) && !options[:as]
+ options.delete(:as)
+ else
+ options[:as] = name_for_action(options[:as], action)
end
- args.push(options)
- super
+ super(path, options)
end
def root(options={})
if @scope[:scope_level] == :resources
- with_scope_level(:nested) do
- scope(parent_resource.path, :as => parent_resource.collection_name) do
+ with_scope_level(:root) do
+ scope(parent_resource.path) do
super(options)
end
end
@@ -767,12 +1296,21 @@ module ActionDispatch
@scope[:scope_level_resource]
end
- def apply_common_behavior_for(method, resources, options, &block)
+ def apply_common_behavior_for(method, resources, options, &block) #:nodoc:
if resources.length > 1
resources.each { |r| send(method, r, options, &block) }
return true
end
+ if resource_scope?
+ nested { send(method, resources.pop, options, &block) }
+ return true
+ end
+
+ options.keys.each do |k|
+ (options[:constraints] ||= {})[k] = options.delete(k) if options[k].is_a?(Regexp)
+ end
+
scope_options = options.slice!(*RESOURCE_OPTIONS)
unless scope_options.empty?
scope(scope_options) do
@@ -785,33 +1323,26 @@ module ActionDispatch
options.merge!(scope_action_options) if scope_action_options?
end
- if resource_scope?
- nested do
- send(method, resources.pop, options, &block)
- end
- return true
- end
-
false
end
- def action_options?(options)
+ def action_options?(options) #:nodoc:
options[:only] || options[:except]
end
- def scope_action_options?
+ def scope_action_options? #:nodoc:
@scope[:options].is_a?(Hash) && (@scope[:options][:only] || @scope[:options][:except])
end
- def scope_action_options
+ def scope_action_options #:nodoc:
@scope[:options].slice(:only, :except)
end
- def resource_scope?
+ def resource_scope? #:nodoc:
[:resource, :resources].include?(@scope[:scope_level])
end
- def resource_method_scope?
+ def resource_method_scope? #:nodoc:
[:collection, :member, :new].include?(@scope[:scope_level])
end
@@ -837,7 +1368,7 @@ module ActionDispatch
@scope[:scope_level_resource] = old_resource
end
- def resource_scope(resource)
+ def resource_scope(resource) #:nodoc:
with_scope_level(resource.is_a?(SingletonResource) ? :resource : :resources, resource) do
scope(parent_resource.resource_scope) do
yield
@@ -845,116 +1376,108 @@ module ActionDispatch
end
end
- def new_scope
- with_scope_level(:new) do
- scope(parent_resource.new_scope(action_path(:new))) do
- yield
- end
- end
- end
-
- def collection_scope
- with_scope_level(:collection) do
- scope(parent_resource.collection_scope) do
- yield
- end
- end
- end
-
- def member_scope
- with_scope_level(:member) do
- scope(parent_resource.member_scope) do
- yield
- end
- end
- end
-
- def nested_options
+ def nested_options #:nodoc:
{}.tap do |options|
options[:as] = parent_resource.member_name
options[:constraints] = { "#{parent_resource.singular}_id".to_sym => id_constraint } if id_constraint?
end
end
- def id_constraint?
- @scope[:id].is_a?(Regexp) || (@scope[:constraints] && @scope[:constraints][:id].is_a?(Regexp))
+ def id_constraint? #:nodoc:
+ @scope[:constraints] && @scope[:constraints][:id].is_a?(Regexp)
end
- def id_constraint
- @scope[:id] || @scope[:constraints][:id]
+ def id_constraint #:nodoc:
+ @scope[:constraints][:id]
end
- def canonical_action?(action, flag)
- flag && CANONICAL_ACTIONS.include?(action)
+ def canonical_action?(action, flag) #:nodoc:
+ flag && resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s)
end
- def shallow_scoping?
+ def shallow_scoping? #:nodoc:
shallow? && @scope[:scope_level] == :member
end
- def path_for_action(action, path)
+ def path_for_action(action, path) #:nodoc:
prefix = shallow_scoping? ?
"#{@scope[:shallow_path]}/#{parent_resource.path}/:id" : @scope[:path]
- if canonical_action?(action, path.blank?)
- "#{prefix}(.:format)"
- else
- "#{prefix}/#{action_path(action, path)}(.:format)"
- end
- end
-
- def path_for_custom_action
- if shallow_scoping?
- "#{@scope[:shallow_path]}/#{parent_resource.path}/:id"
+ path = if canonical_action?(action, path.blank?)
+ prefix.to_s
else
- @scope[:path]
+ "#{prefix}/#{action_path(action, path)}"
end
end
- def action_path(name, path = nil)
+ def action_path(name, path = nil) #:nodoc:
path || @scope[:path_names][name.to_sym] || name.to_s
end
- def prefix_name_for_action(action, as)
- if as.present?
- "#{as}_"
- elsif as
- ""
+ def prefix_name_for_action(as, action) #:nodoc:
+ if as
+ as.to_s
elsif !canonical_action?(action, @scope[:scope_level])
- "#{action}_"
+ action.to_s
end
end
- def name_for_action(action, as=nil)
- prefix = prefix_name_for_action(action, as)
+ 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
+ return nil if as.nil? && action.nil?
+
collection_name = parent_resource.collection_name
member_name = parent_resource.member_name
- name_prefix = "#{name_prefix}_" if name_prefix.present?
end
- case @scope[:scope_level]
+ name = case @scope[:scope_level]
+ when :nested
+ [name_prefix, prefix]
when :collection
- "#{prefix}#{name_prefix}#{collection_name}"
+ [prefix, name_prefix, collection_name]
when :new
- "#{prefix}new_#{name_prefix}#{member_name}"
+ [prefix, :new, name_prefix, member_name]
+ when :member
+ [prefix, shallow_scoping? ? @scope[:shallow_prefix] : name_prefix, member_name]
+ when :root
+ [name_prefix, collection_name, prefix]
else
- if shallow_scoping?
- shallow_prefix = "#{@scope[:shallow_prefix]}_" if @scope[:shallow_prefix].present?
- "#{prefix}#{shallow_prefix}#{member_name}"
- else
- "#{prefix}#{name_prefix}#{member_name}"
- end
+ [name_prefix, member_name, prefix]
end
+
+ candidate = name.select(&:present?).join("_").presence
+ candidate unless as.nil? && @set.routes.find { |r| r.name == candidate }
+ end
+ end
+
+ module Shorthand #:nodoc:
+ def match(*args)
+ if args.size == 1 && args.last.is_a?(Hash)
+ options = args.pop
+ path, to = options.find { |name, value| name.is_a?(String) }
+ options.merge!(:to => to).delete(path)
+ super(path, options)
+ else
+ super
end
+ end
+ end
+
+ def initialize(set) #:nodoc:
+ @set = set
+ @scope = { :path_names => @set.resources_path_names }
end
include Base
include HttpHelpers
+ include Redirection
include Scoping
include Resources
+ include Shorthand
end
end
end
diff --git a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb
index 31dba835ac..82c4fadb50 100644
--- a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb
+++ b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb
@@ -17,7 +17,7 @@ module ActionDispatch
#
# == Usage within the framework
#
- # Polymorphic URL helpers are used in a number of places throughout the Rails framework:
+ # Polymorphic URL helpers are used in a number of places throughout the \Rails framework:
#
# * <tt>url_for</tt>, so you can use it with a record as the argument, e.g.
# <tt>url_for(@article)</tt>;
@@ -42,6 +42,18 @@ module ActionDispatch
#
# edit_polymorphic_path(@post) # => "/posts/1/edit"
# polymorphic_path(@post, :format => :pdf) # => "/posts/1.pdf"
+ #
+ # == Using with mounted engines
+ #
+ # If you use mounted engine, there is a possibility that you will need to use
+ # polymorphic_url pointing at engine's routes. To do that, just pass proxy used
+ # to reach engine's routes as a first argument:
+ #
+ # For example:
+ #
+ # polymorphic_url([blog, @post]) # it will call blog.post_path(@post)
+ # form_for([blog, @post]) # => "/blog/posts/1
+ #
module PolymorphicRoutes
# Constructs a call to a named RESTful route for the given record and returns the
# resulting URL string. For example:
@@ -78,19 +90,20 @@ module ActionDispatch
def polymorphic_url(record_or_hash_or_array, options = {})
if record_or_hash_or_array.kind_of?(Array)
record_or_hash_or_array = record_or_hash_or_array.compact
+ if record_or_hash_or_array.first.is_a?(ActionDispatch::Routing::RoutesProxy)
+ proxy = record_or_hash_or_array.shift
+ end
record_or_hash_or_array = record_or_hash_or_array[0] if record_or_hash_or_array.size == 1
end
record = extract_record(record_or_hash_or_array)
record = record.to_model if record.respond_to?(:to_model)
- args = case record_or_hash_or_array
- when Hash; [ record_or_hash_or_array ]
- when Array; record_or_hash_or_array.dup
- else [ record_or_hash_or_array ]
- end
+ args = Array === record_or_hash_or_array ?
+ record_or_hash_or_array.dup :
+ [ record_or_hash_or_array ]
- inflection = if options[:action].to_s == "new"
+ inflection = if options[:action] && options[:action].to_s == "new"
args.pop
:singular
elsif (record.respond_to?(:persisted?) && !record.persisted?)
@@ -111,7 +124,14 @@ module ActionDispatch
args.last.kind_of?(Hash) ? args.last.merge!(url_options) : args << url_options
end
- send(named_route, *args)
+ if proxy
+ proxy.send(named_route, *args)
+ else
+ # we need to use url_for, because polymorphic_url can be used in context of other than
+ # current routes (e.g. engine's routes). As named routes from engine are not included
+ # calling engine's named route directly would fail.
+ url_for _routes.url_helpers.__send__("hash_for_#{named_route}", *args)
+ end
end
# Returns the path component of a URL for the given record. It uses
@@ -146,31 +166,31 @@ module ActionDispatch
end
def build_named_route_call(records, inflection, options = {})
- unless records.is_a?(Array)
- record = extract_record(records)
- route = ''
- else
+ if records.is_a?(Array)
record = records.pop
- route = records.inject("") do |string, parent|
+ route = records.map do |parent|
if parent.is_a?(Symbol) || parent.is_a?(String)
- string << "#{parent}_"
+ parent
else
- string << ActiveModel::Naming.plural(parent).singularize
- string << "_"
+ ActiveModel::Naming.route_key(parent).singularize
end
end
+ else
+ record = extract_record(records)
+ route = []
end
if record.is_a?(Symbol) || record.is_a?(String)
- route << "#{record}_"
+ route << record
else
- route << ActiveModel::Naming.plural(record)
- route = route.singularize if inflection == :singular
- route << "_"
- route << "index_" if ActiveModel::Naming.uncountable?(record) && inflection == :plural
+ route << ActiveModel::Naming.route_key(record)
+ route = [route.join("_").singularize] if inflection == :singular
+ route << "index" if ActiveModel::Naming.uncountable?(record) && inflection == :plural
end
- action_prefix(options) + route + routing_type(options).to_s
+ route << routing_type(options)
+
+ action_prefix(options) + route.join("_")
end
def extract_record(record_or_hash_or_array)
diff --git a/actionpack/lib/action_dispatch/routing/redirection.rb b/actionpack/lib/action_dispatch/routing/redirection.rb
new file mode 100644
index 0000000000..804991ad5f
--- /dev/null
+++ b/actionpack/lib/action_dispatch/routing/redirection.rb
@@ -0,0 +1,110 @@
+require 'action_dispatch/http/request'
+
+module ActionDispatch
+ module Routing
+ module Redirection
+
+ # Redirect any path to another path:
+ #
+ # match "/stories" => redirect("/posts")
+ #
+ # You can also use interpolation in the supplied redirect argument:
+ #
+ # match 'docs/:article', :to => redirect('/wiki/%{article}')
+ #
+ # Alternatively you can use one of the other syntaxes:
+ #
+ # The block version of redirect allows for the easy encapsulation of any logic associated with
+ # the redirect in question. Either the params and request are supplied as arguments, or just
+ # params, depending of how many arguments your block accepts. A string is required as a
+ # return value.
+ #
+ # match 'jokes/:number', :to => redirect do |params, request|
+ # path = (params[:number].to_i.even? ? "/wheres-the-beef" : "/i-love-lamp")
+ # "http://#{request.host_with_port}/#{path}"
+ # end
+ #
+ # The options version of redirect allows you to supply only the parts of the url which need
+ # to change, it also supports interpolation of the path similar to the first example.
+ #
+ # match 'stores/:name', :to => redirect(:subdomain => 'stores', :path => '/%{name}')
+ # match 'stores/:name(*all)', :to => redirect(:subdomain => 'stores', :path => '/%{name}%{all}')
+ #
+ # Finally, an object which responds to call can be supplied to redirect, allowing you to reuse
+ # common redirect routes. The call method must accept two arguments, params and request, and return
+ # a string.
+ #
+ # match 'accounts/:name' => redirect(SubdomainRedirector.new('api'))
+ #
+ def redirect(*args, &block)
+ options = args.last.is_a?(Hash) ? args.pop : {}
+ status = options.delete(:status) || 301
+
+ path = args.shift
+
+ path_proc = if path.is_a?(String)
+ proc { |params| (params.empty? || !path.match(/%\{\w*\}/)) ? path : (path % params) }
+ elsif options.any?
+ options_proc(options)
+ elsif path.respond_to?(:call)
+ proc { |params, request| path.call(params, request) }
+ elsif block
+ block
+ else
+ raise ArgumentError, "redirection argument not supported"
+ end
+
+ redirection_proc(status, path_proc)
+ end
+
+ private
+
+ def options_proc(options)
+ proc do |params, request|
+ path = if options[:path].nil?
+ request.path
+ elsif params.empty? || !options[:path].match(/%\{\w*\}/)
+ options.delete(:path)
+ else
+ (options.delete(:path) % params)
+ end
+
+ default_options = {
+ :protocol => request.protocol,
+ :host => request.host,
+ :port => request.optional_port,
+ :path => path,
+ :params => request.query_parameters
+ }
+
+ ActionDispatch::Http::URL.url_for(options.reverse_merge(default_options))
+ end
+ end
+
+ def redirection_proc(status, path_proc)
+ lambda do |env|
+ req = Request.new(env)
+
+ params = [req.symbolized_path_parameters]
+ params << req if path_proc.arity > 1
+
+ uri = URI.parse(path_proc.call(*params))
+ uri.scheme ||= req.scheme
+ uri.host ||= req.host
+ uri.port ||= req.port unless req.standard_port?
+
+ body = %(<html><body>You are being <a href="#{ERB::Util.h(uri.to_s)}">redirected</a>.</body></html>)
+
+ headers = {
+ 'Location' => uri.to_s,
+ 'Content-Type' => 'text/html',
+ 'Content-Length' => body.length.to_s
+ }
+
+ [ status, headers, [body] ]
+ end
+ end
+
+ end
+ end
+end \ No newline at end of file
diff --git a/actionpack/lib/action_dispatch/routing/route.rb b/actionpack/lib/action_dispatch/routing/route.rb
index aefebf8f80..a049510182 100644
--- a/actionpack/lib/action_dispatch/routing/route.rb
+++ b/actionpack/lib/action_dispatch/routing/route.rb
@@ -1,3 +1,5 @@
+require 'active_support/core_ext/module/deprecation'
+
module ActionDispatch
module Routing
class Route #:nodoc:
@@ -10,6 +12,8 @@ module ActionDispatch
@defaults = defaults
@name = name
+ # FIXME: we should not be doing this much work in a constructor.
+
@requirements = requirements.merge(defaults)
@requirements.delete(:controller) if @requirements[:controller].is_a?(Regexp)
@requirements.delete_if { |k, v|
@@ -21,24 +25,22 @@ module ActionDispatch
conditions[:path_info] = ::Rack::Mount::Strexp.compile(path, requirements, SEPARATORS, anchor)
end
- @conditions = conditions.inject({}) { |h, (k, v)|
- h[k] = Rack::Mount::RegexpWithNamedGroups.new(v)
- h
- }
+ @verbs = conditions[:request_method] || []
+ @conditions = 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.
+ @conditions[:request_method] = %r[^#{verb}$] unless @verbs.empty?
+ @conditions[:path_info] = Rack::Mount::RegexpWithNamedGroups.new(@conditions[:path_info]) if @conditions[:path_info]
@conditions.delete_if{ |k,v| k != :path_info && !valid_condition?(k) }
@requirements.delete_if{ |k,v| !valid_condition?(k) }
end
def verb
- if method = conditions[:request_method]
- case method
- when Regexp
- method.source.upcase
- else
- method.to_s.upcase
- end
- end
+ @verbs.join '|'
end
def segment_keys
@@ -48,6 +50,7 @@ module ActionDispatch
def to_a
[@app, @conditions, @defaults, @name]
end
+ deprecate :to_a
def to_s
@to_s ||= begin
diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb
index d23b580d97..b28f6c2297 100644
--- a/actionpack/lib/action_dispatch/routing/route_set.rb
+++ b/actionpack/lib/action_dispatch/routing/route_set.rb
@@ -1,7 +1,8 @@
require 'rack/mount'
require 'forwardable'
+require 'active_support/core_ext/object/blank'
require 'active_support/core_ext/object/to_query'
-require 'action_dispatch/routing/deprecated_mapper'
+require 'active_support/core_ext/hash/slice'
module ActionDispatch
module Routing
@@ -50,12 +51,13 @@ module ActionDispatch
private
def controller_reference(controller_param)
+ controller_name = "#{controller_param.camelize}Controller"
+
unless controller = @controllers[controller_param]
- controller_name = "#{controller_param.camelize}Controller"
controller = @controllers[controller_param] =
- ActiveSupport::Dependencies.ref(controller_name)
+ ActiveSupport::Dependencies.reference(controller_name)
end
- controller.get
+ controller.get(controller_name)
end
def dispatch(controller, action, env)
@@ -67,7 +69,7 @@ module ActionDispatch
end
def split_glob_param!(params)
- params[@glob_param] = params[@glob_param].split('/').map { |v| URI.unescape(v) }
+ params[@glob_param] = params[@glob_param].split('/').map { |v| URI.parser.unescape(v) }
end
end
@@ -158,10 +160,18 @@ module ActionDispatch
# We use module_eval to avoid leaks
@module.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1
- def #{selector}(options = nil) # def hash_for_users_url(options = nil)
- options ? #{options.inspect}.merge(options) : #{options.inspect} # options ? {:only_path=>false}.merge(options) : {:only_path=>false}
- end # end
- protected :#{selector} # protected :hash_for_users_url
+ remove_method :#{selector} if method_defined?(:#{selector})
+ def #{selector}(*args)
+ options = args.extract_options!
+
+ if args.any?
+ options[:_positional_args] = args
+ options[:_positional_keys] = #{route.segment_keys.inspect}
+ end
+
+ options ? #{options.inspect}.merge(options) : #{options.inspect}
+ end
+ protected :#{selector}
END_EVAL
helpers << selector
end
@@ -184,22 +194,16 @@ module ActionDispatch
hash_access_method = hash_access_name(name, kind)
@module.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1
+ remove_method :#{selector} if method_defined?(:#{selector})
def #{selector}(*args)
- options = #{hash_access_method}(args.extract_options!)
-
- if args.any?
- options[:_positional_args] = args
- options[:_positional_keys] = #{route.segment_keys.inspect}
- end
-
- url_for(options)
+ url_for(#{hash_access_method}(*args))
end
END_EVAL
helpers << selector
end
end
- attr_accessor :set, :routes, :named_routes
+ attr_accessor :set, :routes, :named_routes, :default_scope
attr_accessor :disable_clear_and_finalize, :resources_path_names
attr_accessor :default_url_options, :request_class, :valid_conditions
@@ -211,7 +215,6 @@ module ActionDispatch
self.routes = []
self.named_routes = NamedRouteCollection.new
self.resources_path_names = self.class.default_resources_path_names.dup
- self.controller_namespaces = Set.new
self.default_url_options = {}
self.request_class = request_class
@@ -219,27 +222,35 @@ module ActionDispatch
self.valid_conditions.delete(:id)
self.valid_conditions.push(:controller, :action)
+ @append = []
@disable_clear_and_finalize = false
clear!
end
def draw(&block)
clear! unless @disable_clear_and_finalize
+ eval_block(block)
+ finalize! unless @disable_clear_and_finalize
+ nil
+ end
+
+ def append(&block)
+ @append << block
+ end
+
+ def eval_block(block)
mapper = Mapper.new(self)
- if block.arity == 1
- mapper.instance_exec(DeprecatedMapper.new(self), &block)
+ if default_scope
+ mapper.with_default_scope(default_scope, &block)
else
mapper.instance_exec(&block)
end
-
- finalize! unless @disable_clear_and_finalize
-
- nil
end
def finalize!
return if @finalized
+ @append.each { |blk| eval_block(blk) }
@finalized = true
@set.freeze
end
@@ -261,6 +272,31 @@ module ActionDispatch
named_routes.install(destinations, regenerate_code)
end
+ module MountedHelpers
+ end
+
+ def mounted_helpers(name = :main_app)
+ define_mounted_helper(name) if name
+ MountedHelpers
+ end
+
+ def define_mounted_helper(name)
+ return if MountedHelpers.method_defined?(name)
+
+ routes = self
+ MountedHelpers.class_eval do
+ define_method "_#{name}" do
+ RoutesProxy.new(routes, self._routes_context)
+ end
+ end
+
+ MountedHelpers.class_eval <<-RUBY
+ def #{name}
+ @#{name} ||= _#{name}
+ end
+ RUBY
+ end
+
def url_helpers
@url_helpers ||= begin
routes = self
@@ -269,9 +305,9 @@ module ActionDispatch
extend ActiveSupport::Concern
include UrlFor
- @routes = routes
+ @_routes = routes
class << self
- delegate :url_for, :to => '@routes'
+ delegate :url_for, :to => '@_routes'
end
extend routes.named_routes.module
@@ -280,10 +316,10 @@ module ActionDispatch
# Yes plz - JP
included do
routes.install_helpers(self)
- singleton_class.send(:define_method, :_routes) { routes }
+ singleton_class.send(:redefine_method, :_routes) { routes }
end
- define_method(:_routes) { routes }
+ define_method(:_routes) { @_routes || routes }
end
helpers
@@ -295,18 +331,31 @@ module ActionDispatch
end
def add_route(app, conditions = {}, requirements = {}, defaults = {}, name = nil, anchor = true)
+ raise ArgumentError, "Invalid route name: '#{name}'" unless name.blank? || name.to_s.match(/^[_a-z]\w*$/i)
route = Route.new(self, app, conditions, requirements, defaults, name, anchor)
- @set.add_route(*route)
+ @set.add_route(route.app, route.conditions, route.defaults, route.name)
named_routes[name] = route if name
routes << route
route
end
class Generator #:nodoc:
- attr_reader :options, :recall, :set, :script_name, :named_route
+ PARAMETERIZE = {
+ :parameterize => lambda do |name, value|
+ if name == :controller
+ value
+ elsif value.is_a?(Array)
+ value.map { |v| Rack::Mount::Utils.escape_uri(v.to_param) }.join('/')
+ else
+ return nil unless param = value.to_param
+ param.split('/').map { |v| Rack::Mount::Utils.escape_uri(v) }.join("/")
+ end
+ end
+ }
+
+ attr_reader :options, :recall, :set, :named_route
def initialize(options, recall, set, extras = false)
- @script_name = options.delete(:script_name)
@named_route = options.delete(:use_route)
@options = options.dup
@recall = recall.dup
@@ -392,36 +441,19 @@ module ActionDispatch
end
def generate
- path, params = @set.set.generate(:path_info, named_route, options, recall, opts)
+ path, params = @set.set.generate(:path_info, named_route, options, recall, PARAMETERIZE)
raise_routing_error unless path
- params.reject! {|k,v| !v }
-
return [path, params.keys] if @extras
- path << "?#{params.to_query}" if params.any?
- "#{script_name}#{path}"
+ [path, params]
rescue Rack::Mount::RoutingError
raise_routing_error
end
- def opts
- parameterize = lambda do |name, value|
- if name == :controller
- value
- elsif value.is_a?(Array)
- value.map { |v| Rack::Mount::Utils.escape_uri(v.to_param) }.join('/')
- else
- return nil unless param = value.to_param
- param.split('/').map { |v| Rack::Mount::Utils.escape_uri(v) }.join("/")
- end
- end
- {:parameterize => parameterize}
- end
-
def raise_routing_error
- raise ActionController::RoutingError.new("No route matches #{options.inspect}")
+ raise ActionController::RoutingError, "No route matches #{options.inspect}"
end
def different_controller?
@@ -453,7 +485,12 @@ module ActionDispatch
Generator.new(options, recall, self, extras).generate
end
- RESERVED_OPTIONS = [:anchor, :params, :only_path, :host, :protocol, :port, :trailing_slash]
+ RESERVED_OPTIONS = [:host, :protocol, :port, :subdomain, :domain, :tld_length,
+ :trailing_slash, :anchor, :params, :only_path, :script_name]
+
+ def _generate_prefix(options = {})
+ nil
+ end
def url_for(options)
finalize!
@@ -461,30 +498,24 @@ module ActionDispatch
handle_positional_args(options)
- rewritten_url = ""
-
- path_segments = options.delete(:_path_segments)
-
- unless options[:only_path]
- rewritten_url << (options[:protocol] || "http")
- rewritten_url << "://" unless rewritten_url.match("://")
- rewritten_url << rewrite_authentication(options)
-
- raise "Missing host to link to! Please provide :host parameter or set default_url_options[:host]" unless options[:host]
+ user, password = extract_authentication(options)
+ path_segments = options.delete(:_path_segments)
+ script_name = options.delete(:script_name)
- rewritten_url << options[:host]
- rewritten_url << ":#{options.delete(:port)}" if options.key?(:port)
- end
+ path = (script_name.blank? ? _generate_prefix(options) : script_name.chomp('/')).to_s
path_options = options.except(*RESERVED_OPTIONS)
path_options = yield(path_options) if block_given?
- path = generate(path_options, path_segments || {})
- # ROUTES TODO: This can be called directly, so script_name should probably be set in the routes
- rewritten_url << (options[:trailing_slash] ? path.sub(/\?|\z/) { "/" + $& } : path)
- rewritten_url << "##{Rack::Mount::Utils.escape_uri(options[:anchor].to_param.to_s)}" if options[:anchor]
+ path_addition, params = generate(path_options, path_segments || {})
+ path << path_addition
- rewritten_url
+ ActionDispatch::Http::URL.url_for(options.merge({
+ :path => path,
+ :params => params,
+ :user => user,
+ :password => password
+ }))
end
def call(env)
@@ -494,7 +525,7 @@ module ActionDispatch
def recognize_path(path, environment = {})
method = (environment[:method] || "GET").to_s.upcase
- path = Rack::Mount::Utils.normalize_path(path)
+ path = Rack::Mount::Utils.normalize_path(path) unless path =~ %r{://}
begin
env = Rack::MockRequest.env_for(path, {:method => method})
@@ -502,17 +533,19 @@ module ActionDispatch
raise ActionController::RoutingError, e.message
end
- req = Rack::Request.new(env)
+ req = @request_class.new(env)
@set.recognize(req) do |route, matches, params|
params.each do |key, value|
if value.is_a?(String)
value = value.dup.force_encoding(Encoding::BINARY) if value.encoding_aware?
- params[key] = URI.unescape(value)
+ params[key] = URI.parser.unescape(value)
end
end
dispatcher = route.app
- dispatcher = dispatcher.app while dispatcher.is_a?(Mapper::Constraints)
+ while dispatcher.is_a?(Mapper::Constraints) && dispatcher.matches?(env) do
+ dispatcher = dispatcher.app
+ end
if dispatcher.is_a?(Dispatcher) && dispatcher.controller(params, false)
dispatcher.prepare_params!(params)
@@ -524,28 +557,25 @@ module ActionDispatch
end
private
+
+ def extract_authentication(options)
+ if options[:user] && options[:password]
+ [options.delete(:user), options.delete(:password)]
+ else
+ nil
+ end
+ end
+
def handle_positional_args(options)
return unless args = options.delete(:_positional_args)
keys = options.delete(:_positional_keys)
keys -= options.keys if args.size < keys.size - 1 # take format into account
- args = args.zip(keys).inject({}) do |h, (v, k)|
- h[k] = v
- h
- end
-
# Tell url_for to skip default_url_options
- options.merge!(args)
+ options.merge!(Hash[args.zip(keys).map { |v, k| [k, v] }])
end
- def rewrite_authentication(options)
- if options[:user] && options[:password]
- "#{Rack::Utils.escape(options.delete(:user))}:#{Rack::Utils.escape(options.delete(:password))}@"
- else
- ""
- end
- end
end
end
end
diff --git a/actionpack/lib/action_dispatch/routing/routes_proxy.rb b/actionpack/lib/action_dispatch/routing/routes_proxy.rb
new file mode 100644
index 0000000000..f7d5f6397d
--- /dev/null
+++ b/actionpack/lib/action_dispatch/routing/routes_proxy.rb
@@ -0,0 +1,35 @@
+module ActionDispatch
+ module Routing
+ class RoutesProxy #:nodoc:
+ include ActionDispatch::Routing::UrlFor
+
+ attr_accessor :scope, :routes
+ alias :_routes :routes
+
+ def initialize(routes, scope)
+ @routes, @scope = routes, scope
+ end
+
+ def url_options
+ scope.send(:_with_routes, routes) do
+ scope.url_options
+ end
+ end
+
+ def method_missing(method, *args)
+ if routes.url_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)
+ end
+ RUBY
+ send(method, *args)
+ else
+ super
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb
index ba93ff8630..d4db78a25a 100644
--- a/actionpack/lib/action_dispatch/routing/url_for.rb
+++ b/actionpack/lib/action_dispatch/routing/url_for.rb
@@ -1,6 +1,6 @@
module ActionDispatch
module Routing
- # In <b>routes.rb</b> one defines URL-to-controller mappings, but the reverse
+ # 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.
# URL generation functionality is centralized in this module.
#
@@ -12,15 +12,14 @@ module ActionDispatch
#
# == URL generation from parameters
#
- # As you may know, some functions - such as ActionController::Base#url_for
+ # As you may know, some functions, such as ActionController::Base#url_for
# and ActionView::Helpers::UrlHelper#link_to, can generate URLs given a set
# of parameters. For example, you've probably had the chance to write code
# like this in one of your views:
#
# <%= link_to('Click here', :controller => 'users',
# :action => 'new', :message => 'Welcome!') %>
- #
- # # Generates a link to /users/new?message=Welcome%21
+ # # => "/users/new?message=Welcome%21"
#
# link_to, and all other functions that require URL generation functionality,
# actually use ActionController::UrlFor under the hood. And in particular,
@@ -61,7 +60,7 @@ module ActionDispatch
#
# UrlFor also allows one to access methods that have been auto-generated from
# named routes. For example, suppose that you have a 'users' resource in your
- # <b>routes.rb</b>:
+ # <tt>config/routes.rb</tt>:
#
# resources :users
#
@@ -99,6 +98,11 @@ module ActionDispatch
end
end
+ def initialize(*)
+ @_routes = nil
+ super
+ end
+
def url_options
default_url_options
end
@@ -111,6 +115,13 @@ module ActionDispatch
# * <tt>:host</tt> - Specifies the host the link should be targeted at.
# If <tt>:only_path</tt> is false, this option must be
# provided either explicitly, or via +default_url_options+.
+ # * <tt>:subdomain</tt> - Specifies the subdomain of the link, using the +tld_length+
+ # to split the domain from the host.
+ # * <tt>:domain</tt> - Specifies the domain of the link, using the +tld_length+
+ # to split the subdomain from the host.
+ # * <tt>:tld_length</tt> - Number of labels the TLD id composed of, only used if
+ # <tt>:subdomain</tt> or <tt>:domain</tt> are supplied. Defaults to
+ # <tt>ActionDispatch::Http::URL.tld_length</tt>, which in turn defaults to 1.
# * <tt>:port</tt> - Optionally specify the port to connect to.
# * <tt>:anchor</tt> - An anchor name to be appended to the path.
# * <tt>:trailing_slash</tt> - If true, adds a trailing slash, as in "/archive/2009/"
@@ -134,6 +145,18 @@ module ActionDispatch
polymorphic_url(options)
end
end
+
+ protected
+ def _with_routes(routes)
+ old_routes, @_routes = @_routes, routes
+ yield
+ ensure
+ @_routes = old_routes
+ end
+
+ def _routes_context
+ self
+ end
end
end
end
diff --git a/actionpack/lib/action_dispatch/testing/assertions/dom.rb b/actionpack/lib/action_dispatch/testing/assertions/dom.rb
index 9c215de743..47c84742aa 100644
--- a/actionpack/lib/action_dispatch/testing/assertions/dom.rb
+++ b/actionpack/lib/action_dispatch/testing/assertions/dom.rb
@@ -3,7 +3,7 @@ require 'action_controller/vendor/html-scanner'
module ActionDispatch
module Assertions
module DomAssertions
- # Test two HTML strings for equivalency (e.g., identical up to reordering of attributes)
+ # \Test two HTML strings for equivalency (e.g., identical up to reordering of attributes)
#
# ==== Examples
#
diff --git a/actionpack/lib/action_dispatch/testing/assertions/response.rb b/actionpack/lib/action_dispatch/testing/assertions/response.rb
index ec5e9efe44..77a15f3e97 100644
--- a/actionpack/lib/action_dispatch/testing/assertions/response.rb
+++ b/actionpack/lib/action_dispatch/testing/assertions/response.rb
@@ -1,6 +1,6 @@
module ActionDispatch
module Assertions
- # A small suite of assertions that test responses from Rails applications.
+ # A small suite of assertions that test responses from \Rails applications.
module ResponseAssertions
extend ActiveSupport::Concern
@@ -18,9 +18,9 @@ module ActionDispatch
# * <tt>:missing</tt> - Status code was 404
# * <tt>:error</tt> - Status code was in the 500-599 range
#
- # You can also pass an explicit status number like assert_response(501)
- # or its symbolic equivalent assert_response(:not_implemented).
- # See ActionDispatch::StatusCodes for a full list.
+ # You can also pass an explicit status number like <tt>assert_response(501)</tt>
+ # or its symbolic equivalent <tt>assert_response(:not_implemented)</tt>.
+ # See Rack::Utils::SYMBOL_TO_STATUS_CODE for a full list.
#
# ==== Examples
#
@@ -45,8 +45,8 @@ module ActionDispatch
end
# Assert that the redirection options passed in match those of the redirect called in the latest action.
- # This match can be partial, such that assert_redirected_to(:controller => "weblog") will also
- # match the redirection of redirect_to(:controller => "weblog", :action => "show") and so on.
+ # This match can be partial, such that <tt>assert_redirected_to(:controller => "weblog")</tt> will also
+ # match the redirection of <tt>redirect_to(:controller => "weblog", :action => "show")</tt> and so on.
#
# ==== Examples
#
@@ -81,14 +81,10 @@ module ActionDispatch
def normalize_argument_to_redirection(fragment)
case fragment
- when %r{^\w[\w\d+.-]*:.*}
+ when %r{^\w[A-Za-z\d+.-]*:.*}
fragment
when String
- if fragment =~ %r{^\w[\w\d+.-]*:.*}
- fragment
- else
- @request.protocol + @request.host_with_port + fragment
- end
+ @request.protocol + @request.host_with_port + fragment
when :back
raise RedirectBackError unless refer = @request.headers["Referer"]
refer
diff --git a/actionpack/lib/action_dispatch/testing/assertions/routing.rb b/actionpack/lib/action_dispatch/testing/assertions/routing.rb
index 9338fa9e70..11e8c63fa0 100644
--- a/actionpack/lib/action_dispatch/testing/assertions/routing.rb
+++ b/actionpack/lib/action_dispatch/testing/assertions/routing.rb
@@ -1,12 +1,13 @@
+require 'uri'
require 'active_support/core_ext/hash/diff'
require 'active_support/core_ext/hash/indifferent_access'
module ActionDispatch
module Assertions
- # Suite of assertions to test routes generated by Rails and the handling of requests made to them.
+ # Suite of assertions to test routes generated by \Rails and the handling of requests made to them.
module RoutingAssertions
# Asserts that the routing of the given +path+ was handled correctly and that the parsed options (given in the +expected_options+ hash)
- # match +path+. Basically, it asserts that Rails recognizes the route given by +expected_options+.
+ # match +path+. Basically, it asserts that \Rails recognizes the route given by +expected_options+.
#
# Pass a hash in the second argument (+path+) to specify the request method. This is useful for routes
# requiring a specific HTTP method. The hash should contain a :path with the incoming request path
@@ -36,18 +37,8 @@ module ActionDispatch
#
# # Test a custom route
# assert_recognizes({:controller => 'items', :action => 'show', :id => '1'}, 'view/item1')
- #
- # # Check a Simply RESTful generated route
- # assert_recognizes list_items_url, 'items/list'
def assert_recognizes(expected_options, path, extras={}, message=nil)
- if path.is_a? Hash
- request_method = path[:method]
- path = path[:path]
- else
- request_method = nil
- end
-
- request = recognized_request_for(path, request_method)
+ request = recognized_request_for(path)
expected_options = expected_options.clone
extras.each_key { |key| expected_options.delete key } unless extras.nil?
@@ -77,7 +68,16 @@ 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)
- expected_path = "/#{expected_path}" unless expected_path[0] == ?/
+ if expected_path =~ %r{://}
+ begin
+ uri = URI.parse(expected_path)
+ expected_path = uri.path.to_s.empty? ? "/" : uri.path
+ rescue URI::InvalidURIError => e
+ raise ActionController::RoutingError, e.message
+ end
+ else
+ expected_path = "/#{expected_path}" unless expected_path.first == '/'
+ end
# Load routes.rb if it hasn't been loaded.
generated_path, extra_keys = @routes.generate_extras(options, defaults)
@@ -121,7 +121,8 @@ module ActionDispatch
options[:controller] = "/#{controller}"
end
- assert_generates(path.is_a?(Hash) ? path[:path] : path, options, defaults, extras, message)
+ generate_options = options.dup.delete_if{ |k,v| defaults.key?(k) }
+ assert_generates(path.is_a?(Hash) ? path[:path] : path, generate_options, defaults, extras, message)
end
# A helper to make it easier to test different route configurations.
@@ -143,16 +144,16 @@ module ActionDispatch
#
def with_routing
old_routes, @routes = @routes, ActionDispatch::Routing::RouteSet.new
- old_controller, @controller = @controller, @controller.clone if @controller
- _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
- if @controller
+ if defined?(@controller) && @controller
+ 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.view_context_class = Class.new(@controller.view_context_class) do
include _routes.url_helpers
@@ -161,14 +162,14 @@ module ActionDispatch
yield @routes
ensure
@routes = old_routes
- if @controller
+ if defined?(@controller) && @controller
@controller = old_controller
end
end
# ROUTES TODO: These assertions should really work in an integration context
def method_missing(selector, *args, &block)
- if @controller && @routes && @routes.named_routes.helpers.include?(selector)
+ if defined?(@controller) && @controller && @routes && @routes.named_routes.helpers.include?(selector)
@controller.send(selector, *args, &block)
else
super
@@ -177,15 +178,35 @@ module ActionDispatch
private
# Recognizes the route for a given path.
- def recognized_request_for(path, request_method = nil)
- path = "/#{path}" unless path.first == '/'
+ def recognized_request_for(path)
+ if path.is_a?(Hash)
+ method = path[:method]
+ path = path[:path]
+ else
+ method = :get
+ end
# Assume given controller
request = ActionController::TestRequest.new
- request.env["REQUEST_METHOD"] = request_method.to_s.upcase if request_method
- request.path = path
- params = @routes.recognize_path(path, { :method => request.method })
+ if path =~ %r{://}
+ begin
+ uri = URI.parse(path)
+ request.env["rack.url_scheme"] = uri.scheme || "http"
+ request.host = uri.host if uri.host
+ request.port = uri.port if uri.port
+ request.path = uri.path.to_s.empty? ? "/" : uri.path
+ rescue URI::InvalidURIError => e
+ raise ActionController::RoutingError, e.message
+ end
+ else
+ path = "/#{path}" unless path.first == "/"
+ request.path = path
+ end
+
+ request.request_method = method if method
+
+ params = @routes.recognize_path(path, { :method => method })
request.path_parameters = params.with_indifferent_access
request
diff --git a/actionpack/lib/action_dispatch/testing/assertions/selector.rb b/actionpack/lib/action_dispatch/testing/assertions/selector.rb
index 2fc9e2b7d6..2b862fb7d6 100644
--- a/actionpack/lib/action_dispatch/testing/assertions/selector.rb
+++ b/actionpack/lib/action_dispatch/testing/assertions/selector.rb
@@ -24,10 +24,6 @@ module ActionDispatch
#
# Also see HTML::Selector to learn how to use selectors.
module SelectorAssertions
- # :call-seq:
- # css_select(selector) => array
- # css_select(element, selector) => array
- #
# Select and return all matching elements.
#
# If called with a single argument, uses that argument as a selector
@@ -71,7 +67,7 @@ module ActionDispatch
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 @selected
+ elsif defined?(@selected) && @selected
matches = []
@selected.each do |selected|
@@ -99,10 +95,6 @@ module ActionDispatch
selector.select(root)
end
- # :call-seq:
- # assert_select(selector, equality?, message?)
- # assert_select(element, selector, equality?, message?)
- #
# An assertion that selects elements and makes one or more equality tests.
#
# If the first argument is an element, selects all matching elements
@@ -195,6 +187,7 @@ module ActionDispatch
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),
@@ -332,11 +325,6 @@ module ActionDispatch
end
end
- # :call-seq:
- # assert_select_rjs(id?) { |elements| ... }
- # assert_select_rjs(statement, id?) { |elements| ... }
- # assert_select_rjs(:insert, position, id?) { |elements| ... }
- #
# Selects content from the RJS response.
#
# === Narrowing down
@@ -455,6 +443,7 @@ module ActionDispatch
assert_block("") { true } # to count the assertion
if block_given? && !([:remove, :show, :hide, :toggle].include? rjs_type)
begin
+ @selected ||= nil
in_scope, @selected = @selected, matches
yield matches
ensure
@@ -474,9 +463,6 @@ module ActionDispatch
end
end
- # :call-seq:
- # assert_select_encoded(element?) { |elements| ... }
- #
# Extracts the content of an element, treats it as encoded HTML and runs
# nested assertion on it.
#
@@ -529,8 +515,8 @@ module ActionDispatch
node.content.gsub(/<!\[CDATA\[(.*)(\]\]>)?/m) { Rack::Utils.escapeHTML($1) }
end
- selected = elements.map do |element|
- text = element.children.select{ |c| not c.tag? }.map{ |c| fix_content[c] }.join
+ selected = elements.map do |_element|
+ text = _element.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
@@ -543,9 +529,6 @@ module ActionDispatch
end
end
- # :call-seq:
- # assert_select_email { }
- #
# Extracts the body of an email and runs nested assertions on it.
#
# You must enable deliveries for this assertion to work, use:
diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb
index b52795c575..5c6416a19e 100644
--- a/actionpack/lib/action_dispatch/testing/integration.rb
+++ b/actionpack/lib/action_dispatch/testing/integration.rb
@@ -115,8 +115,8 @@ module ActionDispatch
end
end
- # An integration Session instance represents a set of requests and responses
- # performed sequentially by some virtual user. Because you can instantiate
+ # An instance of this class represents a set of requests and responses
+ # performed sequentially by a test process. Because you can instantiate
# multiple sessions and run them side-by-side, you can also mimic (to some
# limited extent) multiple simultaneous users interacting with your system.
#
@@ -171,6 +171,7 @@ module ActionDispatch
# Create and initialize a new Session instance.
def initialize(app)
+ super()
@app = app
# If the app is a Rails app, make url_helpers available on the session
@@ -182,6 +183,7 @@ module ActionDispatch
reset!
end
+ remove_method :default_url_options
def default_url_options
{ :host => host, :protocol => https? ? "https" : "http" }
end
@@ -233,9 +235,7 @@ module ActionDispatch
# Set the host name to use in the next request.
#
# session.host! "www.example.com"
- def host!(name)
- @host = name
- end
+ alias :host! :host=
private
def _mock_session
@@ -257,12 +257,14 @@ module ActionDispatch
end
end
+ hostname, port = host.split(':')
+
env = {
:method => method,
:params => parameters,
- "SERVER_NAME" => host.split(':')[0],
- "SERVER_PORT" => (https? ? "443" : "80"),
+ "SERVER_NAME" => hostname,
+ "SERVER_PORT" => port || (https? ? "443" : "80"),
"HTTPS" => https? ? "on" : "off",
"rack.url_scheme" => https? ? "https" : "http",
@@ -305,7 +307,7 @@ module ActionDispatch
include ActionDispatch::Assertions
def app
- @app
+ @app ||= nil
end
# Reset the current session. This is useful for testing multiple sessions
@@ -317,10 +319,10 @@ module ActionDispatch
%w(get post 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! unless integration_session
# reset the html_document variable, but only for new get/post calls
@html_document = nil unless %w(cookies assigns).include?(method)
- @integration_session.__send__(method, *args).tap do
+ integration_session.__send__(method, *args).tap do
copy_session_variables!
end
end
@@ -345,7 +347,7 @@ module ActionDispatch
# Copy the instance variables from the current session instance into the
# test instance.
def copy_session_variables! #:nodoc:
- return unless @integration_session
+ return unless integration_session
%w(controller response request).each do |var|
instance_variable_set("@#{var}", @integration_session.__send__(var))
end
@@ -355,35 +357,44 @@ module ActionDispatch
include ActionDispatch::Routing::UrlFor
def url_options
- reset! unless @integration_session
- @integration_session.url_options
+ reset! unless integration_session
+ integration_session.url_options
+ end
+
+ def respond_to?(method, include_private = false)
+ integration_session.respond_to?(method, include_private) || super
end
# 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
+ reset! unless integration_session
+ if integration_session.respond_to?(sym)
+ integration_session.__send__(sym, *args, &block).tap do
copy_session_variables!
end
else
super
end
end
+
+ private
+ def integration_session
+ @integration_session ||= nil
+ end
end
end
- # An IntegrationTest is one that spans multiple controllers and actions,
+ # An test that spans multiple controllers and actions,
# tying them all together to ensure they work together as expected. It tests
# more completely than either unit or functional tests do, exercising the
# entire stack, from the dispatcher to the database.
#
- # At its simplest, you simply extend IntegrationTest and write your tests
+ # At its simplest, you simply extend <tt>IntegrationTest</tt> and write your tests
# using the get/post methods:
#
# require "test_helper"
#
- # class ExampleTest < ActionController::IntegrationTest
+ # class ExampleTest < ActionDispatch::IntegrationTest
# fixtures :people
#
# def test_login
@@ -403,11 +414,11 @@ module ActionDispatch
# However, you can also have multiple session instances open per test, and
# even extend those instances with assertions and methods to create a very
# powerful testing DSL that is specific for your application. You can even
- # reference any named routes you happen to have defined!
+ # reference any named routes you happen to have defined.
#
# require "test_helper"
#
- # class AdvancedTest < ActionController::IntegrationTest
+ # class AdvancedTest < ActionDispatch::IntegrationTest
# fixtures :people, :rooms
#
# def test_login_and_speak
diff --git a/actionpack/lib/action_dispatch/testing/performance_test.rb b/actionpack/lib/action_dispatch/testing/performance_test.rb
index 33a5c68b9d..e7aeb45fb3 100644
--- a/actionpack/lib/action_dispatch/testing/performance_test.rb
+++ b/actionpack/lib/action_dispatch/testing/performance_test.rb
@@ -1,5 +1,4 @@
require 'active_support/testing/performance'
-require 'active_support/testing/default'
begin
module ActionDispatch
@@ -11,9 +10,8 @@ begin
# formats are written, so you'll have two output files per test method.
class PerformanceTest < ActionDispatch::IntegrationTest
include ActiveSupport::Testing::Performance
- include ActiveSupport::Testing::Default
end
end
rescue NameError
$stderr.puts "Specify ruby-prof as application's dependency in Gemfile to run benchmarks."
-end \ No newline at end of file
+end
diff --git a/actionpack/lib/action_dispatch/testing/test_process.rb b/actionpack/lib/action_dispatch/testing/test_process.rb
index c56ebc6438..d430691429 100644
--- a/actionpack/lib/action_dispatch/testing/test_process.rb
+++ b/actionpack/lib/action_dispatch/testing/test_process.rb
@@ -22,7 +22,7 @@ module ActionDispatch
end
def cookies
- @request.cookies.merge(@response.cookies)
+ @request.cookies.merge(@response.cookies).with_indifferent_access
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 b3e67f6e36..822adb6a47 100644
--- a/actionpack/lib/action_dispatch/testing/test_request.rb
+++ b/actionpack/lib/action_dispatch/testing/test_request.rb
@@ -1,5 +1,6 @@
require 'active_support/core_ext/object/blank'
require 'active_support/core_ext/hash/reverse_merge'
+require 'rack/utils'
module ActionDispatch
class TestRequest < Request
@@ -10,9 +11,10 @@ module ActionDispatch
end
def initialize(env = {})
- env = Rails.application.env_defaults.merge(env) if defined?(Rails.application)
+ env = Rails.application.env_config.merge(env) if defined?(Rails.application)
super(DEFAULT_ENV.merge(env))
+ @cookies = nil
self.host = 'test.host'
self.remote_addr = '0.0.0.0'
self.user_agent = 'Rails Testing'
@@ -66,7 +68,7 @@ module ActionDispatch
def accept=(mime_types)
@env.delete('action_dispatch.request.accepts')
- @env['HTTP_ACCEPT'] = Array(mime_types).collect { |mime_types| mime_types.to_s }.join(",")
+ @env['HTTP_ACCEPT'] = Array(mime_types).collect { |mime_type| mime_type.to_s }.join(",")
end
def cookies
@@ -76,10 +78,14 @@ module ActionDispatch
private
def write_cookies!
unless @cookies.blank?
- @env['HTTP_COOKIE'] = @cookies.map { |name, value| "#{name}=#{value};" }.join(' ')
+ @env['HTTP_COOKIE'] = @cookies.map { |name, value| escape_cookie(name, value) }.join('; ')
end
end
+ def escape_cookie(name, value)
+ "#{Rack::Utils.escape(name)}=#{Rack::Utils.escape(value)}"
+ end
+
def delete_nil_values!
@env.delete_if { |k, v| v.nil? }
end
diff --git a/actionpack/lib/action_dispatch/testing/test_response.rb b/actionpack/lib/action_dispatch/testing/test_response.rb
index 44fb1bde99..82039e72e7 100644
--- a/actionpack/lib/action_dispatch/testing/test_response.rb
+++ b/actionpack/lib/action_dispatch/testing/test_response.rb
@@ -14,123 +14,16 @@ module ActionDispatch
end
end
- module DeprecatedHelpers
- def template
- ActiveSupport::Deprecation.warn("response.template has been deprecated. Use controller.template instead", caller)
- @template
- end
- attr_writer :template
-
- def session
- ActiveSupport::Deprecation.warn("response.session has been deprecated. Use request.session instead", caller)
- @request.session
- end
-
- def assigns
- ActiveSupport::Deprecation.warn("response.assigns has been deprecated. Use controller.assigns instead", caller)
- @template.controller.assigns
- end
-
- def layout
- ActiveSupport::Deprecation.warn("response.layout has been deprecated. Use template.layout instead", caller)
- @template.layout
- end
-
- def redirected_to
- ::ActiveSupport::Deprecation.warn("response.redirected_to is deprecated. Use response.redirect_url instead", caller)
- redirect_url
- end
-
- def redirect_url_match?(pattern)
- ::ActiveSupport::Deprecation.warn("response.redirect_url_match? is deprecated. Use assert_match(/foo/, response.redirect_url) instead", caller)
- return false if redirect_url.nil?
- p = Regexp.new(pattern) if pattern.class == String
- p = pattern if pattern.class == Regexp
- return false if p.nil?
- p.match(redirect_url) != nil
- end
-
- # Returns the template of the file which was used to
- # render this response (or nil)
- def rendered
- ActiveSupport::Deprecation.warn("response.rendered has been deprecated. Use template.rendered instead", caller)
- @template.instance_variable_get(:@_rendered)
- end
-
- # A shortcut to the flash. Returns an empty hash if no session flash exists.
- def flash
- ActiveSupport::Deprecation.warn("response.flash has been deprecated. Use request.flash instead", caller)
- request.session['flash'] || {}
- end
-
- # Do we have a flash?
- def has_flash?
- ActiveSupport::Deprecation.warn("response.has_flash? has been deprecated. Use flash.any? instead", caller)
- !flash.empty?
- end
-
- # Do we have a flash that has contents?
- def has_flash_with_contents?
- ActiveSupport::Deprecation.warn("response.has_flash_with_contents? has been deprecated. Use flash.any? instead", caller)
- !flash.empty?
- end
-
- # Does the specified flash object exist?
- def has_flash_object?(name=nil)
- ActiveSupport::Deprecation.warn("response.has_flash_object? has been deprecated. Use flash[name] instead", caller)
- !flash[name].nil?
- end
-
- # Does the specified object exist in the session?
- def has_session_object?(name=nil)
- ActiveSupport::Deprecation.warn("response.has_session_object? has been deprecated. Use session[name] instead", caller)
- !session[name].nil?
- end
-
- # A shortcut to the template.assigns
- def template_objects
- ActiveSupport::Deprecation.warn("response.template_objects has been deprecated. Use template.assigns instead", caller)
- @template.assigns || {}
- end
-
- # Does the specified template object exist?
- def has_template_object?(name=nil)
- ActiveSupport::Deprecation.warn("response.has_template_object? has been deprecated. Use tempate.assigns[name].nil? instead", caller)
- !template_objects[name].nil?
- end
-
- # Returns binary content (downloadable file), converted to a String
- def binary_content
- ActiveSupport::Deprecation.warn("response.binary_content has been deprecated. Use response.body instead", caller)
- body
- end
- end
- include DeprecatedHelpers
-
# Was the response successful?
- def success?
- (200..299).include?(response_code)
- end
+ alias_method :success?, :successful?
# Was the URL not found?
- def missing?
- response_code == 404
- end
+ alias_method :missing?, :not_found?
# Were we redirected?
- def redirect?
- (300..399).include?(response_code)
- end
+ alias_method :redirect?, :redirection?
# Was there a server-side error?
- def error?
- (500..599).include?(response_code)
- end
- alias_method :server_error?, :error?
-
- # Was there a client client?
- def client_error?
- (400..499).include?(response_code)
- end
+ alias_method :error?, :server_error?
end
end
diff --git a/actionpack/lib/action_pack.rb b/actionpack/lib/action_pack.rb
index 1a1497385a..914b13dbfb 100644
--- a/actionpack/lib/action_pack.rb
+++ b/actionpack/lib/action_pack.rb
@@ -1,5 +1,5 @@
#--
-# Copyright (c) 2004-2010 David Heinemeier Hansson
+# Copyright (c) 2004-2011 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/version.rb b/actionpack/lib/action_pack/version.rb
index 7eaf7d0534..170ceb299a 100644
--- a/actionpack/lib/action_pack/version.rb
+++ b/actionpack/lib/action_pack/version.rb
@@ -1,10 +1,10 @@
module ActionPack
module VERSION #:nodoc:
MAJOR = 3
- MINOR = 0
+ MINOR = 1
TINY = 0
- BUILD = "rc"
+ PRE = "beta"
- STRING = [MAJOR, MINOR, TINY, BUILD].join('.')
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
end
end
diff --git a/actionpack/lib/action_view.rb b/actionpack/lib/action_view.rb
index c0d7423682..60665387b6 100644
--- a/actionpack/lib/action_view.rb
+++ b/actionpack/lib/action_view.rb
@@ -1,5 +1,5 @@
#--
-# Copyright (c) 2004-2010 David Heinemeier Hansson
+# Copyright (c) 2004-2011 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@@ -21,9 +21,6 @@
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++
-activesupport_path = File.expand_path('../../../activesupport/lib', __FILE__)
-$:.unshift(activesupport_path) if File.directory?(activesupport_path) && !$:.include?(activesupport_path)
-
require 'active_support/ruby/shim'
require 'active_support/core_ext/class/attribute_accessors'
@@ -33,35 +30,43 @@ module ActionView
extend ActiveSupport::Autoload
eager_autoload do
+ autoload :Base
autoload :Context
- autoload :Template
autoload :Helpers
+ autoload :LookupContext
+ autoload :Partials
+ autoload :PathSet
+ autoload :Rendering
+ autoload :Template
+ autoload :TestCase
- autoload_under "render" do
- autoload :Layouts
- autoload :Partials
- autoload :Rendering
+ autoload_under "renderer" do
+ autoload :AbstractRenderer
+ autoload :PartialRenderer
+ autoload :TemplateRenderer
end
- autoload :Base
- autoload :LookupContext
- autoload :Resolver, 'action_view/template/resolver'
- autoload :PathResolver, 'action_view/template/resolver'
- autoload :FileSystemResolver, 'action_view/template/resolver'
- autoload :PathSet, 'action_view/paths'
+ autoload_at "action_view/template/resolver" do
+ autoload :Resolver
+ autoload :PathResolver
+ autoload :FileSystemResolver
+ autoload :FallbackFileSystemResolver
+ end
- autoload :MissingTemplate, 'action_view/template/error'
- autoload :ActionViewError, 'action_view/template/error'
- autoload :EncodingError, 'action_view/template/error'
- autoload :TemplateError, 'action_view/template/error'
- autoload :WrongEncodingError, 'action_view/template/error'
+ autoload_at "action_view/template/error" do
+ autoload :MissingTemplate
+ autoload :ActionViewError
+ autoload :EncodingError
+ autoload :TemplateError
+ autoload :WrongEncodingError
+ end
- autoload :TemplateHandler, 'action_view/template'
- autoload :TemplateHandlers, 'action_view/template'
+ autoload_at "action_view/template" do
+ autoload :TemplateHandler
+ autoload :TemplateHandlers
+ end
end
- autoload :TestCase, 'action_view/test_case'
-
ENCODING_FLAG = '#.*coding[:=]\s*(\S+)[ \t]*'
end
diff --git a/actionpack/lib/action_view/base.rb b/actionpack/lib/action_view/base.rb
index 20a2e7c1f0..ab8c6259c5 100644
--- a/actionpack/lib/action_view/base.rb
+++ b/actionpack/lib/action_view/base.rb
@@ -6,9 +6,6 @@ require 'active_support/ordered_options'
require 'action_view/log_subscriber'
module ActionView #:nodoc:
- class NonConcattingString < ActiveSupport::SafeBuffer
- end
-
# = Action View Base
#
# Action View templates can be written in three ways. If the template file has a <tt>.erb</tt> (or <tt>.rhtml</tt>) extension then it uses a mixture of ERb
@@ -21,7 +18,7 @@ module ActionView #:nodoc:
# following loop for names:
#
# <b>Names of all the people</b>
- # <% for person in @people %>
+ # <% @people.each do |person| %>
# Name: <%= person.name %><br/>
# <% end %>
#
@@ -79,8 +76,8 @@ module ActionView #:nodoc:
#
# === Template caching
#
- # By default, Rails will compile each template to a method in order to render it. When you alter a template, Rails will
- # check the file's modification time and recompile it.
+ # By default, Rails will compile each template to a method in order to render it. When you alter a template,
+ # Rails will check the file's modification time and recompile it in development mode.
#
# == Builder
#
@@ -156,12 +153,9 @@ module ActionView #:nodoc:
#
# This refreshes the sidebar, removes a person element and highlights the user list.
#
- # See the ActionView::Helpers::PrototypeHelper::GeneratorMethods documentation for more details.
+ # See the ActionView::Helpers::PrototypeHelper::JavaScriptGenerator::GeneratorMethods documentation for more details.
class Base
- module Subclasses
- end
-
- include Helpers, Rendering, Partials, Layouts, ::ERB::Util, Context
+ include Helpers, Rendering, Partials, ::ERB::Util, Context
# Specify whether RJS responses should be wrapped in a try/catch block
# that alert()s the caught exception (and then re-raises it).
@@ -178,24 +172,26 @@ module ActionView #:nodoc:
class << self
delegate :erb_trim_mode=, :to => 'ActionView::Template::Handlers::ERB'
delegate :logger, :to => 'ActionController::Base', :allow_nil => true
+
+ def cache_template_loading
+ ActionView::Resolver.caching?
+ end
+
+ def cache_template_loading=(value)
+ ActionView::Resolver.caching = value
+ end
end
- attr_accessor :base_path, :assigns, :template_extension, :lookup_context
- attr_internal :captures, :request, :controller, :template, :config
+ attr_accessor :_template
+ attr_internal :request, :controller, :config, :assigns, :lookup_context
- delegate :find_template, :template_exists?, :formats, :formats=, :locale, :locale=,
- :view_paths, :view_paths=, :with_fallbacks, :update_details, :with_layout_format, :to => :lookup_context
+ delegate :formats, :formats=, :locale, :locale=, :view_paths, :view_paths=, :to => :lookup_context
- delegate :request_forgery_protection_token, :template, :params, :session, :cookies, :response, :headers,
+ delegate :request_forgery_protection_token, :params, :session, :cookies, :response, :headers,
:flash, :action_name, :controller_name, :to => :controller
delegate :logger, :to => :controller, :allow_nil => true
- # TODO: HACK FOR RJS
- def view_context
- self
- end
-
def self.xss_safe? #:nodoc:
true
end
@@ -206,33 +202,40 @@ module ActionView #:nodoc:
end
def assign(new_assigns) # :nodoc:
- self.assigns = new_assigns.each { |key, value| instance_variable_set("@#{key}", value) }
+ @_assigns = new_assigns.each { |key, value| instance_variable_set("@#{key}", value) }
end
def initialize(lookup_context = nil, assigns_for_first_render = {}, controller = nil, formats = nil) #:nodoc:
assign(assigns_for_first_render)
- self.helpers = self.class.helpers || Module.new
-
- if @_controller = controller
- @_request = controller.request if controller.respond_to?(:request)
- end
-
- config = controller && controller.respond_to?(:config) ? controller.config : {}
- @_config = ActiveSupport::InheritableOptions.new(config)
+ self.helpers = Module.new unless self.class.helpers
+ @_config = {}
@_content_for = Hash.new { |h,k| h[k] = ActiveSupport::SafeBuffer.new }
@_virtual_path = nil
@output_buffer = nil
- @lookup_context = lookup_context.is_a?(ActionView::LookupContext) ?
+ if @_controller = controller
+ @_request = controller.request if controller.respond_to?(:request)
+ @_config = controller.config.inheritable_copy if controller.respond_to?(:config)
+ end
+
+ @_lookup_context = lookup_context.is_a?(ActionView::LookupContext) ?
lookup_context : ActionView::LookupContext.new(lookup_context)
- @lookup_context.formats = formats if formats
+ @_lookup_context.formats = formats if formats
+ end
+
+ def store_content_for(key, value)
+ @_content_for[key] = value
end
def controller_path
@controller_path ||= controller && controller.controller_path
end
+ def controller_prefixes
+ @controller_prefixes ||= controller && controller._prefixes
+ end
+
ActiveSupport.run_load_hooks(:action_view, self)
end
end
diff --git a/actionpack/lib/action_view/helpers.rb b/actionpack/lib/action_view/helpers.rb
index b7ffa345cc..d338ce616a 100644
--- a/actionpack/lib/action_view/helpers.rb
+++ b/actionpack/lib/action_view/helpers.rb
@@ -12,14 +12,13 @@ module ActionView #:nodoc:
autoload :CsrfHelper
autoload :DateHelper
autoload :DebugHelper
- autoload :DeprecatedBlockHelpers
autoload :FormHelper
autoload :FormOptionsHelper
autoload :FormTagHelper
autoload :JavaScriptHelper, "action_view/helpers/javascript_helper"
autoload :NumberHelper
autoload :PrototypeHelper
- autoload :RawOutputHelper
+ autoload :OutputSafetyHelper
autoload :RecordTagHelper
autoload :SanitizeHelper
autoload :ScriptaculousHelper
@@ -49,7 +48,7 @@ module ActionView #:nodoc:
include JavaScriptHelper
include NumberHelper
include PrototypeHelper
- include RawOutputHelper
+ include OutputSafetyHelper
include RecordTagHelper
include SanitizeHelper
include ScriptaculousHelper
diff --git a/actionpack/lib/action_view/helpers/active_model_helper.rb b/actionpack/lib/action_view/helpers/active_model_helper.rb
index 6bb0875bc3..96c3eec337 100644
--- a/actionpack/lib/action_view/helpers/active_model_helper.rb
+++ b/actionpack/lib/action_view/helpers/active_model_helper.rb
@@ -7,25 +7,6 @@ module ActionView
# = Active Model Helpers
module Helpers
module ActiveModelHelper
- %w(input form error_messages_for error_message_on).each do |method|
- class_eval <<-RUBY, __FILE__, __LINE__ + 1
- def #{method}(*args)
- ActiveSupport::Deprecation.warn "#{method} was removed from Rails and is now available as a plugin. " <<
- "Please install it with `rails plugin install git://github.com/rails/dynamic_form.git`.", caller
- end
- RUBY
- end
- end
-
- module ActiveModelFormBuilder
- %w(error_messages error_message_on).each do |method|
- class_eval <<-RUBY, __FILE__, __LINE__ + 1
- def #{method}(*args)
- ActiveSupport::Deprecation.warn "f.#{method} was removed from Rails and is now available as a plugin. " <<
- "Please install it with `rails plugin install git://github.com/rails/dynamic_form.git`.", caller
- end
- RUBY
- end
end
module ActiveModelInstanceTag
@@ -67,10 +48,6 @@ module ActionView
end
end
- class FormBuilder
- include ActiveModelFormBuilder
- end
-
class InstanceTag
include ActiveModelInstanceTag
end
diff --git a/actionpack/lib/action_view/helpers/asset_tag_helper.rb b/actionpack/lib/action_view/helpers/asset_tag_helper.rb
index a3c43d3e93..f6b2d4f3f4 100644
--- a/actionpack/lib/action_view/helpers/asset_tag_helper.rb
+++ b/actionpack/lib/action_view/helpers/asset_tag_helper.rb
@@ -1,9 +1,6 @@
-require 'thread'
-require 'cgi'
-require 'action_view/helpers/url_helper'
-require 'action_view/helpers/tag_helper'
-require 'active_support/core_ext/file'
-require 'active_support/core_ext/object/blank'
+require 'action_view/helpers/asset_tag_helpers/javascript_tag_helpers'
+require 'action_view/helpers/asset_tag_helpers/stylesheet_tag_helpers'
+require 'action_view/helpers/asset_tag_helpers/asset_paths'
module ActionView
# = Action View Asset Tag Helpers
@@ -152,7 +149,7 @@ module ActionView
#
# # Normally you'd calculate RELEASE_NUMBER at startup.
# RELEASE_NUMBER = 12345
- # config.action_controller.asset_path_template = proc { |asset_path|
+ # config.action_controller.asset_path = proc { |asset_path|
# "/release-#{RELEASE_NUMBER}#{asset_path}"
# }
#
@@ -194,20 +191,8 @@ module ActionView
# RewriteEngine On
# RewriteRule ^/release-\d+/(images|javascripts|stylesheets)/(.*)$ /$1/$2 [L]
module AssetTagHelper
- mattr_reader :javascript_expansions
- @@javascript_expansions = { }
-
- mattr_reader :stylesheet_expansions
- @@stylesheet_expansions = {}
-
- # You can enable or disable the asset tag timestamps cache.
- # With the cache enabled, the asset tag helper methods will make fewer
- # expensive file system calls. However this prevents you from modifying
- # any asset files while the server is running.
- #
- # ActionView::Helpers::AssetTagHelper.cache_asset_timestamps = false
- mattr_accessor :cache_asset_timestamps
-
+ include JavascriptTagHelpers
+ include StylesheetTagHelpers
# Returns a link tag that browsers and news readers can use to auto-detect
# an RSS or ATOM feed. The +type+ can either be <tt>:rss</tt> (default) or
# <tt>:atom</tt>. Control the link options in url_for format using the
@@ -241,263 +226,6 @@ module ActionView
)
end
- # Computes the path to a javascript asset in the public javascripts directory.
- # If the +source+ filename has no extension, .js will be appended (except for explicit URIs)
- # Full paths from the document root will be passed through.
- # Used internally by javascript_include_tag to build the script path.
- #
- # ==== Examples
- # javascript_path "xmlhr" # => /javascripts/xmlhr.js
- # javascript_path "dir/xmlhr.js" # => /javascripts/dir/xmlhr.js
- # javascript_path "/dir/xmlhr" # => /dir/xmlhr.js
- # javascript_path "http://www.railsapplication.com/js/xmlhr" # => http://www.railsapplication.com/js/xmlhr
- # javascript_path "http://www.railsapplication.com/js/xmlhr.js" # => http://www.railsapplication.com/js/xmlhr.js
- def javascript_path(source)
- compute_public_path(source, 'javascripts', 'js')
- end
- alias_method :path_to_javascript, :javascript_path # aliased to avoid conflicts with a javascript_path named route
-
- # Returns an html script tag for each of the +sources+ provided. You
- # can pass in the filename (.js extension is optional) of javascript files
- # that exist in your public/javascripts directory for inclusion into the
- # current page or you can pass the full path relative to your document
- # root. To include the Prototype and Scriptaculous javascript libraries in
- # your application, pass <tt>:defaults</tt> as the source. When using
- # <tt>:defaults</tt>, if an application.js file exists in your public
- # javascripts directory, it will be included as well. You can modify the
- # html attributes of the script tag by passing a hash as the last argument.
- #
- # ==== Examples
- # javascript_include_tag "xmlhr" # =>
- # <script type="text/javascript" src="/javascripts/xmlhr.js"></script>
- #
- # javascript_include_tag "xmlhr.js" # =>
- # <script type="text/javascript" src="/javascripts/xmlhr.js"></script>
- #
- # javascript_include_tag "common.javascript", "/elsewhere/cools" # =>
- # <script type="text/javascript" src="/javascripts/common.javascript"></script>
- # <script type="text/javascript" src="/elsewhere/cools.js"></script>
- #
- # javascript_include_tag "http://www.railsapplication.com/xmlhr" # =>
- # <script type="text/javascript" src="http://www.railsapplication.com/xmlhr.js"></script>
- #
- # javascript_include_tag "http://www.railsapplication.com/xmlhr.js" # =>
- # <script type="text/javascript" src="http://www.railsapplication.com/xmlhr.js"></script>
- #
- # javascript_include_tag :defaults # =>
- # <script type="text/javascript" src="/javascripts/prototype.js"></script>
- # <script type="text/javascript" src="/javascripts/effects.js"></script>
- # ...
- # <script type="text/javascript" src="/javascripts/application.js"></script>
- #
- # * = The application.js file is only referenced if it exists
- #
- # Though it's not really recommended practice, if you need to extend the default JavaScript set for any reason
- # (e.g., you're going to be using a certain .js file in every action), then take a look at the register_javascript_include_default method.
- #
- # You can also include all javascripts in the javascripts directory using <tt>:all</tt> as the source:
- #
- # javascript_include_tag :all # =>
- # <script type="text/javascript" src="/javascripts/prototype.js"></script>
- # <script type="text/javascript" src="/javascripts/effects.js"></script>
- # ...
- # <script type="text/javascript" src="/javascripts/application.js"></script>
- # <script type="text/javascript" src="/javascripts/shop.js"></script>
- # <script type="text/javascript" src="/javascripts/checkout.js"></script>
- #
- # Note that the default javascript files will be included first. So Prototype and Scriptaculous are available to
- # all subsequently included files.
- #
- # If you want Rails to search in all the subdirectories under javascripts, you should explicitly set <tt>:recursive</tt>:
- #
- # javascript_include_tag :all, :recursive => true
- #
- # == Caching multiple javascripts into one
- #
- # You can also cache multiple javascripts into one file, which requires less HTTP connections to download and can better be
- # compressed by gzip (leading to faster transfers). Caching will only happen if config.perform_caching
- # is set to <tt>true</tt> (which is the case by default for the Rails production environment, but not for the development
- # environment).
- #
- # ==== Examples
- # javascript_include_tag :all, :cache => true # when config.perform_caching is false =>
- # <script type="text/javascript" src="/javascripts/prototype.js"></script>
- # <script type="text/javascript" src="/javascripts/effects.js"></script>
- # ...
- # <script type="text/javascript" src="/javascripts/application.js"></script>
- # <script type="text/javascript" src="/javascripts/shop.js"></script>
- # <script type="text/javascript" src="/javascripts/checkout.js"></script>
- #
- # javascript_include_tag :all, :cache => true # when config.perform_caching is true =>
- # <script type="text/javascript" src="/javascripts/all.js"></script>
- #
- # javascript_include_tag "prototype", "cart", "checkout", :cache => "shop" # when config.perform_caching is false =>
- # <script type="text/javascript" src="/javascripts/prototype.js"></script>
- # <script type="text/javascript" src="/javascripts/cart.js"></script>
- # <script type="text/javascript" src="/javascripts/checkout.js"></script>
- #
- # javascript_include_tag "prototype", "cart", "checkout", :cache => "shop" # when config.perform_caching is true =>
- # <script type="text/javascript" src="/javascripts/shop.js"></script>
- #
- # The <tt>:recursive</tt> option is also available for caching:
- #
- # javascript_include_tag :all, :cache => true, :recursive => true
- def javascript_include_tag(*sources)
- options = sources.extract_options!.stringify_keys
- concat = options.delete("concat")
- cache = concat || options.delete("cache")
- recursive = options.delete("recursive")
-
- if concat || (config.perform_caching && cache)
- joined_javascript_name = (cache == true ? "all" : cache) + ".js"
- joined_javascript_path = File.join(joined_javascript_name[/^#{File::SEPARATOR}/] ? config.assets_dir : config.javascripts_dir, joined_javascript_name)
-
- unless config.perform_caching && File.exists?(joined_javascript_path)
- write_asset_file_contents(joined_javascript_path, compute_javascript_paths(sources, recursive))
- end
- javascript_src_tag(joined_javascript_name, options)
- else
- sources = expand_javascript_sources(sources, recursive)
- ensure_javascript_sources!(sources) if cache
- sources.collect { |source| javascript_src_tag(source, options) }.join("\n").html_safe
- end
- end
-
- # Register one or more javascript files to be included when <tt>symbol</tt>
- # is passed to <tt>javascript_include_tag</tt>. This method is typically intended
- # to be called from plugin initialization to register javascript files
- # that the plugin installed in <tt>public/javascripts</tt>.
- #
- # ActionView::Helpers::AssetTagHelper.register_javascript_expansion :monkey => ["head", "body", "tail"]
- #
- # javascript_include_tag :monkey # =>
- # <script type="text/javascript" src="/javascripts/head.js"></script>
- # <script type="text/javascript" src="/javascripts/body.js"></script>
- # <script type="text/javascript" src="/javascripts/tail.js"></script>
- def self.register_javascript_expansion(expansions)
- @@javascript_expansions.merge!(expansions)
- end
-
- # Register one or more stylesheet files to be included when <tt>symbol</tt>
- # is passed to <tt>stylesheet_link_tag</tt>. This method is typically intended
- # to be called from plugin initialization to register stylesheet files
- # that the plugin installed in <tt>public/stylesheets</tt>.
- #
- # ActionView::Helpers::AssetTagHelper.register_stylesheet_expansion :monkey => ["head", "body", "tail"]
- #
- # stylesheet_link_tag :monkey # =>
- # <link href="/stylesheets/head.css" media="screen" rel="stylesheet" type="text/css" />
- # <link href="/stylesheets/body.css" media="screen" rel="stylesheet" type="text/css" />
- # <link href="/stylesheets/tail.css" media="screen" rel="stylesheet" type="text/css" />
- def self.register_stylesheet_expansion(expansions)
- @@stylesheet_expansions.merge!(expansions)
- end
-
- # Computes the path to a stylesheet asset in the public stylesheets directory.
- # If the +source+ filename has no extension, <tt>.css</tt> will be appended (except for explicit URIs).
- # Full paths from the document root will be passed through.
- # Used internally by +stylesheet_link_tag+ to build the stylesheet path.
- #
- # ==== Examples
- # stylesheet_path "style" # => /stylesheets/style.css
- # stylesheet_path "dir/style.css" # => /stylesheets/dir/style.css
- # stylesheet_path "/dir/style.css" # => /dir/style.css
- # stylesheet_path "http://www.railsapplication.com/css/style" # => http://www.railsapplication.com/css/style
- # stylesheet_path "http://www.railsapplication.com/css/style.css" # => http://www.railsapplication.com/css/style.css
- def stylesheet_path(source)
- compute_public_path(source, 'stylesheets', 'css')
- end
- alias_method :path_to_stylesheet, :stylesheet_path # aliased to avoid conflicts with a stylesheet_path named route
-
- # Returns a stylesheet link tag for the sources specified as arguments. If
- # you don't specify an extension, <tt>.css</tt> will be appended automatically.
- # You can modify the link attributes by passing a hash as the last argument.
- #
- # ==== Examples
- # stylesheet_link_tag "style" # =>
- # <link href="/stylesheets/style.css" media="screen" rel="stylesheet" type="text/css" />
- #
- # stylesheet_link_tag "style.css" # =>
- # <link href="/stylesheets/style.css" media="screen" rel="stylesheet" type="text/css" />
- #
- # stylesheet_link_tag "http://www.railsapplication.com/style.css" # =>
- # <link href="http://www.railsapplication.com/style.css" media="screen" rel="stylesheet" type="text/css" />
- #
- # stylesheet_link_tag "style", :media => "all" # =>
- # <link href="/stylesheets/style.css" media="all" rel="stylesheet" type="text/css" />
- #
- # stylesheet_link_tag "style", :media => "print" # =>
- # <link href="/stylesheets/style.css" media="print" rel="stylesheet" type="text/css" />
- #
- # stylesheet_link_tag "random.styles", "/css/stylish" # =>
- # <link href="/stylesheets/random.styles" media="screen" rel="stylesheet" type="text/css" />
- # <link href="/css/stylish.css" media="screen" rel="stylesheet" type="text/css" />
- #
- # You can also include all styles in the stylesheets directory using <tt>:all</tt> as the source:
- #
- # stylesheet_link_tag :all # =>
- # <link href="/stylesheets/style1.css" media="screen" rel="stylesheet" type="text/css" />
- # <link href="/stylesheets/styleB.css" media="screen" rel="stylesheet" type="text/css" />
- # <link href="/stylesheets/styleX2.css" media="screen" rel="stylesheet" type="text/css" />
- #
- # If you want Rails to search in all the subdirectories under stylesheets, you should explicitly set <tt>:recursive</tt>:
- #
- # stylesheet_link_tag :all, :recursive => true
- #
- # == Caching multiple stylesheets into one
- #
- # You can also cache multiple stylesheets into one file, which requires less HTTP connections and can better be
- # compressed by gzip (leading to faster transfers). Caching will only happen if config.perform_caching
- # is set to true (which is the case by default for the Rails production environment, but not for the development
- # environment). Examples:
- #
- # ==== Examples
- # stylesheet_link_tag :all, :cache => true # when config.perform_caching is false =>
- # <link href="/stylesheets/style1.css" media="screen" rel="stylesheet" type="text/css" />
- # <link href="/stylesheets/styleB.css" media="screen" rel="stylesheet" type="text/css" />
- # <link href="/stylesheets/styleX2.css" media="screen" rel="stylesheet" type="text/css" />
- #
- # stylesheet_link_tag :all, :cache => true # when config.perform_caching is true =>
- # <link href="/stylesheets/all.css" media="screen" rel="stylesheet" type="text/css" />
- #
- # stylesheet_link_tag "shop", "cart", "checkout", :cache => "payment" # when config.perform_caching is false =>
- # <link href="/stylesheets/shop.css" media="screen" rel="stylesheet" type="text/css" />
- # <link href="/stylesheets/cart.css" media="screen" rel="stylesheet" type="text/css" />
- # <link href="/stylesheets/checkout.css" media="screen" rel="stylesheet" type="text/css" />
- #
- # stylesheet_link_tag "shop", "cart", "checkout", :cache => "payment" # when config.perform_caching is true =>
- # <link href="/stylesheets/payment.css" media="screen" rel="stylesheet" type="text/css" />
- #
- # The <tt>:recursive</tt> option is also available for caching:
- #
- # stylesheet_link_tag :all, :cache => true, :recursive => true
- #
- # To force concatenation (even in development mode) set <tt>:concat</tt> to true. This is useful if
- # you have too many stylesheets for IE to load.
- #
- # stylesheet_link_tag :all, :concat => true
- #
- def stylesheet_link_tag(*sources)
- options = sources.extract_options!.stringify_keys
- concat = options.delete("concat")
- cache = concat || options.delete("cache")
- recursive = options.delete("recursive")
-
- if concat || (config.perform_caching && cache)
- joined_stylesheet_name = (cache == true ? "all" : cache) + ".css"
- joined_stylesheet_path = File.join(joined_stylesheet_name[/^#{File::SEPARATOR}/] ? config.assets_dir : config.stylesheets_dir, joined_stylesheet_name)
-
- unless config.perform_caching && File.exists?(joined_stylesheet_path)
- write_asset_file_contents(joined_stylesheet_path, compute_stylesheet_paths(sources, recursive))
- end
- stylesheet_tag(joined_stylesheet_name, options)
- else
- sources = expand_stylesheet_sources(sources, recursive)
- ensure_stylesheet_sources!(sources) if cache
- sources.collect { |source| stylesheet_tag(source, options) }.join("\n").html_safe
- end
- end
-
# Web browsers cache favicons. If you just throw a <tt>favicon.ico</tt> into the document
# root of your application and it changes later, clients that have it in their cache
# won't see the update. Using this helper prevents that because it appends an asset ID:
@@ -546,7 +274,7 @@ module ActionView
# The alias +path_to_image+ is provided to avoid that. Rails uses the alias internally, and
# plugin authors are encouraged to do so.
def image_path(source)
- compute_public_path(source, 'images')
+ asset_paths.compute_public_path(source, 'images')
end
alias_method :path_to_image, :image_path # aliased to avoid conflicts with an image_path named route
@@ -561,7 +289,7 @@ module ActionView
# video_path("/trailers/hd.avi") # => /trailers/hd.avi
# video_path("http://www.railsapplication.com/vid/hd.avi") # => http://www.railsapplication.com/vid/hd.avi
def video_path(source)
- compute_public_path(source, 'videos')
+ asset_paths.compute_public_path(source, 'videos')
end
alias_method :path_to_video, :video_path # aliased to avoid conflicts with a video_path named route
@@ -571,12 +299,12 @@ module ActionView
#
# ==== Examples
# audio_path("horse") # => /audios/horse
- # audio_path("horse.wav") # => /audios/horse.avi
- # audio_path("sounds/horse.wav") # => /audios/sounds/horse.avi
- # audio_path("/sounds/horse.wav") # => /sounds/horse.avi
+ # audio_path("horse.wav") # => /audios/horse.wav
+ # audio_path("sounds/horse.wav") # => /audios/sounds/horse.wav
+ # audio_path("/sounds/horse.wav") # => /sounds/horse.wav
# audio_path("http://www.railsapplication.com/sounds/horse.wav") # => http://www.railsapplication.com/sounds/horse.wav
def audio_path(source)
- compute_public_path(source, 'audios')
+ asset_paths.compute_public_path(source, 'audios')
end
alias_method :path_to_audio, :audio_path # aliased to avoid conflicts with an audio_path named route
@@ -705,202 +433,8 @@ module ActionView
private
- def rewrite_extension?(source, dir, ext)
- source_ext = File.extname(source)[1..-1]
- ext && (source_ext.blank? || (ext != source_ext && File.exist?(File.join(config.assets_dir, dir, "#{source}.#{ext}"))))
- end
-
- def rewrite_host_and_protocol(source, has_request)
- host = compute_asset_host(source)
- if has_request && host.present? && !is_uri?(host)
- host = "#{controller.request.protocol}#{host}"
- end
- "#{host}#{source}"
- end
-
- # Add the the extension +ext+ if not present. Return full URLs otherwise untouched.
- # Prefix with <tt>/dir/</tt> if lacking a leading +/+. Account for relative URL
- # roots. Rewrite the asset path for cache-busting asset ids. Include
- # asset host, if configured, with the correct request protocol.
- def compute_public_path(source, dir, ext = nil, include_host = true)
- return source if is_uri?(source)
-
- source += ".#{ext}" if rewrite_extension?(source, dir, ext)
- source = "/#{dir}/#{source}" unless source[0] == ?/
- source = rewrite_asset_path(source, config.asset_path)
-
- has_request = controller.respond_to?(:request)
- if has_request && include_host && source !~ %r{^#{controller.config.relative_url_root}/}
- source = "#{controller.config.relative_url_root}#{source}"
- end
- source = rewrite_host_and_protocol(source, has_request) if include_host
-
- source
- end
-
- def is_uri?(path)
- path =~ %r{^[-a-z]+://|^cid:}
- end
-
- # Pick an asset host for this source. Returns +nil+ if no host is set,
- # the host if no wildcard is set, the host interpolated with the
- # numbers 0-3 if it contains <tt>%d</tt> (the number is the source hash mod 4),
- # or the value returned from invoking the proc if it's a proc or the value from
- # invoking call if it's an object responding to call.
- def compute_asset_host(source)
- if host = config.asset_host
- if host.is_a?(Proc) || host.respond_to?(:call)
- case host.is_a?(Proc) ? host.arity : host.method(:call).arity
- when 2
- request = controller.respond_to?(:request) && controller.request
- host.call(source, request)
- else
- host.call(source)
- end
- else
- (host =~ /%d/) ? host % (source.hash % 4) : host
- end
- end
- end
-
- @@asset_timestamps_cache = {}
- @@asset_timestamps_cache_guard = Mutex.new
-
- # Use the RAILS_ASSET_ID environment variable or the source's
- # modification time as its cache-busting asset id.
- def rails_asset_id(source)
- if asset_id = ENV["RAILS_ASSET_ID"]
- asset_id
- else
- if @@cache_asset_timestamps && (asset_id = @@asset_timestamps_cache[source])
- asset_id
- else
- path = File.join(config.assets_dir, source)
- asset_id = File.exist?(path) ? File.mtime(path).to_i.to_s : ''
-
- if @@cache_asset_timestamps
- @@asset_timestamps_cache_guard.synchronize do
- @@asset_timestamps_cache[source] = asset_id
- end
- end
-
- asset_id
- end
- end
- end
-
- # Break out the asset path rewrite in case plugins wish to put the asset id
- # someplace other than the query string.
- def rewrite_asset_path(source, path = nil)
- if path && path.respond_to?(:call)
- return path.call(source)
- elsif path && path.is_a?(String)
- return path % [source]
- end
-
- asset_id = rails_asset_id(source)
- if asset_id.blank?
- source
- else
- source + "?#{asset_id}"
- end
- end
-
- def javascript_src_tag(source, options)
- content_tag("script", "", { "type" => Mime::JS, "src" => path_to_javascript(source) }.merge(options))
- end
-
- def stylesheet_tag(source, options)
- tag("link", { "rel" => "stylesheet", "type" => Mime::CSS, "media" => "screen", "href" => html_escape(path_to_stylesheet(source)) }.merge(options), false, false)
- end
-
- def compute_javascript_paths(*args)
- expand_javascript_sources(*args).collect { |source| compute_public_path(source, 'javascripts', 'js', false) }
- end
-
- def compute_stylesheet_paths(*args)
- expand_stylesheet_sources(*args).collect { |source| compute_public_path(source, 'stylesheets', 'css', false) }
- end
-
- def expand_javascript_sources(sources, recursive = false)
- if sources.include?(:all)
- all_javascript_files = collect_asset_files(config.javascripts_dir, ('**' if recursive), '*.js')
- ((determine_source(:defaults, @@javascript_expansions).dup & all_javascript_files) + all_javascript_files).uniq
- else
- expanded_sources = sources.collect do |source|
- determine_source(source, @@javascript_expansions)
- end.flatten
- expanded_sources << "application" if sources.include?(:defaults) && File.exist?(File.join(config.javascripts_dir, "application.js"))
- expanded_sources
- end
- end
-
- def expand_stylesheet_sources(sources, recursive)
- if sources.first == :all
- collect_asset_files(config.stylesheets_dir, ('**' if recursive), '*.css')
- else
- sources.collect do |source|
- determine_source(source, @@stylesheet_expansions)
- end.flatten
- end
- end
-
- def determine_source(source, collection)
- case source
- when Symbol
- collection[source] || raise(ArgumentError, "No expansion found for #{source.inspect}")
- else
- source
- end
- end
-
- def ensure_stylesheet_sources!(sources)
- sources.each do |source|
- asset_file_path!(path_to_stylesheet(source))
- end
- return sources
- end
-
- def ensure_javascript_sources!(sources)
- sources.each do |source|
- asset_file_path!(path_to_javascript(source))
- end
- return sources
- end
-
- def join_asset_file_contents(paths)
- paths.collect { |path| File.read(asset_file_path!(path)) }.join("\n\n")
- end
-
- def write_asset_file_contents(joined_asset_path, asset_paths)
-
- FileUtils.mkdir_p(File.dirname(joined_asset_path))
- File.atomic_write(joined_asset_path) { |cache| cache.write(join_asset_file_contents(asset_paths)) }
-
- # Set mtime to the latest of the combined files to allow for
- # consistent ETag without a shared filesystem.
- mt = asset_paths.map { |p| File.mtime(asset_file_path(p)) }.max
- File.utime(mt, mt, joined_asset_path)
- end
-
- def asset_file_path(path)
- File.join(config.assets_dir, path.split('?').first)
- end
-
- def asset_file_path!(path)
- unless is_uri?(path)
- absolute_path = asset_file_path(path)
- raise(Errno::ENOENT, "Asset file not found at '#{absolute_path}'" ) unless File.exist?(absolute_path)
- return absolute_path
- end
- end
-
- def collect_asset_files(*path)
- dir = path.first
-
- Dir[File.join(*path.compact)].collect do |file|
- file[-(file.size - dir.size - 1)..-1].sub(/\.\w+$/, '')
- end.sort
+ def asset_paths
+ @asset_paths ||= AssetPaths.new(config, controller)
end
end
end
diff --git a/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_include_tag.rb b/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_include_tag.rb
new file mode 100644
index 0000000000..52eb43a1cd
--- /dev/null
+++ b/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_include_tag.rb
@@ -0,0 +1,146 @@
+require 'active_support/core_ext/class/attribute'
+require 'active_support/core_ext/string/inflections'
+require 'active_support/core_ext/file'
+require 'action_view/helpers/tag_helper'
+
+module ActionView
+ module Helpers
+ module AssetTagHelper
+
+ class AssetIncludeTag
+ attr_reader :config, :asset_paths
+
+ class_attribute :expansions
+ def self.inherited(base)
+ base.expansions = { }
+ end
+
+ def initialize(config, asset_paths)
+ @config = config
+ @asset_paths = asset_paths
+ end
+
+ def asset_name
+ raise NotImplementedError
+ end
+
+ def extension
+ raise NotImplementedError
+ end
+
+ def custom_dir
+ raise NotImplementedError
+ end
+
+ def asset_tag(source, options)
+ raise NotImplementedError
+ end
+
+ def include_tag(*sources)
+ options = sources.extract_options!.stringify_keys
+ concat = options.delete("concat")
+ cache = concat || options.delete("cache")
+ recursive = options.delete("recursive")
+
+ if concat || (config.perform_caching && cache)
+ joined_name = (cache == true ? "all" : cache) + ".#{extension}"
+ joined_path = File.join((joined_name[/^#{File::SEPARATOR}/] ? config.assets_dir : custom_dir), joined_name)
+ unless config.perform_caching && File.exists?(joined_path)
+ write_asset_file_contents(joined_path, compute_paths(sources, recursive))
+ end
+ asset_tag(joined_name, options)
+ else
+ sources = expand_sources(sources, recursive)
+ ensure_sources!(sources) if cache
+ sources.collect { |source| asset_tag(source, options) }.join("\n").html_safe
+ end
+ end
+
+
+ private
+
+ def path_to_asset(source, include_host = true)
+ asset_paths.compute_public_path(source, asset_name.to_s.pluralize, extension, include_host)
+ end
+
+ def compute_paths(*args)
+ expand_sources(*args).collect { |source| asset_paths.compute_public_path(source, asset_name.pluralize, extension, false) }
+ end
+
+ def expand_sources(sources, recursive)
+ if sources.first == :all
+ collect_asset_files(custom_dir, ('**' if recursive), "*.#{extension}")
+ else
+ sources.inject([]) do |list, source|
+ determined_source = determine_source(source, expansions)
+ update_source_list(list, determined_source)
+ end
+ end
+ end
+
+ def update_source_list(list, source)
+ case source
+ when String
+ list.delete(source)
+ list << source
+ when Array
+ updated_sources = source - list
+ list.concat(updated_sources)
+ end
+ end
+
+ def ensure_sources!(sources)
+ sources.each do |source|
+ asset_file_path!(path_to_asset(source, false))
+ end
+ end
+
+ def collect_asset_files(*path)
+ dir = path.first
+
+ Dir[File.join(*path.compact)].collect do |file|
+ file[-(file.size - dir.size - 1)..-1].sub(/\.\w+$/, '')
+ end.sort
+ end
+
+ def determine_source(source, collection)
+ case source
+ when Symbol
+ collection[source] || raise(ArgumentError, "No expansion found for #{source.inspect}")
+ else
+ source
+ end
+ end
+
+ def join_asset_file_contents(paths)
+ paths.collect { |path| File.read(asset_file_path!(path, true)) }.join("\n\n")
+ end
+
+ def write_asset_file_contents(joined_asset_path, asset_paths)
+ FileUtils.mkdir_p(File.dirname(joined_asset_path))
+ File.atomic_write(joined_asset_path) { |cache| cache.write(join_asset_file_contents(asset_paths)) }
+
+ # Set mtime to the latest of the combined files to allow for
+ # consistent ETag without a shared filesystem.
+ mt = asset_paths.map { |p| File.mtime(asset_file_path(p)) }.max
+ File.utime(mt, mt, joined_asset_path)
+ end
+
+ def asset_file_path(path)
+ File.join(config.assets_dir, path.split('?').first)
+ end
+
+ def asset_file_path!(path, error_if_file_is_uri = false)
+ if asset_paths.is_uri?(path)
+ raise(Errno::ENOENT, "Asset file #{path} is uri and cannot be merged into single file") if error_if_file_is_uri
+ else
+ absolute_path = asset_file_path(path)
+ raise(Errno::ENOENT, "Asset file not found at '#{absolute_path}'" ) unless File.exist?(absolute_path)
+ return absolute_path
+ end
+ end
+ end
+
+ end
+ end
+end
diff --git a/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_paths.rb b/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_paths.rb
new file mode 100644
index 0000000000..014a03c54d
--- /dev/null
+++ b/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_paths.rb
@@ -0,0 +1,153 @@
+require 'active_support/core_ext/file'
+
+module ActionView
+ module Helpers
+ module AssetTagHelper
+
+ class AssetPaths
+ # You can enable or disable the asset tag ids cache.
+ # With the cache enabled, the asset tag helper methods will make fewer
+ # expensive file system calls (the default implementation checks the file
+ # system timestamp). However this prevents you from modifying any asset
+ # files while the server is running.
+ #
+ # ActionView::Helpers::AssetTagHelper::AssetPaths.cache_asset_ids = false
+ mattr_accessor :cache_asset_ids
+
+ attr_reader :config, :controller
+
+ def initialize(config, controller)
+ @config = config
+ @controller = controller
+ end
+
+ # Add the extension +ext+ if not present. Return full URLs otherwise untouched.
+ # Prefix with <tt>/dir/</tt> if lacking a leading +/+. Account for relative URL
+ # roots. Rewrite the asset path for cache-busting asset ids. Include
+ # asset host, if configured, with the correct request protocol.
+ def compute_public_path(source, dir, ext = nil, include_host = true)
+ return source if is_uri?(source)
+
+ source = rewrite_extension(source, dir, ext) if ext
+ source = "/#{dir}/#{source}" unless source[0] == ?/
+ if controller.respond_to?(:env) && controller.env["action_dispatch.asset_path"]
+ source = rewrite_asset_path(source, controller.env["action_dispatch.asset_path"])
+ end
+ source = rewrite_asset_path(source, config.asset_path)
+
+ has_request = controller.respond_to?(:request)
+ source = rewrite_relative_url_root(source, controller.config.relative_url_root) if has_request && include_host
+ source = rewrite_host_and_protocol(source, has_request) if include_host
+
+ source
+ end
+
+ # Add or change an asset id in the asset id cache. This can be used
+ # for SASS on Heroku.
+ # :api: public
+ def add_to_asset_ids_cache(source, asset_id)
+ self.asset_ids_cache_guard.synchronize do
+ self.asset_ids_cache[source] = asset_id
+ end
+ end
+
+ def is_uri?(path)
+ path =~ %r{^[-a-z]+://|^cid:}
+ end
+
+ private
+
+ def rewrite_extension(source, dir, ext)
+ source_ext = File.extname(source)
+
+ source_with_ext = if source_ext.empty?
+ "#{source}.#{ext}"
+ elsif ext != source_ext[1..-1]
+ with_ext = "#{source}.#{ext}"
+ with_ext if File.exist?(File.join(config.assets_dir, dir, with_ext))
+ end
+
+ source_with_ext || source
+ end
+
+ # Break out the asset path rewrite in case plugins wish to put the asset id
+ # someplace other than the query string.
+ def rewrite_asset_path(source, path = nil)
+ if path && path.respond_to?(:call)
+ return path.call(source)
+ elsif path && path.is_a?(String)
+ return path % [source]
+ end
+
+ asset_id = rails_asset_id(source)
+ if asset_id.empty?
+ source
+ else
+ "#{source}?#{asset_id}"
+ end
+ end
+
+ mattr_accessor :asset_ids_cache
+ self.asset_ids_cache = {}
+
+ mattr_accessor :asset_ids_cache_guard
+ self.asset_ids_cache_guard = Mutex.new
+
+ # Use the RAILS_ASSET_ID environment variable or the source's
+ # modification time as its cache-busting asset id.
+ def rails_asset_id(source)
+ if asset_id = ENV["RAILS_ASSET_ID"]
+ asset_id
+ else
+ if self.cache_asset_ids && (asset_id = self.asset_ids_cache[source])
+ asset_id
+ else
+ path = File.join(config.assets_dir, source)
+ asset_id = File.exist?(path) ? File.mtime(path).to_i.to_s : ''
+
+ if self.cache_asset_ids
+ add_to_asset_ids_cache(source, asset_id)
+ end
+
+ asset_id
+ end
+ end
+ end
+
+ def rewrite_relative_url_root(source, relative_url_root)
+ relative_url_root && !source.starts_with?("#{relative_url_root}/") ? "#{relative_url_root}#{source}" : source
+ end
+
+ def rewrite_host_and_protocol(source, has_request)
+ host = compute_asset_host(source)
+ if has_request && host && !is_uri?(host)
+ host = "#{controller.request.protocol}#{host}"
+ end
+ "#{host}#{source}"
+ end
+
+ # Pick an asset host for this source. Returns +nil+ if no host is set,
+ # the host if no wildcard is set, the host interpolated with the
+ # numbers 0-3 if it contains <tt>%d</tt> (the number is the source hash mod 4),
+ # or the value returned from invoking the proc if it's a proc or the value from
+ # invoking call if it's an object responding to call.
+ def compute_asset_host(source)
+ if host = config.asset_host
+ if host.is_a?(Proc) || host.respond_to?(:call)
+ case host.is_a?(Proc) ? host.arity : host.method(:call).arity
+ when 2
+ request = controller.respond_to?(:request) && controller.request
+ host.call(source, request)
+ else
+ host.call(source)
+ end
+ else
+ (host =~ /%d/) ? host % (source.hash % 4) : host
+ end
+ end
+ end
+ end
+
+ end
+ end
+end \ No newline at end of file
diff --git a/actionpack/lib/action_view/helpers/asset_tag_helpers/javascript_tag_helpers.rb b/actionpack/lib/action_view/helpers/asset_tag_helpers/javascript_tag_helpers.rb
new file mode 100644
index 0000000000..82bbfcc7d2
--- /dev/null
+++ b/actionpack/lib/action_view/helpers/asset_tag_helpers/javascript_tag_helpers.rb
@@ -0,0 +1,184 @@
+require 'active_support/concern'
+require 'active_support/core_ext/file'
+require 'action_view/helpers/tag_helper'
+require 'action_view/helpers/asset_tag_helpers/asset_include_tag'
+
+module ActionView
+ module Helpers
+ module AssetTagHelper
+
+ class JavascriptIncludeTag < AssetIncludeTag
+ include TagHelper
+
+ def asset_name
+ 'javascript'
+ end
+
+ def extension
+ 'js'
+ end
+
+ def asset_tag(source, options)
+ content_tag("script", "", { "type" => Mime::JS, "src" => path_to_asset(source) }.merge(options))
+ end
+
+ def custom_dir
+ config.javascripts_dir
+ end
+
+ private
+
+ def expand_sources(sources, recursive = false)
+ if sources.include?(:all)
+ all_asset_files = (collect_asset_files(custom_dir, ('**' if recursive), "*.#{extension}") - ['application']) << 'application'
+ ((determine_source(:defaults, expansions).dup & all_asset_files) + all_asset_files).uniq
+ else
+ expanded_sources = sources.inject([]) do |list, source|
+ determined_source = determine_source(source, expansions)
+ update_source_list(list, determined_source)
+ end
+ add_application_js(expanded_sources, sources)
+ expanded_sources
+ end
+ end
+
+ def add_application_js(expanded_sources, sources)
+ if sources.include?(:defaults) && File.exist?(File.join(custom_dir, "application.#{extension}"))
+ expanded_sources.delete('application')
+ expanded_sources << "application"
+ end
+ end
+ end
+
+
+ module JavascriptTagHelpers
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ # Register one or more javascript files to be included when <tt>symbol</tt>
+ # is passed to <tt>javascript_include_tag</tt>. This method is typically intended
+ # to be called from plugin initialization to register javascript files
+ # that the plugin installed in <tt>public/javascripts</tt>.
+ #
+ # ActionView::Helpers::AssetTagHelper.register_javascript_expansion :monkey => ["head", "body", "tail"]
+ #
+ # javascript_include_tag :monkey # =>
+ # <script type="text/javascript" src="/javascripts/head.js"></script>
+ # <script type="text/javascript" src="/javascripts/body.js"></script>
+ # <script type="text/javascript" src="/javascripts/tail.js"></script>
+ def register_javascript_expansion(expansions)
+ js_expansions = JavascriptIncludeTag.expansions
+ expansions.each do |key, values|
+ js_expansions[key] = (js_expansions[key] || []) | Array(values)
+ end
+ end
+ end
+
+ # Computes the path to a javascript asset in the public javascripts directory.
+ # If the +source+ filename has no extension, .js will be appended (except for explicit URIs)
+ # Full paths from the document root will be passed through.
+ # Used internally by javascript_include_tag to build the script path.
+ #
+ # ==== Examples
+ # javascript_path "xmlhr" # => /javascripts/xmlhr.js
+ # javascript_path "dir/xmlhr.js" # => /javascripts/dir/xmlhr.js
+ # javascript_path "/dir/xmlhr" # => /dir/xmlhr.js
+ # javascript_path "http://www.railsapplication.com/js/xmlhr" # => http://www.railsapplication.com/js/xmlhr
+ # javascript_path "http://www.railsapplication.com/js/xmlhr.js" # => http://www.railsapplication.com/js/xmlhr.js
+ def javascript_path(source)
+ asset_paths.compute_public_path(source, 'javascripts', 'js')
+ end
+ alias_method :path_to_javascript, :javascript_path # aliased to avoid conflicts with a javascript_path named route
+
+ # Returns an HTML script tag for each of the +sources+ provided. You
+ # can pass in the filename (.js extension is optional) of JavaScript files
+ # that exist in your <tt>public/javascripts</tt> directory for inclusion into the
+ # current page or you can pass the full path relative to your document
+ # root. To include the Prototype and Scriptaculous JavaScript libraries in
+ # your application, pass <tt>:defaults</tt> as the source. When using
+ # <tt>:defaults</tt>, if an <tt>application.js</tt> file exists in
+ # <tt>public/javascripts</tt> it will be included as well. You can modify the
+ # HTML attributes of the script tag by passing a hash as the last argument.
+ #
+ # ==== Examples
+ # javascript_include_tag "xmlhr" # =>
+ # <script type="text/javascript" src="/javascripts/xmlhr.js?1284139606"></script>
+ #
+ # javascript_include_tag "xmlhr.js" # =>
+ # <script type="text/javascript" src="/javascripts/xmlhr.js?1284139606"></script>
+ #
+ # javascript_include_tag "common.javascript", "/elsewhere/cools" # =>
+ # <script type="text/javascript" src="/javascripts/common.javascript?1284139606"></script>
+ # <script type="text/javascript" src="/elsewhere/cools.js?1423139606"></script>
+ #
+ # javascript_include_tag "http://www.railsapplication.com/xmlhr" # =>
+ # <script type="text/javascript" src="http://www.railsapplication.com/xmlhr.js?1284139606"></script>
+ #
+ # javascript_include_tag "http://www.railsapplication.com/xmlhr.js" # =>
+ # <script type="text/javascript" src="http://www.railsapplication.com/xmlhr.js?1284139606"></script>
+ #
+ # javascript_include_tag :defaults # =>
+ # <script type="text/javascript" src="/javascripts/prototype.js?1284139606"></script>
+ # <script type="text/javascript" src="/javascripts/effects.js?1284139606"></script>
+ # ...
+ # <script type="text/javascript" src="/javascripts/application.js?1284139606"></script>
+ #
+ # * = The application.js file is only referenced if it exists
+ #
+ # You can also include all javascripts in the +javascripts+ directory using <tt>:all</tt> as the source:
+ #
+ # javascript_include_tag :all # =>
+ # <script type="text/javascript" src="/javascripts/prototype.js?1284139606"></script>
+ # <script type="text/javascript" src="/javascripts/effects.js?1284139606"></script>
+ # ...
+ # <script type="text/javascript" src="/javascripts/application.js?1284139606"></script>
+ # <script type="text/javascript" src="/javascripts/shop.js?1284139606"></script>
+ # <script type="text/javascript" src="/javascripts/checkout.js?1284139606"></script>
+ #
+ # Note that the default javascript files will be included first. So Prototype and Scriptaculous are available to
+ # all subsequently included files.
+ #
+ # If you want Rails to search in all the subdirectories under javascripts, you should explicitly set <tt>:recursive</tt>:
+ #
+ # javascript_include_tag :all, :recursive => true
+ #
+ # == Caching multiple javascripts into one
+ #
+ # You can also cache multiple javascripts into one file, which requires less HTTP connections to download and can better be
+ # compressed by gzip (leading to faster transfers). Caching will only happen if config.perform_caching
+ # is set to <tt>true</tt> (which is the case by default for the Rails production environment, but not for the development
+ # environment).
+ #
+ # ==== Examples
+ # javascript_include_tag :all, :cache => true # when config.perform_caching is false =>
+ # <script type="text/javascript" src="/javascripts/prototype.js?1284139606"></script>
+ # <script type="text/javascript" src="/javascripts/effects.js?1284139606"></script>
+ # ...
+ # <script type="text/javascript" src="/javascripts/application.js?1284139606"></script>
+ # <script type="text/javascript" src="/javascripts/shop.js?1284139606"></script>
+ # <script type="text/javascript" src="/javascripts/checkout.js?1284139606"></script>
+ #
+ # javascript_include_tag :all, :cache => true # when config.perform_caching is true =>
+ # <script type="text/javascript" src="/javascripts/all.js?1344139789"></script>
+ #
+ # javascript_include_tag "prototype", "cart", "checkout", :cache => "shop" # when config.perform_caching is false =>
+ # <script type="text/javascript" src="/javascripts/prototype.js?1284139606"></script>
+ # <script type="text/javascript" src="/javascripts/cart.js?1289139157"></script>
+ # <script type="text/javascript" src="/javascripts/checkout.js?1299139816"></script>
+ #
+ # javascript_include_tag "prototype", "cart", "checkout", :cache => "shop" # when config.perform_caching is true =>
+ # <script type="text/javascript" src="/javascripts/shop.js?1299139816"></script>
+ #
+ # The <tt>:recursive</tt> option is also available for caching:
+ #
+ # javascript_include_tag :all, :cache => true, :recursive => true
+ def javascript_include_tag(*sources)
+ @javascript_include ||= JavascriptIncludeTag.new(config, asset_paths)
+ @javascript_include.include_tag(*sources)
+ end
+
+ end
+
+ end
+ end
+end
diff --git a/actionpack/lib/action_view/helpers/asset_tag_helpers/stylesheet_tag_helpers.rb b/actionpack/lib/action_view/helpers/asset_tag_helpers/stylesheet_tag_helpers.rb
new file mode 100644
index 0000000000..a48c87b49a
--- /dev/null
+++ b/actionpack/lib/action_view/helpers/asset_tag_helpers/stylesheet_tag_helpers.rb
@@ -0,0 +1,147 @@
+require 'active_support/concern'
+require 'active_support/core_ext/file'
+require 'action_view/helpers/tag_helper'
+require 'action_view/helpers/asset_tag_helpers/asset_include_tag'
+
+module ActionView
+ module Helpers
+ module AssetTagHelper
+
+ class StylesheetIncludeTag < AssetIncludeTag
+ include TagHelper
+
+ def asset_name
+ 'stylesheet'
+ end
+
+ def extension
+ 'css'
+ end
+
+ def asset_tag(source, options)
+ tag("link", { "rel" => "stylesheet", "type" => Mime::CSS, "media" => "screen", "href" => ERB::Util.html_escape(path_to_asset(source)) }.merge(options), false, false)
+ end
+
+ def custom_dir
+ config.stylesheets_dir
+ end
+ end
+
+
+ module StylesheetTagHelpers
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ # Register one or more stylesheet files to be included when <tt>symbol</tt>
+ # is passed to <tt>stylesheet_link_tag</tt>. This method is typically intended
+ # to be called from plugin initialization to register stylesheet files
+ # that the plugin installed in <tt>public/stylesheets</tt>.
+ #
+ # ActionView::Helpers::AssetTagHelper.register_stylesheet_expansion :monkey => ["head", "body", "tail"]
+ #
+ # stylesheet_link_tag :monkey # =>
+ # <link href="/stylesheets/head.css" media="screen" rel="stylesheet" type="text/css" />
+ # <link href="/stylesheets/body.css" media="screen" rel="stylesheet" type="text/css" />
+ # <link href="/stylesheets/tail.css" media="screen" rel="stylesheet" type="text/css" />
+ def register_stylesheet_expansion(expansions)
+ style_expansions = StylesheetIncludeTag.expansions
+ expansions.each do |key, values|
+ style_expansions[key] = (style_expansions[key] || []) | Array(values)
+ end
+ end
+ end
+
+ # Computes the path to a stylesheet asset in the public stylesheets directory.
+ # If the +source+ filename has no extension, <tt>.css</tt> will be appended (except for explicit URIs).
+ # Full paths from the document root will be passed through.
+ # Used internally by +stylesheet_link_tag+ to build the stylesheet path.
+ #
+ # ==== Examples
+ # stylesheet_path "style" # => /stylesheets/style.css
+ # stylesheet_path "dir/style.css" # => /stylesheets/dir/style.css
+ # stylesheet_path "/dir/style.css" # => /dir/style.css
+ # stylesheet_path "http://www.railsapplication.com/css/style" # => http://www.railsapplication.com/css/style
+ # stylesheet_path "http://www.railsapplication.com/css/style.css" # => http://www.railsapplication.com/css/style.css
+ def stylesheet_path(source)
+ asset_paths.compute_public_path(source, 'stylesheets', 'css')
+ end
+ alias_method :path_to_stylesheet, :stylesheet_path # aliased to avoid conflicts with a stylesheet_path named route
+
+ # Returns a stylesheet link tag for the sources specified as arguments. If
+ # you don't specify an extension, <tt>.css</tt> will be appended automatically.
+ # You can modify the link attributes by passing a hash as the last argument.
+ #
+ # ==== Examples
+ # stylesheet_link_tag "style" # =>
+ # <link href="/stylesheets/style.css" media="screen" rel="stylesheet" type="text/css" />
+ #
+ # stylesheet_link_tag "style.css" # =>
+ # <link href="/stylesheets/style.css" media="screen" rel="stylesheet" type="text/css" />
+ #
+ # stylesheet_link_tag "http://www.railsapplication.com/style.css" # =>
+ # <link href="http://www.railsapplication.com/style.css" media="screen" rel="stylesheet" type="text/css" />
+ #
+ # stylesheet_link_tag "style", :media => "all" # =>
+ # <link href="/stylesheets/style.css" media="all" rel="stylesheet" type="text/css" />
+ #
+ # stylesheet_link_tag "style", :media => "print" # =>
+ # <link href="/stylesheets/style.css" media="print" rel="stylesheet" type="text/css" />
+ #
+ # stylesheet_link_tag "random.styles", "/css/stylish" # =>
+ # <link href="/stylesheets/random.styles" media="screen" rel="stylesheet" type="text/css" />
+ # <link href="/css/stylish.css" media="screen" rel="stylesheet" type="text/css" />
+ #
+ # You can also include all styles in the stylesheets directory using <tt>:all</tt> as the source:
+ #
+ # stylesheet_link_tag :all # =>
+ # <link href="/stylesheets/style1.css" media="screen" rel="stylesheet" type="text/css" />
+ # <link href="/stylesheets/styleB.css" media="screen" rel="stylesheet" type="text/css" />
+ # <link href="/stylesheets/styleX2.css" media="screen" rel="stylesheet" type="text/css" />
+ #
+ # If you want Rails to search in all the subdirectories under stylesheets, you should explicitly set <tt>:recursive</tt>:
+ #
+ # stylesheet_link_tag :all, :recursive => true
+ #
+ # == Caching multiple stylesheets into one
+ #
+ # You can also cache multiple stylesheets into one file, which requires less HTTP connections and can better be
+ # compressed by gzip (leading to faster transfers). Caching will only happen if config.perform_caching
+ # is set to true (which is the case by default for the Rails production environment, but not for the development
+ # environment). Examples:
+ #
+ # ==== Examples
+ # stylesheet_link_tag :all, :cache => true # when config.perform_caching is false =>
+ # <link href="/stylesheets/style1.css" media="screen" rel="stylesheet" type="text/css" />
+ # <link href="/stylesheets/styleB.css" media="screen" rel="stylesheet" type="text/css" />
+ # <link href="/stylesheets/styleX2.css" media="screen" rel="stylesheet" type="text/css" />
+ #
+ # stylesheet_link_tag :all, :cache => true # when config.perform_caching is true =>
+ # <link href="/stylesheets/all.css" media="screen" rel="stylesheet" type="text/css" />
+ #
+ # stylesheet_link_tag "shop", "cart", "checkout", :cache => "payment" # when config.perform_caching is false =>
+ # <link href="/stylesheets/shop.css" media="screen" rel="stylesheet" type="text/css" />
+ # <link href="/stylesheets/cart.css" media="screen" rel="stylesheet" type="text/css" />
+ # <link href="/stylesheets/checkout.css" media="screen" rel="stylesheet" type="text/css" />
+ #
+ # stylesheet_link_tag "shop", "cart", "checkout", :cache => "payment" # when config.perform_caching is true =>
+ # <link href="/stylesheets/payment.css" media="screen" rel="stylesheet" type="text/css" />
+ #
+ # The <tt>:recursive</tt> option is also available for caching:
+ #
+ # stylesheet_link_tag :all, :cache => true, :recursive => true
+ #
+ # To force concatenation (even in development mode) set <tt>:concat</tt> to true. This is useful if
+ # you have too many stylesheets for IE to load.
+ #
+ # stylesheet_link_tag :all, :concat => true
+ #
+ def stylesheet_link_tag(*sources)
+ @stylesheet_include ||= StylesheetIncludeTag.new(config, asset_paths)
+ @stylesheet_include.include_tag(*sources)
+ end
+
+ end
+
+ end
+ end
+end
diff --git a/actionpack/lib/action_view/helpers/atom_feed_helper.rb b/actionpack/lib/action_view/helpers/atom_feed_helper.rb
index 8e7cf2e701..db9d7a08ff 100644
--- a/actionpack/lib/action_view/helpers/atom_feed_helper.rb
+++ b/actionpack/lib/action_view/helpers/atom_feed_helper.rb
@@ -51,7 +51,7 @@ module ActionView
# * <tt>:language</tt>: Defaults to "en-US".
# * <tt>:root_url</tt>: The HTML alternative that this feed is doubling for. Defaults to / on the current host.
# * <tt>:url</tt>: The URL for this feed. Defaults to the current URL.
- # * <tt>:id</tt>: The id for this feed. Defaults to "tag:#{request.host},#{options[:schema_date]}:#{request.request_uri.split(".")[0]}"
+ # * <tt>:id</tt>: The id for this feed. Defaults to "tag:#{request.host},#{options[:schema_date]}:#{request.fullpath.split(".")[0]}"
# * <tt>:schema_date</tt>: The date at which the tag scheme for the feed was first used. A good default is the year you
# created the feed. See http://feedvalidator.org/docs/error/InvalidTAG.html for more information. If not specified,
# 2005 is used (as an "I don't care" value).
diff --git a/actionpack/lib/action_view/helpers/cache_helper.rb b/actionpack/lib/action_view/helpers/cache_helper.rb
index f544a9d147..385378ea29 100644
--- a/actionpack/lib/action_view/helpers/cache_helper.rb
+++ b/actionpack/lib/action_view/helpers/cache_helper.rb
@@ -2,31 +2,29 @@ module ActionView
# = Action View Cache Helper
module Helpers
module CacheHelper
- # This helper to exposes a method for caching of view fragments.
- # See ActionController::Caching::Fragments for usage instructions.
+ # This helper exposes a method for caching fragments of a view
+ # rather than an entire action or page. This technique is useful
+ # caching pieces like menus, lists of newstopics, static HTML
+ # fragments, and so on. This method takes a block that contains
+ # the content you wish to cache.
#
- # A method for caching fragments of a view rather than an entire
- # action or page. This technique is useful caching pieces like
- # menus, lists of news topics, static HTML fragments, and so on.
- # This method takes a block that contains the content you wish
- # to cache. See ActionController::Caching::Fragments for more
- # information.
+ # See ActionController::Caching::Fragments for usage instructions.
#
# ==== Examples
- # If you wanted to cache a navigation menu, you could do the
- # following.
+ # If you want to cache a navigation menu, you can do following:
#
# <% cache do %>
# <%= render :partial => "menu" %>
# <% end %>
#
- # You can also cache static content...
+ # You can also cache static content:
#
# <% cache do %>
# <p>Hello users! Welcome to our website!</p>
# <% end %>
#
- # ...and static content mixed with RHTML content.
+ # Static content with embedded ruby content can be cached as
+ # well:
#
# <% cache do %>
# Topics:
@@ -46,8 +44,8 @@ module ActionView
private
# TODO: Create an object that has caching read/write on it
def fragment_for(name = {}, options = nil, &block) #:nodoc:
- if controller.fragment_exist?(name, options)
- controller.read_fragment(name, options)
+ if fragment = controller.read_fragment(name, options)
+ fragment
else
# VIEW TODO: Make #capture usable outside of ERB
# This dance is needed because Builder can't use capture
diff --git a/actionpack/lib/action_view/helpers/capture_helper.rb b/actionpack/lib/action_view/helpers/capture_helper.rb
index 89e95e8694..c88bd1efd5 100644
--- a/actionpack/lib/action_view/helpers/capture_helper.rb
+++ b/actionpack/lib/action_view/helpers/capture_helper.rb
@@ -1,4 +1,5 @@
require 'active_support/core_ext/object/blank'
+require 'active_support/core_ext/string/output_safety'
module ActionView
# = Action View Capture Helper
@@ -38,7 +39,7 @@ module ActionView
value = nil
buffer = with_output_buffer { value = yield(*args) }
if string = buffer.presence || value and string.is_a?(String)
- NonConcattingString.new(string)
+ ERB::Util.html_escape string
end
end
@@ -106,7 +107,7 @@ module ActionView
# <%= javascript_include_tag :defaults %>
# <% end %>
#
- # That will place <script> tags for Prototype, Scriptaculous, and application.js (if it exists)
+ # That will place <tt>script</tt> tags for Prototype, Scriptaculous, and application.js (if it exists)
# on the page; this technique is useful if you'll only be using these scripts in a few views.
#
# Note that content_for concatenates the blocks it is given for a particular
diff --git a/actionpack/lib/action_view/helpers/csrf_helper.rb b/actionpack/lib/action_view/helpers/csrf_helper.rb
index 3d03f6aac6..65c8debc76 100644
--- a/actionpack/lib/action_view/helpers/csrf_helper.rb
+++ b/actionpack/lib/action_view/helpers/csrf_helper.rb
@@ -1,14 +1,30 @@
+require 'active_support/core_ext/string/strip'
+
module ActionView
# = Action View CSRF Helper
module Helpers
module CsrfHelper
- # Returns a meta tag with the cross-site request forgery protection token
- # for forms to use. Place this in your head.
- def csrf_meta_tag
- if protect_against_forgery?
- %(<meta name="csrf-param" content="#{Rack::Utils.escape_html(request_forgery_protection_token)}"/>\n<meta name="csrf-token" content="#{Rack::Utils.escape_html(form_authenticity_token)}"/>).html_safe
- end
+ # Returns meta tags "csrf-param" and "csrf-token" with the name of the cross-site
+ # request forgery protection parameter and token, respectively.
+ #
+ # <head>
+ # <%= csrf_meta_tags %>
+ # </head>
+ #
+ # These are used to generate the dynamic forms that implement non-remote links with
+ # <tt>:method</tt>.
+ #
+ # Note that regular forms generate hidden fields, and that Ajax calls are whitelisted,
+ # so they do not use these tags.
+ def csrf_meta_tags
+ <<-METAS.strip_heredoc.chomp.html_safe if protect_against_forgery?
+ <meta name="csrf-param" content="#{Rack::Utils.escape_html(request_forgery_protection_token)}"/>
+ <meta name="csrf-token" content="#{Rack::Utils.escape_html(form_authenticity_token)}"/>
+ METAS
end
+
+ # For backwards compatibility.
+ alias csrf_meta_tag csrf_meta_tags
end
end
end
diff --git a/actionpack/lib/action_view/helpers/date_helper.rb b/actionpack/lib/action_view/helpers/date_helper.rb
index 8050669adb..6cd1565031 100644
--- a/actionpack/lib/action_view/helpers/date_helper.rb
+++ b/actionpack/lib/action_view/helpers/date_helper.rb
@@ -1,6 +1,8 @@
-require "date"
+require 'date'
require 'action_view/helpers/tag_helper'
+require 'active_support/core_ext/date/conversions'
require 'active_support/core_ext/hash/slice'
+require 'active_support/core_ext/object/with_options'
module ActionView
module Helpers
@@ -214,21 +216,10 @@ module ActionView
# # Creates a time select tag that, when POSTed, will be stored in the post variable in the sunrise attribute
# time_select("post", "sunrise")
#
- # # Creates a time select tag that, when POSTed, will be stored in the order variable in the submitted
- # # attribute
- # time_select("order", "submitted")
- #
- # # Creates a time select tag that, when POSTed, will be stored in the mail variable in the sent_at attribute
- # time_select("mail", "sent_at")
- #
# # Creates a time select tag with a seconds field that, when POSTed, will be stored in the post variables in
# # the sunrise attribute.
# time_select("post", "start_time", :include_seconds => true)
#
- # # Creates a time select tag with a seconds field that, when POSTed, will be stored in the entry variables in
- # # the submission_time attribute.
- # time_select("entry", "submission_time", :include_seconds => true)
- #
# # You can set the :minute_step to 15 which will give you: 00, 15, 30 and 45.
# time_select 'game', 'game_time', {:minute_step => 15}
#
@@ -576,6 +567,27 @@ module ActionView
def select_year(date, options = {}, html_options = {})
DateTimeSelector.new(date, options, html_options).select_year
end
+
+ # Returns an html time tag for the given date or time.
+ #
+ # ==== Examples
+ # time_tag Date.today # =>
+ # <time datetime="2010-11-04">November 04, 2010</time>
+ # time_tag Time.now # =>
+ # <time datetime="2010-11-04T17:55:45+01:00">November 04, 2010 17:55</time>
+ # time_tag Date.yesterday, 'Yesterday' # =>
+ # <time datetime="2010-11-03">Yesterday</time>
+ # time_tag Date.today, :pubdate => true # =>
+ # <time datetime="2010-11-04" pubdate="pubdate">November 04, 2010</time>
+ #
+ def time_tag(date_or_time, *args)
+ options = args.extract_options!
+ format = options.delete(:format) || :long
+ content = args.first || I18n.l(date_or_time, :format => format)
+ datetime = date_or_time.acts_like?(:time) ? date_or_time.xmlschema : date_or_time.rfc3339
+
+ content_tag(:time, content, options.reverse_merge(:datetime => datetime))
+ end
end
class DateTimeSelector #:nodoc:
@@ -605,7 +617,7 @@ module ActionView
@options[:discard_second] ||= true unless @options[:include_seconds] && !@options[:discard_minute]
# If the day is hidden and the month is visible, the day should be set to the 1st so all month choices are
- # valid (otherwise it could be 31 and february wouldn't be a valid date)
+ # valid (otherwise it could be 31 and February wouldn't be a valid date)
if @datetime && @options[:discard_day] && !@options[:discard_month]
@datetime = @datetime.change(:day => 1)
end
@@ -632,7 +644,7 @@ module ActionView
@options[:discard_day] ||= true if @options[:discard_month] || !order.include?(:day)
# If the day is hidden and the month is visible, the day should be set to the 1st so all month choices are
- # valid (otherwise it could be 31 and february wouldn't be a valid date)
+ # valid (otherwise it could be 31 and February wouldn't be a valid date)
if @datetime && @options[:discard_day] && !@options[:discard_month]
@datetime = @datetime.change(:day => 1)
end
@@ -751,10 +763,8 @@ module ActionView
# => [nil, "Jan", "Feb", "Mar", "Apr", "May", "Jun",
# "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
def translated_month_names
- begin
- key = @options[:use_short_month] ? :'date.abbr_month_names' : :'date.month_names'
- I18n.translate(key, :locale => @options[:locale])
- end
+ key = @options[:use_short_month] ? :'date.abbr_month_names' : :'date.month_names'
+ I18n.translate(key, :locale => @options[:locale])
end
# Lookup month name for number
@@ -781,9 +791,7 @@ module ActionView
memoize :date_order
def translated_date_order
- begin
- I18n.translate(:'date.order', :locale => @options[:locale]) || []
- end
+ I18n.translate(:'date.order', :locale => @options[:locale]) || []
end
# Build full select tag from date type and options
@@ -837,15 +845,14 @@ module ActionView
# prompt_option_tag(:month, :prompt => 'Select month')
# => "<option value="">Select month</option>"
def prompt_option_tag(type, options)
- default_options = {:year => false, :month => false, :day => false, :hour => false, :minute => false, :second => false}
-
- case options
- when Hash
- prompt = default_options.merge(options)[type.to_sym]
- when String
- prompt = options
- else
- prompt = I18n.translate(('datetime.prompts.' + type.to_s).to_sym, :locale => @options[:locale])
+ prompt = case options
+ when Hash
+ default_options = {:year => false, :month => false, :day => false, :hour => false, :minute => false, :second => false}
+ default_options.merge!(options)[type.to_sym]
+ when String
+ options
+ else
+ I18n.translate(:"datetime.prompts.#{type}", :locale => @options[:locale])
end
prompt ? content_tag(:option, prompt, :value => '') : ''
@@ -897,6 +904,8 @@ module ActionView
# Returns the separator for a given datetime component
def separator(type)
case type
+ when :year
+ @options[:discard_year] ? "" : @options[:date_separator]
when :month
@options[:discard_month] ? "" : @options[:date_separator]
when :day
@@ -927,6 +936,7 @@ module ActionView
private
def datetime_selector(options, html_options)
datetime = value(object) || default_datetime(options)
+ @auto_index ||= nil
options = options.dup
options[:field_name] = @method_name
diff --git a/actionpack/lib/action_view/helpers/debug_helper.rb b/actionpack/lib/action_view/helpers/debug_helper.rb
index 1491cb073f..cd67851642 100644
--- a/actionpack/lib/action_view/helpers/debug_helper.rb
+++ b/actionpack/lib/action_view/helpers/debug_helper.rb
@@ -1,6 +1,6 @@
module ActionView
# = Action View Debug Helper
- #
+ #
# Provides a set of methods for making it easier to debug Rails objects.
module Helpers
module DebugHelper
diff --git a/actionpack/lib/action_view/helpers/form_helper.rb b/actionpack/lib/action_view/helpers/form_helper.rb
index ebe055bebd..48abf119f1 100644
--- a/actionpack/lib/action_view/helpers/form_helper.rb
+++ b/actionpack/lib/action_view/helpers/form_helper.rb
@@ -2,9 +2,11 @@ require 'cgi'
require 'action_view/helpers/date_helper'
require 'action_view/helpers/tag_helper'
require 'action_view/helpers/form_tag_helper'
-require 'active_support/core_ext/class/inheritable_attributes'
+require 'active_support/core_ext/class/attribute'
require 'active_support/core_ext/hash/slice'
require 'active_support/core_ext/object/blank'
+require 'active_support/core_ext/string/output_safety'
+require 'active_support/core_ext/array/extract_options'
module ActionView
# = Action View Form Helpers
@@ -111,6 +113,7 @@ module ActionView
# <%= f.text_field :version %><br />
# <%= f.label :author, 'Author' %>:
# <%= f.text_field :author %><br />
+ # <%= f.submit %>
# <% end %>
#
# There, +form_for+ is able to generate the rest of RESTful form
@@ -128,6 +131,7 @@ module ActionView
# Last name : <%= f.text_field :last_name %><br />
# Biography : <%= f.text_area :biography %><br />
# Admin? : <%= f.check_box :admin %><br />
+ # <%= f.submit %>
# <% end %>
#
# There, the argument is a symbol or string with the name of the
@@ -159,6 +163,7 @@ module ActionView
# Last name : <%= f.text_field :last_name %>
# Biography : <%= text_area :person, :biography %>
# Admin? : <%= check_box_tag "person[admin]", @person.company.admin? %>
+ # <%= f.submit %>
# <% end %>
#
# This also works for the methods in FormOptionHelper and DateHelper that
@@ -202,10 +207,16 @@ module ActionView
# ...
# <% end %>
#
+ # You can also set the answer format, like this:
+ #
+ # <%= form_for(@post, :format => :json) do |f| %>
+ # ...
+ # <% end %>
+ #
# If you have an object that needs to be represented as a different
# parameter, like a Client that acts as a Person:
#
- # <%= form_for(@post, :as => :client do |f| %>
+ # <%= form_for(@post, :as => :client) do |f| %>
# ...
# <% end %>
#
@@ -222,8 +233,8 @@ module ActionView
# ...
# <% end %>
#
- # Where +@document = Document.find(params[:id])+ and
- # +@comment = Comment.new+.
+ # Where <tt>@document = Document.find(params[:id])</tt> and
+ # <tt>@comment = Comment.new</tt>.
#
# === Unobtrusive JavaScript
#
@@ -252,6 +263,24 @@ module ActionView
# ...
# </form>
#
+ # === Removing hidden model id's
+ #
+ # The form_for method automatically includes the model id as a hidden field in the form.
+ # This is used to maintain the correlation between the form data and its associated model.
+ # Some ORM systems do not use IDs on nested models so in this case you want to be able
+ # to disable the hidden id.
+ #
+ # In the following example the Post model has many Comments stored within it in a NoSQL database,
+ # thus there is no primary key for comments.
+ #
+ # Example:
+ #
+ # <%= form(@post) do |f| %>
+ # <% f.fields_for(:comments, :include_id => false) do |cf| %>
+ # ...
+ # <% end %>
+ # <% end %>
+ #
# === Customized form builders
#
# You can also build forms using a customized FormBuilder class. Subclass
@@ -262,8 +291,9 @@ module ActionView
# <%= form_for @person, :url => { :action => "create" }, :builder => LabellingFormBuilder do |f| %>
# <%= f.text_field :first_name %>
# <%= f.text_field :last_name %>
- # <%= text_area :person, :biography %>
- # <%= check_box_tag "person[admin]", @person.company.admin? %>
+ # <%= f.text_area :biography %>
+ # <%= f.check_box :admin %>
+ # <%= f.submit %>
# <% end %>
#
# In this case, if you use this:
@@ -287,31 +317,46 @@ module ActionView
#
# If you don't need to attach a form to a model instance, then check out
# FormTagHelper#form_tag.
- def form_for(record_or_name_or_array, *args, &proc)
+ #
+ # === Form to external resources
+ #
+ # When you build forms to external resources sometimes you need to set an authenticity token or just render a form
+ # without it, for example when you submit data to a payment gateway number and types of fields could be limited.
+ #
+ # To set an authenticity token you need to pass an <tt>:authenticity_token</tt> parameter
+ #
+ # <%= form_for @invoice, :url => external_url, :authenticity_token => 'external_token' do |f|
+ # ...
+ # <% end %>
+ #
+ # If you don't want to an authenticity token field be rendered at all just pass <tt>false</tt>:
+ #
+ # <%= form_for @invoice, :url => external_url, :authenticity_token => false do |f|
+ # ...
+ # <% end %>
+ def form_for(record, options = {}, &proc)
raise ArgumentError, "Missing block" unless block_given?
- options = args.extract_options!
+ options[:html] ||= {}
- case record_or_name_or_array
+ case record
when String, Symbol
- ActiveSupport::Deprecation.warn("Using form_for(:name, @resource) is deprecated. Please use form_for(@resource, :as => :name) instead.", caller) unless args.empty?
- object_name = record_or_name_or_array
- when Array
- object = record_or_name_or_array.last
- object_name = options[:as] || ActiveModel::Naming.singular(object)
- apply_form_for_options!(record_or_name_or_array, options)
- args.unshift object
+ object_name = record
+ object = nil
else
- object = record_or_name_or_array
- object_name = options[:as] || ActiveModel::Naming.singular(object)
- apply_form_for_options!([object], options)
- args.unshift object
+ object = record.is_a?(Array) ? record.last : record
+ object_name = options[:as] || ActiveModel::Naming.param_key(object)
+ apply_form_for_options!(record, options)
end
- (options[:html] ||= {})[:remote] = true if options.delete(:remote)
+ options[:html][:remote] = options.delete(:remote)
+ options[:html][:authenticity_token] = options.delete(:authenticity_token)
- output = form_tag(options.delete(:url) || {}, options.delete(:html) || {})
- output << fields_for(object_name, *(args << options), &proc)
+ builder = options[:parent_builder] = instantiate_builder(object_name, object, options, &proc)
+ fields_for = fields_for(object_name, object, options, &proc)
+ default_options = builder.multipart? ? { :multipart => true } : {}
+ output = form_tag(options.delete(:url) || {}, default_options.merge!(options.delete(:html)))
+ output << fields_for
output.safe_concat('</form>')
end
@@ -319,21 +364,17 @@ module ActionView
object = object_or_array.is_a?(Array) ? object_or_array.last : object_or_array
object = convert_to_model(object)
- html_options =
- if object.respond_to?(:persisted?) && object.persisted?
- { :class => options[:as] ? "#{options[:as]}_edit" : dom_class(object, :edit),
- :id => options[:as] ? "#{options[:as]}_edit" : dom_id(object, :edit),
- :method => :put }
- else
- { :class => options[:as] ? "#{options[:as]}_new" : dom_class(object, :new),
- :id => options[:as] ? "#{options[:as]}_new" : dom_id(object),
- :method => :post }
- end
+ as = options[:as]
+ action, method = object.respond_to?(:persisted?) && object.persisted? ? [:edit, :put] : [:new, :post]
+ options[:html].reverse_merge!(
+ :class => as ? "#{as}_#{action}" : dom_class(object, action),
+ :id => as ? "#{as}_#{action}" : dom_id(object, action),
+ :method => method
+ )
- options[:html] ||= {}
- options[:html].reverse_merge!(html_options)
- options[:url] ||= polymorphic_path(object_or_array)
+ options[:url] ||= polymorphic_path(object_or_array, :format => options.delete(:format))
end
+ private :apply_form_for_options!
# Creates a scope around a specific model object like form_for, but
# doesn't create the form tags themselves. This makes fields_for suitable
@@ -348,6 +389,8 @@ module ActionView
# <%= fields_for @person.permission do |permission_fields| %>
# Admin? : <%= permission_fields.check_box :admin %>
# <% end %>
+ #
+ # <%= f.submit %>
# <% end %>
#
# ...or if you have an object that needs to be represented as a different
@@ -409,6 +452,7 @@ module ActionView
# Street : <%= address_fields.text_field :street %>
# Zip code: <%= address_fields.text_field :zip_code %>
# <% end %>
+ # ...
# <% end %>
#
# When address is already an association on a Person you can use
@@ -438,6 +482,7 @@ module ActionView
# ...
# Delete: <%= address_fields.check_box :_destroy %>
# <% end %>
+ # ...
# <% end %>
#
# ==== One-to-many
@@ -467,6 +512,7 @@ module ActionView
# Name: <%= project_fields.text_field :name %>
# <% end %>
# <% end %>
+ # ...
# <% end %>
#
# It's also possible to specify the instance to be used:
@@ -480,6 +526,7 @@ module ActionView
# <% end %>
# <% end %>
# <% end %>
+ # ...
# <% end %>
#
# Or a collection to be used:
@@ -489,6 +536,7 @@ module ActionView
# <%= person_form.fields_for :projects, @active_projects do |project_fields| %>
# Name: <%= project_fields.text_field :name %>
# <% end %>
+ # ...
# <% end %>
#
# When projects is already an association on Person you can use
@@ -518,22 +566,13 @@ module ActionView
# <%= person_form.fields_for :projects do |project_fields| %>
# Delete: <%= project_fields.check_box :_destroy %>
# <% end %>
+ # ...
# <% end %>
- def fields_for(record_or_name_or_array, *args, &block)
- raise ArgumentError, "Missing block" unless block_given?
- options = args.extract_options!
-
- case record_or_name_or_array
- when String, Symbol
- object_name = record_or_name_or_array
- object = args.first
- else
- object = record_or_name_or_array
- object_name = ActiveModel::Naming.singular(object)
- end
-
- builder = options[:builder] || ActionView::Base.default_form_builder
- capture(builder.new(object_name, object, self, options, block), &block)
+ def fields_for(record, record_object = nil, options = {}, &block)
+ builder = instantiate_builder(record, record_object, options, &block)
+ output = capture(builder, &block)
+ output.concat builder.hidden_field(:id) if output && options[:hidden_field_id] && !builder.emitted_hidden_id?
+ output
end
# Returns a label tag tailored for labelling an input field for a specified attribute (identified by +method+) on an object
@@ -624,19 +663,19 @@ module ActionView
#
# ==== Examples
# password_field(:login, :pass, :size => 20)
- # # => <input type="text" id="login_pass" name="login[pass]" size="20" value="#{@login.pass}" />
+ # # => <input type="password" id="login_pass" name="login[pass]" size="20" />
#
- # password_field(:account, :secret, :class => "form_input")
- # # => <input type="text" id="account_secret" name="account[secret]" value="#{@account.secret}" class="form_input" />
+ # password_field(:account, :secret, :class => "form_input", :value => @account.secret)
+ # # => <input type="password" id="account_secret" name="account[secret]" value="#{@account.secret}" class="form_input" />
#
# password_field(:user, :password, :onchange => "if $('user[password]').length > 30 { alert('Your password needs to be shorter!'); }")
- # # => <input type="text" id="user_password" name="user[password]" value="#{@user.password}" onchange = "if $('user[password]').length > 30 { alert('Your password needs to be shorter!'); }"/>
+ # # => <input type="password" id="user_password" name="user[password]" onchange = "if $('user[password]').length > 30 { alert('Your password needs to be shorter!'); }"/>
#
# password_field(:account, :pin, :size => 20, :class => 'form_input')
- # # => <input type="text" id="account_pin" name="account[pin]" size="20" value="#{@account.pin}" class="form_input" />
+ # # => <input type="password" id="account_pin" name="account[pin]" size="20" class="form_input" />
#
def password_field(object_name, method, options = {})
- InstanceTag.new(object_name, method, self, options.delete(:object)).to_input_field_tag("password", options)
+ InstanceTag.new(object_name, method, self, options.delete(:object)).to_input_field_tag("password", { :value => nil }.merge!(options))
end
# Returns a hidden input tag tailored for accessing a specified attribute (identified by +method+) on an object
@@ -657,11 +696,13 @@ module ActionView
InstanceTag.new(object_name, method, self, options.delete(:object)).to_input_field_tag("hidden", options)
end
- # Returns an file upload input tag tailored for accessing a specified attribute (identified by +method+) on an object
+ # Returns a file upload input tag tailored for accessing a specified attribute (identified by +method+) on an object
# assigned to the template (identified by +object+). Additional options on the input tag can be passed as a
# hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example
# shown.
#
+ # Using this method inside a +form_for+ block will set the enclosing form's encoding to <tt>multipart/form-data</tt>.
+ #
# ==== Examples
# file_field(:user, :avatar)
# # => <input type="file" id="user_avatar" name="user[avatar]" />
@@ -803,7 +844,7 @@ module ActionView
options["incremental"] = true unless options.has_key?("incremental")
end
- InstanceTag.new(object_name, method, self, options.delete(:object)).to_input_field_tag("search", options)
+ InstanceTag.new(object_name, method, self, options.delete("object")).to_input_field_tag("search", options)
end
# Returns a text_field of type "tel".
@@ -837,29 +878,42 @@ module ActionView
def range_field(object_name, method, options = {})
InstanceTag.new(object_name, method, self, options.delete(:object)).to_number_field_tag("range", options)
end
+
+ private
+
+ def instantiate_builder(record, *args, &block)
+ options = args.extract_options!
+ record_object = args.shift
+
+ case record
+ when String, Symbol
+ object = record_object
+ object_name = record
+ else
+ object = record
+ object_name = ActiveModel::Naming.param_key(object)
+ end
+
+ builder = options[:builder] || ActionView::Base.default_form_builder
+ builder.new(object_name, object, self, options, block)
+ end
end
- module InstanceTagMethods #:nodoc:
- extend ActiveSupport::Concern
+ class InstanceTag
include Helpers::CaptureHelper, Context, Helpers::TagHelper, Helpers::FormTagHelper
- attr_reader :method_name, :object_name
+ attr_reader :object, :method_name, :object_name
- DEFAULT_FIELD_OPTIONS = { "size" => 30 }.freeze
- DEFAULT_RADIO_OPTIONS = { }.freeze
- DEFAULT_TEXT_AREA_OPTIONS = { "cols" => 40, "rows" => 20 }.freeze
+ DEFAULT_FIELD_OPTIONS = { "size" => 30 }
+ DEFAULT_RADIO_OPTIONS = { }
+ DEFAULT_TEXT_AREA_OPTIONS = { "cols" => 40, "rows" => 20 }
def initialize(object_name, method_name, template_object, object = nil)
@object_name, @method_name = object_name.to_s.dup, method_name.to_s.dup
@template_object = template_object
- @object = object
- if @object_name.sub!(/\[\]$/,"") || @object_name.sub!(/\[\]\]$/,"]")
- if (object ||= @template_object.instance_variable_get("@#{Regexp.last_match.pre_match}")) && object.respond_to?(:to_param)
- @auto_index = object.to_param
- else
- raise ArgumentError, "object[] naming but object param and @object var don't exist or don't respond to to_param: #{object.inspect}"
- end
- end
+ @object_name.sub!(/\[\]$/,"") || @object_name.sub!(/\[\]\]$/,"]")
+ @object = retrieve_object(object)
+ @auto_index = retrieve_autoindex(Regexp.last_match.pre_match) if Regexp.last_match
end
def to_label_tag(text = nil, options = {}, &block)
@@ -905,7 +959,7 @@ module ActionView
end
options["type"] ||= field_type
options["value"] = options.fetch("value"){ value_before_type_cast(object) } unless field_type == "file"
- options["value"] &&= html_escape(options["value"])
+ options["value"] &&= ERB::Util.html_escape(options["value"])
add_default_name_and_id(options)
tag("input", options)
end
@@ -941,7 +995,7 @@ module ActionView
options["cols"], options["rows"] = size.split("x") if size.respond_to?(:split)
end
- content_tag("textarea", html_escape(options.delete('value') || value_before_type_cast(object)), options)
+ content_tag("textarea", ERB::Util.html_escape(options.delete('value') || value_before_type_cast(object)), options)
end
def to_check_box_tag(options = {}, checked_value = "1", unchecked_value = "0")
@@ -983,14 +1037,26 @@ module ActionView
content_tag(tag_name, value(object), options)
end
- def object
- @object || @template_object.instance_variable_get("@#{@object_name}")
+ def retrieve_object(object)
+ if object
+ object
+ elsif @template_object.instance_variable_defined?("@#{@object_name}")
+ @template_object.instance_variable_get("@#{@object_name}")
+ end
rescue NameError
- # As @object_name may contain the nested syntax (item[subobject]) we
- # need to fallback to nil.
+ # As @object_name may contain the nested syntax (item[subobject]) we need to fallback to nil.
nil
end
+ def retrieve_autoindex(pre_match)
+ object = self.object || @template_object.instance_variable_get("@#{pre_match}")
+ if object && object.respond_to?(:to_param)
+ object.to_param
+ else
+ raise ArgumentError, "object[] naming but object param and @object var don't exist or don't respond to to_param: #{object.inspect}"
+ end
+ end
+
def value(object)
self.class.value(object, @method_name)
end
@@ -999,16 +1065,16 @@ module ActionView
self.class.value_before_type_cast(object, @method_name)
end
- module ClassMethods
+ class << self
def value(object, method_name)
- object.send method_name unless object.nil?
+ object.send method_name if object
end
def value_before_type_cast(object, method_name)
unless object.nil?
- object.respond_to?(method_name) ?
- object.send(method_name) :
- object.send(method_name + "_before_type_cast")
+ object.respond_to?(method_name + "_before_type_cast") ?
+ object.send(method_name + "_before_type_cast") :
+ object.send(method_name)
end
end
@@ -1085,17 +1151,21 @@ module ActionView
end
end
- class InstanceTag
- include InstanceTagMethods
- end
-
- class FormBuilder #:nodoc:
+ class FormBuilder
# The methods which wrap a form helper call.
- class_inheritable_accessor :field_helpers
+ class_attribute :field_helpers
self.field_helpers = (FormHelper.instance_method_names - ['form_for'])
attr_accessor :object_name, :object, :options
+ attr_reader :multipart, :parent_builder
+ alias :multipart? :multipart
+
+ def multipart=(multipart)
+ @multipart = multipart
+ parent_builder.multipart = multipart if parent_builder
+ end
+
def self.model_name
@model_name ||= Struct.new(:partial_path).new(name.demodulize.underscore.sub!(/_builder$/, ''))
end
@@ -1107,6 +1177,7 @@ module ActionView
def initialize(object_name, object, template, options, proc)
@nested_child_index = {}
@object_name, @object, @template, @options, @proc = object_name, object, template, options, proc
+ @parent_builder = options[:parent_builder]
@default_options = @options ? @options.slice(:index) : {}
if @object_name.to_s.match(/\[\]$/)
if object ||= @template.instance_variable_get("@#{Regexp.last_match.pre_match}") and object.respond_to?(:to_param)
@@ -1115,9 +1186,10 @@ module ActionView
raise ArgumentError, "object[] naming but object param and @object var don't exist or don't respond to to_param: #{object.inspect}"
end
end
+ @multipart = nil
end
- (field_helpers - %w(label check_box radio_button fields_for hidden_field)).each do |selector|
+ (field_helpers - %w(label check_box radio_button fields_for hidden_field file_field)).each do |selector|
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
def #{selector}(method, options = {}) # def text_field(method, options = {})
@template.send( # @template.send(
@@ -1139,27 +1211,23 @@ module ActionView
index = ""
end
- if options[:builder]
- args << {} unless args.last.is_a?(Hash)
- args.last[:builder] ||= options[:builder]
- end
+ args << {} unless args.last.is_a?(Hash)
+ args.last[:builder] ||= options[:builder]
+ args.last[:parent_builder] = self
case record_or_name_or_array
when String, Symbol
if nested_attributes_association?(record_or_name_or_array)
return fields_for_with_nested_attributes(record_or_name_or_array, args, block)
else
- name = "#{object_name}#{index}[#{record_or_name_or_array}]"
+ name = record_or_name_or_array
end
- when Array
- object = record_or_name_or_array.last
- name = "#{object_name}#{index}[#{ActiveModel::Naming.singular(object)}]"
- args.unshift(object)
else
- object = record_or_name_or_array
- name = "#{object_name}#{index}[#{ActiveModel::Naming.singular(object)}]"
+ object = record_or_name_or_array.is_a?(Array) ? record_or_name_or_array.last : record_or_name_or_array
+ name = ActiveModel::Naming.param_key(object)
args.unshift(object)
end
+ name = "#{object_name}#{index}[#{name}]"
@template.fields_for(name, *args, &block)
end
@@ -1181,6 +1249,11 @@ module ActionView
@template.hidden_field(@object_name, method, objectify_options(options))
end
+ def file_field(method, options = {})
+ self.multipart = true
+ @template.file_field(@object_name, method, objectify_options(options))
+ end
+
# Add the submit button for the given form. When no value is given, it checks
# if the object is a new resource or not to create the proper label:
#
@@ -1211,11 +1284,11 @@ module ActionView
def submit(value=nil, options={})
value, options = nil, value if value.is_a?(Hash)
value ||= submit_default_value
- @template.submit_tag(value, options.reverse_merge(:id => "#{object_name}_submit"))
+ @template.submit_tag(value, options)
end
def emitted_hidden_id?
- @emitted_hidden_id
+ @emitted_hidden_id ||= nil
end
private
@@ -1224,7 +1297,7 @@ module ActionView
end
def submit_default_value
- object = @object.respond_to?(:to_model) ? @object.to_model : @object
+ object = convert_to_model(@object)
key = object ? (object.persisted? ? :update : :create) : :submit
model = if object.class.respond_to?(:model_name)
@@ -1249,15 +1322,15 @@ module ActionView
name = "#{object_name}[#{association_name}_attributes]"
options = args.extract_options!
association = args.shift
- association = association.to_model if association.respond_to?(:to_model)
+ association = convert_to_model(association)
if association.respond_to?(:persisted?)
association = [association] if @object.send(association_name).is_a?(Array)
- elsif !association.is_a?(Array)
+ elsif !association.respond_to?(:to_ary)
association = @object.send(association_name)
end
- if association.is_a?(Array)
+ if association.respond_to?(:to_ary)
explicit_child_index = options[:child_index]
output = ActiveSupport::SafeBuffer.new
association.each do |child|
@@ -1270,22 +1343,22 @@ module ActionView
end
def fields_for_nested_model(name, object, options, block)
- object = object.to_model if object.respond_to?(:to_model)
+ object = convert_to_model(object)
- if object.persisted?
- @template.fields_for(name, object, options) do |builder|
- block.call(builder)
- @template.concat builder.hidden_field(:id) unless builder.emitted_hidden_id?
- end
- else
- @template.fields_for(name, object, options, &block)
- end
+ parent_include_id = self.options.fetch(:include_id, true)
+ include_id = options.fetch(:include_id, parent_include_id)
+ options[:hidden_field_id] = object.persisted? && include_id
+ @template.fields_for(name, object, options, &block)
end
def nested_child_index(name)
@nested_child_index[name] ||= -1
@nested_child_index[name] += 1
end
+
+ def convert_to_model(object)
+ object.respond_to?(:to_model) ? object.to_model : object
+ end
end
end
diff --git a/actionpack/lib/action_view/helpers/form_options_helper.rb b/actionpack/lib/action_view/helpers/form_options_helper.rb
index ee34452769..7698602022 100644
--- a/actionpack/lib/action_view/helpers/form_options_helper.rb
+++ b/actionpack/lib/action_view/helpers/form_options_helper.rb
@@ -2,6 +2,7 @@ require 'cgi'
require 'erb'
require 'action_view/helpers/form_helper'
require 'active_support/core_ext/object/blank'
+require 'active_support/core_ext/string/output_safety'
module ActionView
# = Action View Form Option Helpers
@@ -53,16 +54,16 @@ module ActionView
# <option value="2">Sam</option>
# <option value="3">Tobias</option>
# </select>
- #
- # Like the other form helpers, +select+ can accept an <tt>:index</tt> option to manually set the ID used in the resulting output. Unlike other helpers, +select+ expects this
+ #
+ # Like the other form helpers, +select+ can accept an <tt>:index</tt> option to manually set the ID used in the resulting output. Unlike other helpers, +select+ expects this
# option to be in the +html_options+ parameter.
- #
- # Example:
- #
+ #
+ # Example:
+ #
# select("album[]", "genre", %w[rap rock country], {}, { :index => nil })
- #
+ #
# becomes:
- #
+ #
# <select name="album[][genre]" id="album__genre">
# <option value="rap">rap</option>
# <option value="rock">rock</option>
@@ -100,7 +101,6 @@ module ActionView
#
module FormOptionsHelper
# ERB::Util can mask some helpers like textilize. Make sure to include them.
- include ERB::Util
include TextHelper
# Create a select tag and a series of contained option tags for the provided object and method.
@@ -140,7 +140,7 @@ module ActionView
# The <tt>:value_method</tt> and <tt>:text_method</tt> parameters are methods to be called on each member
# of +collection+. The return values are used as the +value+ attribute and contents of each
# <tt><option></tt> tag, respectively.
- #
+ #
# Example object structure for use with this method:
# class Post < ActiveRecord::Base
# belongs_to :author
@@ -247,7 +247,7 @@ module ActionView
#
# time_zone_select( "user", 'time_zone', /Australia/)
#
- # time_zone_select( "user", "time_zone", ActiveSupport::Timezone.all.sort, :model => ActiveSupport::Timezone)
+ # time_zone_select( "user", "time_zone", ActiveSupport::TimeZone.all.sort, :model => ActiveSupport::TimeZone)
def time_zone_select(object, method, priority_zones = nil, options = {}, html_options = {})
InstanceTag.new(object, method, self, options.delete(:object)).to_time_zone_select_tag(priority_zones, options, html_options)
end
@@ -297,18 +297,18 @@ module ActionView
def options_for_select(container, selected = nil)
return container if String === container
- container = container.to_a if Hash === container
- selected, disabled = extract_selected_and_disabled(selected)
+ selected, disabled = extract_selected_and_disabled(selected).map do | r |
+ Array.wrap(r).map(&:to_s)
+ end
- options_for_select = container.inject([]) do |options, element|
+ container.map do |element|
html_attributes = option_html_attributes(element)
- text, value = option_text_and_value(element)
+ text, value = option_text_and_value(element).map(&:to_s)
selected_attribute = ' selected="selected"' if option_value_selected?(value, selected)
disabled_attribute = ' disabled="disabled"' if disabled && option_value_selected?(value, disabled)
- options << %(<option value="#{html_escape(value.to_s)}"#{selected_attribute}#{disabled_attribute}#{html_attributes}>#{html_escape(text.to_s)}</option>)
- end
+ %(<option value="#{ERB::Util.html_escape(value)}"#{selected_attribute}#{disabled_attribute}#{html_attributes}>#{ERB::Util.html_escape(text)}</option>)
+ end.join("\n").html_safe
- options_for_select.join("\n").html_safe
end
# Returns a string of option tags that have been compiled by iterating over the +collection+ and assigning the
@@ -394,12 +394,12 @@ module ActionView
# <b>Note:</b> Only the <tt><optgroup></tt> and <tt><option></tt> tags are returned, so you still have to
# wrap the output in an appropriate <tt><select></tt> tag.
def option_groups_from_collection_for_select(collection, group_method, group_label_method, option_key_method, option_value_method, selected_key = nil)
- collection.inject("") do |options_for_select, group|
+ collection.map do |group|
group_label_string = eval("group.#{group_label_method}")
- options_for_select += "<optgroup label=\"#{html_escape(group_label_string)}\">"
- options_for_select += options_from_collection_for_select(eval("group.#{group_method}"), option_key_method, option_value_method, selected_key)
- options_for_select += '</optgroup>'
- end.html_safe
+ "<optgroup label=\"#{ERB::Util.html_escape(group_label_string)}\">" +
+ options_from_collection_for_select(eval("group.#{group_method}"), option_key_method, option_value_method, selected_key) +
+ '</optgroup>'
+ end.join.html_safe
end
# Returns a string of <tt><option></tt> tags, like <tt>options_for_select</tt>, but
@@ -493,7 +493,7 @@ module ActionView
end
zone_options += options_for_select(convert_zones[zones], selected)
- zone_options
+ zone_options.html_safe
end
private
@@ -501,7 +501,7 @@ module ActionView
return "" unless Array === element
html_attributes = []
element.select { |e| Hash === e }.reduce({}, :merge).each do |k, v|
- html_attributes << " #{k}=\"#{html_escape(v.to_s)}\""
+ html_attributes << " #{k}=\"#{ERB::Util.html_escape(v.to_s)}\""
end
html_attributes.join
end
@@ -528,10 +528,12 @@ module ActionView
end
def extract_selected_and_disabled(selected)
- if selected.is_a?(Hash)
- [selected[:selected], selected[:disabled]]
+ if selected.is_a?(Proc)
+ [ selected, nil ]
else
- [selected, nil]
+ selected = Array.wrap(selected)
+ options = selected.extract_options!.symbolize_keys
+ [ options.include?(:selected) ? options[:selected] : selected, options[:disabled] ]
end
end
@@ -593,11 +595,11 @@ module ActionView
private
def add_options(option_tags, options, value = nil)
if options[:include_blank]
- option_tags = "<option value=\"\">#{html_escape(options[:include_blank]) if options[:include_blank].kind_of?(String)}</option>\n" + option_tags
+ option_tags = "<option value=\"\">#{ERB::Util.html_escape(options[:include_blank]) if options[:include_blank].kind_of?(String)}</option>\n" + option_tags
end
if value.blank? && options[:prompt]
prompt = options[:prompt].kind_of?(String) ? options[:prompt] : I18n.translate('helpers.select.prompt', :default => 'Please select')
- option_tags = "<option value=\"\">#{html_escape(prompt)}</option>\n" + option_tags
+ option_tags = "<option value=\"\">#{ERB::Util.html_escape(prompt)}</option>\n" + option_tags
end
option_tags.html_safe
end
diff --git a/actionpack/lib/action_view/helpers/form_tag_helper.rb b/actionpack/lib/action_view/helpers/form_tag_helper.rb
index 1ea870426a..49aa434020 100644
--- a/actionpack/lib/action_view/helpers/form_tag_helper.rb
+++ b/actionpack/lib/action_view/helpers/form_tag_helper.rb
@@ -1,6 +1,7 @@
require 'cgi'
require 'action_view/helpers/tag_helper'
require 'active_support/core_ext/object/blank'
+require 'active_support/core_ext/string/output_safety'
module ActionView
# = Action View Form Tag Helpers
@@ -24,8 +25,11 @@ module ActionView
# * <tt>:method</tt> - The method to use when submitting the form, usually either "get" or "post".
# If "put", "delete", or another verb is used, a hidden input with name <tt>_method</tt>
# is added to simulate the verb over post.
+ # * <tt>:authenticity_token</tt> - Authenticity token to use in the form. Use only if you need to
+ # pass custom authenticity token string, or to not add authenticity_token field at all
+ # (by passing <tt>false</tt>).
# * A list of parameters to feed to the URL the form will be posted to.
- # * <tt>:remote</tt> - If set to true, will allow the Unobtrusive JavaScript drivers to control the
+ # * <tt>:remote</tt> - If set to true, will allow the Unobtrusive JavaScript drivers to control the
# submit behaviour. By default this behaviour is an ajax submit.
#
# ==== Examples
@@ -38,14 +42,20 @@ module ActionView
# form_tag('/upload', :multipart => true)
# # => <form action="/upload" method="post" enctype="multipart/form-data">
#
- # <%= form_tag('/posts')do -%>
+ # <%= form_tag('/posts') do -%>
# <div><%= submit_tag 'Save' %></div>
# <% end -%>
# # => <form action="/posts" method="post"><div><input type="submit" name="submit" value="Save" /></div></form>
- #
+ #
# <%= form_tag('/posts', :remote => true) %>
# # => <form action="/posts" method="post" data-remote="true">
- #
+ #
+ # form_tag('http://far.away.com/form', :authenticity_token => false)
+ # # form without authenticity token
+ #
+ # form_tag('http://far.away.com/form', :authenticity_token => "cf50faa3fe97702ca1ae")
+ # # form with custom authenticity token
+ #
def form_tag(url_for_options = {}, options = {}, *parameters_for_url, &block)
html_options = html_options_for_form(url_for_options, options, *parameters_for_url)
if block_given?
@@ -67,7 +77,7 @@ module ActionView
# * Any other key creates standard HTML attributes for the tag.
#
# ==== Examples
- # select_tag "people", options_from_collection_for_select(@people, "name", "id")
+ # select_tag "people", options_from_collection_for_select(@people, "id", "name")
# # <select id="people" name="people"><option value="1">David</option></select>
#
# select_tag "people", "<option>David</option>"
@@ -93,10 +103,6 @@ module ActionView
# # => <select disabled="disabled" id="destination" name="destination"><option>NYC</option>
# # <option>Paris</option><option>Rome</option></select>
def select_tag(name, option_tags = nil, options = {})
- if Array === option_tags
- ActiveSupport::Deprecation.warn 'Passing an array of option_tags to select_tag implicitly joins them without marking them as HTML-safe. Pass option_tags.join.html_safe instead.', caller
- end
-
html_name = (options[:multiple] == true && !name.to_s.ends_with?("[]")) ? "#{name}[]" : name
if blank = options.delete(:include_blank)
if blank.kind_of?(String)
@@ -115,6 +121,7 @@ module ActionView
# * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
# * <tt>:size</tt> - The number of visible characters that will fit in the input.
# * <tt>:maxlength</tt> - The maximum number of characters that the browser will allow the user to enter.
+ # * <tt>:placeholder</tt> - The text contained in the field by default which is removed when the field receives focus.
# * Any other key creates standard HTML attributes for the tag.
#
# ==== Examples
@@ -124,6 +131,9 @@ module ActionView
# text_field_tag 'query', 'Enter your search query here'
# # => <input id="query" name="query" type="text" value="Enter your search query here" />
#
+ # text_field_tag 'search', nil, :placeholder => 'Enter search term...'
+ # # => <input id="search" name="search" placeholder="Enter search term..." type="text" />
+ #
# text_field_tag 'request', nil, :class => 'special_input'
# # => <input class="special_input" id="request" name="request" type="text" />
#
@@ -291,7 +301,7 @@ module ActionView
end
escape = options.key?("escape") ? options.delete("escape") : true
- content = html_escape(content) if escape
+ content = ERB::Util.html_escape(content) if escape
content_tag :textarea, content.to_s.html_safe, { "name" => name, "id" => sanitize_to_id(name) }.update(options)
end
@@ -351,12 +361,12 @@ module ActionView
# Creates a submit button with the text <tt>value</tt> as the caption.
#
# ==== Options
- # * <tt>:confirm => 'question?'</tt> - If present the unobtrusive JavaScript
- # drivers will provide a prompt with the question specified. If the user accepts,
+ # * <tt>:confirm => 'question?'</tt> - If present the unobtrusive JavaScript
+ # drivers will provide a prompt with the question specified. If the user accepts,
# the form is processed normally, otherwise no action is taken.
# * <tt>:disabled</tt> - If true, the user will not be able to use this input.
- # * <tt>:disable_with</tt> - Value of this parameter will be used as the value for a
- # disabled version of the submit button when the form is submitted. This feature is
+ # * <tt>:disable_with</tt> - Value of this parameter will be used as the value for a
+ # disabled version of the submit button when the form is submitted. This feature is
# provided by the unobtrusive JavaScript driver.
# * Any other key creates standard HTML options for the tag.
#
@@ -383,7 +393,7 @@ module ActionView
# # name="commit" type="submit" value="Edit" />
#
# submit_tag "Save", :confirm => "Are you sure?"
- # # => <input name='commit' type='submit' value='Save'
+ # # => <input name='commit' type='submit' value='Save'
# data-confirm="Are you sure?" />
#
def submit_tag(value = "Save changes", options = {})
@@ -394,12 +404,65 @@ module ActionView
end
if confirm = options.delete("confirm")
- add_confirm_to_attributes!(options, confirm)
+ options["data-confirm"] = confirm
end
tag :input, { "type" => "submit", "name" => "commit", "value" => value }.update(options.stringify_keys)
end
+ # Creates a button element that defines a <tt>submit</tt> button,
+ # <tt>reset</tt>button or a generic button which can be used in
+ # JavaScript, for example. You can use the button tag as a regular
+ # submit tag but it isn't supported in legacy browsers. However,
+ # the button tag allows richer labels such as images and emphasis,
+ # so this helper will also accept a block.
+ #
+ # ==== Options
+ # * <tt>:confirm => 'question?'</tt> - If present, the
+ # unobtrusive JavaScript drivers will provide a prompt with
+ # the question specified. If the user accepts, the form is
+ # processed normally, otherwise no action is taken.
+ # * <tt>:disabled</tt> - If true, the user will not be able to
+ # use this input.
+ # * <tt>:disable_with</tt> - Value of this parameter will be
+ # used as the value for a disabled version of the submit
+ # button when the form is submitted. This feature is provided
+ # by the unobtrusive JavaScript driver.
+ # * Any other key creates standard HTML options for the tag.
+ #
+ # ==== Examples
+ # button_tag
+ # # => <button name="button" type="submit">Button</button>
+ #
+ # button_tag(:type => 'button') do
+ # content_tag(:strong, 'Ask me!')
+ # end
+ # # => <button name="button" type="button">
+ # <strong>Ask me!</strong>
+ # </button>
+ #
+ # button_tag "Checkout", :disable_with => "Please wait..."
+ # # => <button data-disable-with="Please wait..." name="button"
+ # type="submit">Checkout</button>
+ #
+ def button_tag(content_or_options = nil, options = nil, &block)
+ options = content_or_options if block_given? && content_or_options.is_a?(Hash)
+ options ||= {}
+ options.stringify_keys!
+
+ if disable_with = options.delete("disable_with")
+ options["data-disable-with"] = disable_with
+ end
+
+ if confirm = options.delete("confirm")
+ options["data-confirm"] = confirm
+ end
+
+ options.reverse_merge! 'name' => 'button', 'type' => 'submit'
+
+ content_tag :button, content_or_options || 'Button', options, &block
+ end
+
# Displays an image which when clicked will submit the form.
#
# <tt>source</tt> is passed to AssetTagHelper#path_to_image
@@ -427,7 +490,7 @@ module ActionView
options.stringify_keys!
if confirm = options.delete("confirm")
- add_confirm_to_attributes!(options, confirm)
+ options["data-confirm"] = confirm
end
tag :input, { "type" => "image", "src" => path_to_image(source) }.update(options.stringify_keys)
@@ -529,17 +592,19 @@ module ActionView
options.stringify_keys.tap do |html_options|
html_options["enctype"] = "multipart/form-data" if html_options.delete("multipart")
# The following URL is unescaped, this is just a hash of options, and it is the
- # responsability of the caller to escape all the values.
+ # responsibility of the caller to escape all the values.
html_options["action"] = url_for(url_for_options, *parameters_for_url)
html_options["accept-charset"] = "UTF-8"
html_options["data-remote"] = true if html_options.delete("remote")
+ html_options["authenticity_token"] = html_options.delete("authenticity_token") if html_options.has_key?("authenticity_token")
end
end
def extra_tags_for_form(html_options)
snowman_tag = tag(:input, :type => "hidden",
- :name => "_e", :value => "&#9731;".html_safe)
+ :name => "utf8", :value => "&#x2713;".html_safe)
+ authenticity_token = html_options.delete("authenticity_token")
method = html_options.delete("method").to_s
method_tag = case method
@@ -548,10 +613,10 @@ module ActionView
''
when /^post$/i, "", nil
html_options["method"] = "post"
- token_tag
+ token_tag(authenticity_token)
else
html_options["method"] = "post"
- tag(:input, :type => "hidden", :name => "_method", :value => method) + token_tag
+ tag(:input, :type => "hidden", :name => "_method", :value => method) + token_tag(authenticity_token)
end
tags = snowman_tag << method_tag
@@ -571,11 +636,12 @@ module ActionView
output.safe_concat("</form>")
end
- def token_tag
- unless protect_against_forgery?
+ def token_tag(token)
+ if token == false || !protect_against_forgery?
''
else
- tag(:input, :type => "hidden", :name => request_forgery_protection_token.to_s, :value => form_authenticity_token)
+ token = form_authenticity_token if token.nil?
+ tag(:input, :type => "hidden", :name => request_forgery_protection_token.to_s, :value => token)
end
end
diff --git a/actionpack/lib/action_view/helpers/javascript_helper.rb b/actionpack/lib/action_view/helpers/javascript_helper.rb
index 84f53b842c..cd3a3eac80 100644
--- a/actionpack/lib/action_view/helpers/javascript_helper.rb
+++ b/actionpack/lib/action_view/helpers/javascript_helper.rb
@@ -65,7 +65,8 @@ module ActionView
# //]]>
# </script>
#
- # +html_options+ may be a hash of attributes for the <script> tag. Example:
+ # +html_options+ may be a hash of attributes for the <tt>\<script></tt>
+ # tag. Example:
# javascript_tag "alert('All is good')", :defer => 'defer'
# # => <script defer="defer" type="text/javascript">alert('All is good')</script>
#
diff --git a/actionpack/lib/action_view/helpers/number_helper.rb b/actionpack/lib/action_view/helpers/number_helper.rb
index f11027bc93..05a9c5b4f1 100644
--- a/actionpack/lib/action_view/helpers/number_helper.rb
+++ b/actionpack/lib/action_view/helpers/number_helper.rb
@@ -1,6 +1,7 @@
require 'active_support/core_ext/big_decimal/conversions'
require 'active_support/core_ext/float/rounding'
require 'active_support/core_ext/object/blank'
+require 'active_support/core_ext/string/output_safety'
module ActionView
# = Action View Number Helpers
@@ -14,7 +15,7 @@ module ActionView
# unchanged if can't be converted into a valid number.
module NumberHelper
- DEFAULT_CURRENCY_VALUES = { :format => "%u%n", :unit => "$", :separator => ".", :delimiter => ",",
+ DEFAULT_CURRENCY_VALUES = { :format => "%u%n", :negative_format => "-%u%n", :unit => "$", :separator => ".", :delimiter => ",",
:precision => 2, :significant => false, :strip_insignificant_zeros => false }
# Raised when argument +number+ param given to the helpers is invalid and
@@ -47,51 +48,51 @@ module ActionView
# number_to_phone(1235551234, :country_code => 1, :extension => 1343, :delimiter => ".")
# => +1.123.555.1234 x 1343
def number_to_phone(number, options = {})
- return nil if number.nil?
+ return unless number
begin
Float(number)
- is_number_html_safe = true
rescue ArgumentError, TypeError
- if options[:raise]
- raise InvalidNumberError, number
- else
- is_number_html_safe = number.to_s.html_safe?
- end
- end
+ raise InvalidNumberError, number
+ end if options[:raise]
number = number.to_s.strip
options = options.symbolize_keys
- area_code = options[:area_code] || nil
+ area_code = options[:area_code]
delimiter = options[:delimiter] || "-"
- extension = options[:extension].to_s.strip || nil
- country_code = options[:country_code] || nil
+ extension = options[:extension]
+ country_code = options[:country_code]
- str = ""
- str << "+#{country_code}#{delimiter}" unless country_code.blank?
- str << if area_code
- number.gsub!(/([0-9]{1,3})([0-9]{3})([0-9]{4}$)/,"(\\1) \\2#{delimiter}\\3")
+ if area_code
+ number.gsub!(/(\d{1,3})(\d{3})(\d{4}$)/,"(\\1) \\2#{delimiter}\\3")
else
- number.gsub!(/([0-9]{0,3})([0-9]{3})([0-9]{4})$/,"\\1#{delimiter}\\2#{delimiter}\\3")
- number.starts_with?('-') ? number.slice!(1..-1) : number
+ number.gsub!(/(\d{0,3})(\d{3})(\d{4})$/,"\\1#{delimiter}\\2#{delimiter}\\3")
+ number.slice!(0, 1) if number.starts_with?('-')
end
+
+ str = []
+ str << "+#{country_code}#{delimiter}" unless country_code.blank?
+ str << number
str << " x #{extension}" unless extension.blank?
- is_number_html_safe ? str.html_safe : str
+ ERB::Util.html_escape(str.join)
end
# Formats a +number+ into a currency string (e.g., $13.65). You can customize the format
# in the +options+ hash.
#
# ==== Options
- # * <tt>:locale</tt> - Sets the locale to be used for formatting (defaults to current locale).
- # * <tt>:precision</tt> - Sets the level of precision (defaults to 2).
- # * <tt>:unit</tt> - Sets the denomination of the currency (defaults to "$").
- # * <tt>:separator</tt> - Sets the separator between the units (defaults to ".").
- # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults to ",").
- # * <tt>:format</tt> - Sets the format of the output string (defaults to "%u%n"). The field types are:
- #
- # %u The currency unit
- # %n The number
+ # * <tt>:locale</tt> - Sets the locale to be used for formatting (defaults to current locale).
+ # * <tt>:precision</tt> - Sets the level of precision (defaults to 2).
+ # * <tt>:unit</tt> - Sets the denomination of the currency (defaults to "$").
+ # * <tt>:separator</tt> - Sets the separator between the units (defaults to ".").
+ # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults to ",").
+ # * <tt>:format</tt> - Sets the format for non-negative numbers (defaults to "%u%n").
+ # Fields are <tt>%u</tt> for the currency, and <tt>%n</tt>
+ # for the number.
+ # * <tt>:negative_format</tt> - Sets the format for negative numbers (defaults to prepending
+ # an hyphen to the formatted number given by <tt>:format</tt>).
+ # Accepts the same fields than <tt>:format</tt>, except
+ # <tt>%n</tt> is here the absolute value of the number.
#
# ==== Examples
# number_to_currency(1234567890.50) # => $1,234,567,890.50
@@ -99,12 +100,14 @@ module ActionView
# number_to_currency(1234567890.506, :precision => 3) # => $1,234,567,890.506
# number_to_currency(1234567890.506, :locale => :fr) # => 1 234 567 890,506 €
#
+ # number_to_currency(1234567890.50, :negative_format => "(%u%n)")
+ # # => ($1,234,567,890.51)
# number_to_currency(1234567890.50, :unit => "&pound;", :separator => ",", :delimiter => "")
# # => &pound;1234567890,50
# number_to_currency(1234567890.50, :unit => "&pound;", :separator => ",", :delimiter => "", :format => "%n %u")
# # => 1234567890,50 &pound;
def number_to_currency(number, options = {})
- return nil if number.nil?
+ return unless number
options.symbolize_keys!
@@ -112,11 +115,17 @@ module ActionView
currency = I18n.translate(:'number.currency.format', :locale => options[:locale], :default => {})
defaults = DEFAULT_CURRENCY_VALUES.merge(defaults).merge!(currency)
+ defaults[:negative_format] = "-" + options[:format] if options[:format]
options = defaults.merge!(options)
unit = options.delete(:unit)
format = options.delete(:format)
+ if number.to_f < 0
+ format = options.delete(:negative_format)
+ number = number.respond_to?("abs") ? number.abs : number.sub(/^-/, '')
+ end
+
begin
value = number_with_precision(number, options.merge(:raise => true))
format.gsub(/%n/, value).gsub(/%u/, unit).html_safe
@@ -149,7 +158,7 @@ module ActionView
# number_to_percentage(302.24398923423, :precision => 5) # => 302.24399%
# number_to_percentage(1000, :locale => :fr) # => 1 000,000%
def number_to_percentage(number, options = {})
- return nil if number.nil?
+ return unless number
options.symbolize_keys!
@@ -260,13 +269,14 @@ module ActionView
if number == 0
digits, rounded_number = 1, 0
else
- digits = (Math.log10(number) + 1).floor
- rounded_number = BigDecimal.new((number / 10 ** (digits - precision)).to_s).round.to_f * 10 ** (digits - precision)
+ digits = (Math.log10(number.abs) + 1).floor
+ rounded_number = (BigDecimal.new(number.to_s) / BigDecimal.new((10 ** (digits - precision)).to_f.to_s)).round.to_f * 10 ** (digits - precision)
+ digits = (Math.log10(rounded_number.abs) + 1).floor # After rounding, the number of digits may have changed
end
- precision = precision - digits
+ precision -= digits
precision = precision > 0 ? precision : 0 #don't let it be negative
else
- rounded_number = BigDecimal.new((number * (10 ** precision)).to_s).round.to_f / 10 ** precision
+ rounded_number = BigDecimal.new(number.to_s).round(precision).to_f
end
formatted_number = number_with_delimiter("%01.#{precision}f" % rounded_number, options)
if strip_insignificant_zeros
@@ -325,7 +335,7 @@ module ActionView
defaults = I18n.translate(:'number.format', :locale => options[:locale], :default => {})
human = I18n.translate(:'number.human.format', :locale => options[:locale], :default => {})
defaults = defaults.merge(human)
-
+
options = options.reverse_merge(defaults)
#for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files
options[:strip_insignificant_zeros] = true if not options.key?(:strip_insignificant_zeros)
@@ -359,7 +369,7 @@ module ActionView
# See <tt>number_to_human_size</tt> if you want to print a file size.
#
# You can also define you own unit-quantifier names if you want to use other decimal units
- # (eg.: 1500 becomes "1.5 kilometers", 0.150 becomes "150 mililiters", etc). You may define
+ # (eg.: 1500 becomes "1.5 kilometers", 0.150 becomes "150 milliliters", etc). You may define
# a wide range of unit quantifiers, even fractional ones (centi, deci, mili, etc).
#
# ==== Options
@@ -415,13 +425,13 @@ module ActionView
# thousand:
# one: "kilometer"
# other: "kilometers"
- # billion: "gazilion-distance"
+ # billion: "gazillion-distance"
#
# Then you could do:
#
# number_to_human(543934, :units => :distance) # => "544 kilometers"
# number_to_human(54393498, :units => :distance) # => "54400 kilometers"
- # number_to_human(54393498000, :units => :distance) # => "54.4 gazilion-distance"
+ # number_to_human(54393498000, :units => :distance) # => "54.4 gazillion-distance"
# number_to_human(343, :units => :distance, :precision => 1) # => "300 meters"
# number_to_human(1, :units => :distance) # => "1 meter"
# number_to_human(0.34, :units => :distance) # => "34 centimeters"
@@ -447,6 +457,8 @@ module ActionView
#for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files
options[:strip_insignificant_zeros] = true if not options.key?(:strip_insignificant_zeros)
+ inverted_du = DECIMAL_UNITS.invert
+
units = options.delete :units
unit_exponents = case units
when Hash
@@ -457,10 +469,10 @@ module ActionView
I18n.translate(:"number.human.decimal_units.units", :locale => options[:locale], :raise => true)
else
raise ArgumentError, ":units must be a Hash or String translation scope."
- end.keys.map{|e_name| DECIMAL_UNITS.invert[e_name] }.sort_by{|e| -e}
+ end.keys.map{|e_name| inverted_du[e_name] }.sort_by{|e| -e}
- number_exponent = Math.log10(number).floor
- display_exponent = unit_exponents.find{|e| number_exponent >= e }
+ number_exponent = number != 0 ? Math.log10(number.abs).floor : 0
+ display_exponent = unit_exponents.find{ |e| number_exponent >= e } || 0
number /= 10 ** display_exponent
unit = case units
diff --git a/actionpack/lib/action_view/helpers/output_safety_helper.rb b/actionpack/lib/action_view/helpers/output_safety_helper.rb
new file mode 100644
index 0000000000..a035dd70ad
--- /dev/null
+++ b/actionpack/lib/action_view/helpers/output_safety_helper.rb
@@ -0,0 +1,38 @@
+require 'active_support/core_ext/string/output_safety'
+
+module ActionView #:nodoc:
+ # = Action View Raw Output Helper
+ module Helpers #:nodoc:
+ module OutputSafetyHelper
+ # This method outputs without escaping a string. Since escaping tags is
+ # now default, this can be used when you don't want Rails to automatically
+ # escape tags. This is not recommended if the data is coming from the user's
+ # input.
+ #
+ # For example:
+ #
+ # <%=raw @user.name %>
+ def raw(stringish)
+ stringish.to_s.html_safe
+ end
+
+ # This method returns a html safe string similar to what <tt>Array#join</tt>
+ # would return. All items in the array, including the supplied separator, are
+ # html escaped unless they are html safe, and the returned string is marked
+ # as html safe.
+ #
+ # safe_join(["<p>foo</p>".html_safe, "<p>bar</p>"], "<br />")
+ # # => "<p>foo</p>&lt;br /&gt;&lt;p&gt;bar&lt;/p&gt;"
+ #
+ # safe_join(["<p>foo</p>".html_safe, "<p>bar</p>".html_safe], "<br />".html_safe)
+ # # => "<p>foo</p><br /><p>bar</p>"
+ #
+ def safe_join(array, sep=$,)
+ sep ||= "".html_safe
+ sep = ERB::Util.html_escape(sep)
+
+ array.map { |i| ERB::Util.html_escape(i) }.join(sep).html_safe
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/actionpack/lib/action_view/helpers/prototype_helper.rb b/actionpack/lib/action_view/helpers/prototype_helper.rb
index 99f9363a9a..18e303778c 100644
--- a/actionpack/lib/action_view/helpers/prototype_helper.rb
+++ b/actionpack/lib/action_view/helpers/prototype_helper.rb
@@ -1,6 +1,7 @@
require 'set'
require 'active_support/json'
require 'active_support/core_ext/object/blank'
+require 'active_support/core_ext/string/output_safety'
module ActionView
# = Action View Prototype Helpers
@@ -102,7 +103,7 @@ module ActionView
:form, :with, :update, :script, :type ]).merge(CALLBACKS)
# Returns the JavaScript needed for a remote function.
- # See the link_to_remote documentation at http://github.com/rails/prototype_legacy_helper as it takes the same arguments.
+ # See the link_to_remote documentation at https://github.com/rails/prototype_legacy_helper as it takes the same arguments.
#
# Example:
# # Generates: <select id="options" onchange="new Ajax.Updater('options',
@@ -130,8 +131,7 @@ module ActionView
"new Ajax.Updater(#{update}, "
url_options = options[:url]
- url_options = url_options.merge(:escape => false) if url_options.is_a?(Hash)
- function << "'#{html_escape(escape_javascript(url_for(url_options)))}'"
+ function << "'#{ERB::Util.html_escape(escape_javascript(url_for(url_options)))}'"
function << ", #{javascript_options})"
function = "#{options[:before]}; #{function}" if options[:before]
@@ -161,8 +161,8 @@ module ActionView
# JavaScriptGenerator generates blocks of JavaScript code that allow you
# to change the content and presentation of multiple DOM elements. Use
- # this in your Ajax response bodies, either in a <script> tag or as plain
- # JavaScript sent with a Content-type of "text/javascript".
+ # this in your Ajax response bodies, either in a <tt>\<script></tt> tag
+ # or as plain JavaScript sent with a Content-type of "text/javascript".
#
# Create new instances with PrototypeHelper#update_page or with
# ActionController::Base#render, then call +insert_html+, +replace_html+,
@@ -224,7 +224,7 @@ module ActionView
#
# You can also use PrototypeHelper#update_page_tag instead of
# PrototypeHelper#update_page to wrap the generated JavaScript in a
- # <script> tag.
+ # <tt>\<script></tt> tag.
module GeneratorMethods
def to_s #:nodoc:
(@lines * $/).tap do |javascript|
@@ -546,7 +546,7 @@ module ActionView
end
def with_formats(*args)
- @context ? @context.update_details(:formats => args) { yield } : yield
+ @context ? @context.lookup_context.update_details(:formats => args) { yield } : yield
end
def javascript_object_for(object)
@@ -579,15 +579,15 @@ module ActionView
# page.hide 'spinner'
# end
def update_page(&block)
- JavaScriptGenerator.new(view_context, &block).to_s.html_safe
+ JavaScriptGenerator.new(self, &block).to_s.html_safe
end
- # Works like update_page but wraps the generated JavaScript in a <script>
- # tag. Use this to include generated JavaScript in an ERb template.
- # See JavaScriptGenerator for more information.
+ # Works like update_page but wraps the generated JavaScript in a
+ # <tt>\<script></tt> tag. Use this to include generated JavaScript in an
+ # ERb template. See JavaScriptGenerator for more information.
#
- # +html_options+ may be a hash of <script> attributes to be passed
- # to ActionView::Helpers::JavaScriptHelper#javascript_tag.
+ # +html_options+ may be a hash of <tt>\<script></tt> attributes to be
+ # passed to ActionView::Helpers::JavaScriptHelper#javascript_tag.
def update_page_tag(html_options = {}, &block)
javascript_tag update_page(&block), html_options
end
diff --git a/actionpack/lib/action_view/helpers/raw_output_helper.rb b/actionpack/lib/action_view/helpers/raw_output_helper.rb
deleted file mode 100644
index da7599fa8f..0000000000
--- a/actionpack/lib/action_view/helpers/raw_output_helper.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-module ActionView #:nodoc:
- # = Action View Raw Output Helper
- module Helpers #:nodoc:
- module RawOutputHelper
- # This method outputs without escaping a string. Since escaping tags is
- # now default, this can be used when you don't want Rails to automatically
- # escape tags. This is not recommended if the data is coming from the user's
- # input.
- #
- # For example:
- #
- # <%=raw @user.name %>
- def raw(stringish)
- stringish.to_s.html_safe
- end
- end
- end
-end \ No newline at end of file
diff --git a/actionpack/lib/action_view/helpers/record_tag_helper.rb b/actionpack/lib/action_view/helpers/record_tag_helper.rb
index e4a9210cde..4d300a1469 100644
--- a/actionpack/lib/action_view/helpers/record_tag_helper.rb
+++ b/actionpack/lib/action_view/helpers/record_tag_helper.rb
@@ -52,9 +52,9 @@ module ActionView
#
# <li id="person_123" class="person bar">...
#
- def content_tag_for(tag_name, record, *args, &block)
- prefix = args.first.is_a?(Hash) ? nil : args.shift
- options = args.extract_options!
+ def content_tag_for(tag_name, record, prefix = nil, options = nil, &block)
+ options, prefix = prefix, nil if prefix.is_a?(Hash)
+ options ||= {}
options.merge!({ :class => "#{dom_class(record, prefix)} #{options[:class]}".strip, :id => dom_id(record, prefix) })
content_tag(tag_name, options, &block)
end
diff --git a/actionpack/lib/action_view/helpers/sanitize_helper.rb b/actionpack/lib/action_view/helpers/sanitize_helper.rb
index d82005fa24..0fee34f8a4 100644
--- a/actionpack/lib/action_view/helpers/sanitize_helper.rb
+++ b/actionpack/lib/action_view/helpers/sanitize_helper.rb
@@ -9,7 +9,7 @@ module ActionView
# These helper methods extend Action View making them callable within your template files.
module SanitizeHelper
extend ActiveSupport::Concern
- # This +sanitize+ helper will html encode all tags and strip all attributes that
+ # This +sanitize+ helper will html encode all tags and strip all attributes that
# aren't specifically allowed.
#
# It also strips href/src tags with invalid protocols, like javascript: especially.
@@ -21,7 +21,7 @@ module ActionView
#
# You can add or remove tags/attributes if you want to customize it a bit.
# See ActionView::Base for full docs on the available options. You can add
- # tags/attributes for single uses of +sanitize+ by passing either the
+ # tags/attributes for single uses of +sanitize+ by passing either the
# <tt>:attributes</tt> or <tt>:tags</tt> options:
#
# Normal Use
diff --git a/actionpack/lib/action_view/helpers/tag_helper.rb b/actionpack/lib/action_view/helpers/tag_helper.rb
index 5d032b32a7..786af5ca58 100644
--- a/actionpack/lib/action_view/helpers/tag_helper.rb
+++ b/actionpack/lib/action_view/helpers/tag_helper.rb
@@ -1,4 +1,5 @@
require 'active_support/core_ext/object/blank'
+require 'active_support/core_ext/string/output_safety'
require 'set'
module ActionView
@@ -7,16 +8,14 @@ module ActionView
# Provides methods to generate HTML tags programmatically when you can't use
# a Builder. By default, they output XHTML compliant tags.
module TagHelper
- include ERB::Util
-
extend ActiveSupport::Concern
include CaptureHelper
BOOLEAN_ATTRIBUTES = %w(disabled readonly multiple checked autobuffer
autoplay controls loop selected hidden scoped async
defer reversed ismap seemless muted required
- autofocus novalidate formnovalidate open).to_set
- BOOLEAN_ATTRIBUTES.merge(BOOLEAN_ATTRIBUTES.map {|attr| attr.to_sym })
+ autofocus novalidate formnovalidate open pubdate).to_set
+ BOOLEAN_ATTRIBUTES.merge(BOOLEAN_ATTRIBUTES.map {|attribute| attribute.to_sym })
# Returns an empty HTML tag of type +name+ which by default is XHTML
# compliant. Set +open+ to true to create an open tag compatible
@@ -25,9 +24,21 @@ module ActionView
# escaping.
#
# ==== Options
- # The +options+ hash is used with attributes with no value like (<tt>disabled</tt> and
- # <tt>readonly</tt>), which you can give a value of true in the +options+ hash. You can use
- # symbols or strings for the attribute names.
+ # You can use symbols or strings for the attribute names.
+ #
+ # Use +true+ with boolean attributes that can render with no value, like
+ # +disabled+ and +readonly+.
+ #
+ # HTML5 <tt>data-*</tt> attributes can be set with a single +data+ key
+ # pointing to a hash of sub-attributes.
+ #
+ # To play nicely with JavaScript conventions sub-attributes are dasherized.
+ # For example, a key +user_id+ would render as <tt>data-user-id</tt> and
+ # thus accessed as <tt>dataset.userId</tt>.
+ #
+ # Values are encoded to JSON, with the exception of strings and symbols.
+ # This may come in handy when using jQuery's HTML5-aware <tt>.data()<tt>
+ # from 1.4.3.
#
# ==== Examples
# tag("br")
@@ -36,14 +47,17 @@ module ActionView
# tag("br", nil, true)
# # => <br>
#
- # tag("input", { :type => 'text', :disabled => true })
+ # tag("input", :type => 'text', :disabled => true)
# # => <input type="text" disabled="disabled" />
#
- # tag("img", { :src => "open & shut.png" })
+ # tag("img", :src => "open & shut.png")
# # => <img src="open &amp; shut.png" />
#
- # tag("img", { :src => "open &amp; shut.png" }, false, false)
+ # tag("img", {:src => "open &amp; shut.png"}, false, false)
# # => <img src="open &amp; shut.png" />
+ #
+ # tag("div", :data => {:name => 'Stephen', :city_state => %w(Chicago IL)})
+ # # => <div data-name="Stephen" data-city-state="[&quot;Chicago&quot;,&quot;IL&quot;]" />
def tag(name, options = nil, open = false, escape = true)
"<#{name}#{tag_options(options, escape) if options}#{open ? ">" : " />"}".html_safe
end
@@ -118,11 +132,19 @@ module ActionView
unless options.blank?
attrs = []
options.each_pair do |key, value|
- if BOOLEAN_ATTRIBUTES.include?(key)
+ if key.to_s == 'data' && value.is_a?(Hash)
+ value.each do |k, v|
+ if !v.is_a?(String) && !v.is_a?(Symbol)
+ v = v.to_json
+ end
+ v = ERB::Util.html_escape(v) if escape
+ attrs << %(data-#{k.to_s.dasherize}="#{v}")
+ end
+ elsif BOOLEAN_ATTRIBUTES.include?(key)
attrs << %(#{key}="#{key}") if value
elsif !value.nil?
final_value = value.is_a?(Array) ? value.join(" ") : value
- final_value = html_escape(final_value) if escape
+ final_value = ERB::Util.html_escape(final_value) if escape
attrs << %(#{key}="#{final_value}")
end
end
diff --git a/actionpack/lib/action_view/helpers/text_helper.rb b/actionpack/lib/action_view/helpers/text_helper.rb
index c1de5c8cb3..2d3c5fe7e7 100644
--- a/actionpack/lib/action_view/helpers/text_helper.rb
+++ b/actionpack/lib/action_view/helpers/text_helper.rb
@@ -9,6 +9,24 @@ module ActionView
# and transforming strings, which can reduce the amount of inline Ruby code in
# your views. These helper methods extend Action View making them callable
# within your template files.
+ #
+ # ==== Sanitization
+ #
+ # Most text helpers by default sanitize the given content, but do not escape it.
+ # This means HTML tags will appear in the page but all malicious code will be removed.
+ # Let's look at some examples using the +simple_format+ method:
+ #
+ # simple_format('<a href="http://example.com/">Example</a>')
+ # # => "<p><a href=\"http://example.com/\">Example</a></p>"
+ #
+ # simple_format('<a href="javascript:alert('no!')">Example</a>')
+ # # => "<p><a>Example</a></p>"
+ #
+ # If you want to escape all content, you should invoke the +h+ method before
+ # calling the text helper.
+ #
+ # simple_format h('<a href="http://example.com/">Example</a>')
+ # # => "<p>&lt;a href=\"http://example.com/\"&gt;Example&lt;/a&gt;</p>"
module TextHelper
extend ActiveSupport::Concern
@@ -134,6 +152,8 @@ module ActionView
# excerpt('This is an example', 'an', 5) # => ...s is an exam...
# excerpt('This is also an example', 'an', 8, '<chop> ') # => <chop> is also an example
def excerpt(text, phrase, *args)
+ return unless text && phrase
+
options = args.extract_options!
unless args.empty?
options[:radius] = args[0] || 100
@@ -141,21 +161,16 @@ module ActionView
end
options.reverse_merge!(:radius => 100, :omission => "...")
- if text && phrase
- phrase = Regexp.escape(phrase)
+ phrase = Regexp.escape(phrase)
+ return unless found_pos = text.mb_chars =~ /(#{phrase})/i
- if found_pos = text.mb_chars =~ /(#{phrase})/i
- start_pos = [ found_pos - options[:radius], 0 ].max
- end_pos = [ [ found_pos + phrase.mb_chars.length + options[:radius] - 1, 0].max, text.mb_chars.length ].min
+ start_pos = [ found_pos - options[:radius], 0 ].max
+ end_pos = [ [ found_pos + phrase.mb_chars.length + options[:radius] - 1, 0].max, text.mb_chars.length ].min
- prefix = start_pos > 0 ? options[:omission] : ""
- postfix = end_pos < text.mb_chars.length - 1 ? options[:omission] : ""
+ prefix = start_pos > 0 ? options[:omission] : ""
+ postfix = end_pos < text.mb_chars.length - 1 ? options[:omission] : ""
- prefix + text.mb_chars[start_pos..end_pos].strip + postfix
- else
- nil
- end
- end
+ prefix + text.mb_chars[start_pos..end_pos].strip + postfix
end
# Attempts to pluralize the +singular+ word unless +count+ is 1. If
@@ -219,6 +234,10 @@ module ActionView
#
# You can pass any HTML attributes into <tt>html_options</tt>. These
# will be added to all created paragraphs.
+ #
+ # ==== Options
+ # * <tt>:sanitize</tt> - If +false+, does not sanitize +text+.
+ #
# ==== Examples
# my_text = "Here is some basic text...\n...with a line break."
#
@@ -232,6 +251,9 @@ module ActionView
#
# simple_format("Look ma! A class!", :class => 'description')
# # => "<p class='description'>Look ma! A class!</p>"
+ #
+ # simple_format("<span>I'm allowed!</span> It's true.", {}, :sanitize => false)
+ # # => "<p><span>I'm allowed!</span> It's true.</p>"
def simple_format(text, html_options={}, options={})
text = ''.html_safe if text.nil?
start_tag = tag('p', html_options, true)
@@ -263,7 +285,7 @@ module ActionView
#
# post_body = "Welcome to my new blog at http://www.myblog.com/. Please e-mail me at me@email.com."
# auto_link(post_body, :html => { :target => '_blank' }) do |text|
- # truncate(text, 15)
+ # truncate(text, :length => 15)
# end
# # => "Welcome to my new blog at <a href=\"http://www.myblog.com/\" target=\"_blank\">http://www.m...</a>.
# Please e-mail me at <a href=\"mailto:me@email.com\">me@email.com</a>."
@@ -345,10 +367,10 @@ module ActionView
values.unshift(first_value)
cycle = get_cycle(name)
- if (cycle.nil? || cycle.values != values)
+ unless cycle && cycle.values == values
cycle = set_cycle(name, Cycle.new(*values))
end
- return cycle.to_s
+ cycle.to_s
end
# Returns the current cycle string after a cycle has been started. Useful
@@ -365,7 +387,7 @@ module ActionView
# <% end %>
def current_cycle(name = "default")
cycle = get_cycle(name)
- cycle.current_value unless cycle.nil?
+ cycle.current_value if cycle
end
# Resets a cycle so that it starts from the first element the next time
@@ -389,7 +411,7 @@ module ActionView
# </table>
def reset_cycle(name = "default")
cycle = get_cycle(name)
- cycle.reset unless cycle.nil?
+ cycle.reset if cycle
end
class Cycle #:nodoc:
@@ -444,7 +466,7 @@ module ActionView
end
AUTO_LINK_RE = %r{
- (?: ([\w+.:-]+:)// | www\. )
+ (?: ([0-9A-Za-z+.:-]+:)// | www\. )
[^\s<]+
}x
@@ -462,7 +484,7 @@ module ActionView
text.gsub(AUTO_LINK_RE) do
scheme, href = $1, $&
punctuation = []
-
+
if auto_linked?($`, $')
# do not change string; URL is already linked
href
@@ -507,7 +529,7 @@ module ActionView
end
end
end
-
+
# Detects already linked context or position in the middle of a tag
def auto_linked?(left, right)
(left =~ AUTO_LINK_CRE[0] and right =~ AUTO_LINK_CRE[1]) or
diff --git a/actionpack/lib/action_view/helpers/translation_helper.rb b/actionpack/lib/action_view/helpers/translation_helper.rb
index dac9c28ab7..59e6ce878f 100644
--- a/actionpack/lib/action_view/helpers/translation_helper.rb
+++ b/actionpack/lib/action_view/helpers/translation_helper.rb
@@ -1,38 +1,56 @@
require 'action_view/helpers/tag_helper'
+require 'i18n/exceptions'
+
+module I18n
+ class ExceptionHandler
+ include Module.new {
+ def call(exception, locale, key, options)
+ exception.is_a?(MissingTranslationData) ? super.html_safe : super
+ end
+ }
+ end
+end
module ActionView
# = Action View Translation Helpers
module Helpers
module TranslationHelper
# Delegates to I18n#translate but also performs three additional functions.
- # First, it'll catch MissingTranslationData exceptions and turn them into
- # inline spans that contains the missing key, such that you can see in a
- # view what is missing where.
#
- # Second, it'll scope the key by the current partial if the key starts
- # with a period. So if you call <tt>translate(".foo")</tt> from the
- # <tt>people/index.html.erb</tt> template, you'll actually be calling
+ # First, it'll pass the :rescue_format => :html option to I18n so that any caught
+ # MissingTranslationData exceptions will be turned into inline spans that
+ #
+ # * have a "translation-missing" class set,
+ # * contain the missing key as a title attribute and
+ # * a titleized version of the last key segment as a text.
+ #
+ # E.g. the value returned for a missing translation key :"blog.post.title" will be
+ # <span class="translation_missing" title="translation missing: blog.post.title">Title</span>.
+ # This way your views will display rather reasonable strings but it will still
+ # be easy to spot missing translations.
+ #
+ # Second, it'll scope the key by the current partial if the key starts
+ # with a period. So if you call <tt>translate(".foo")</tt> from the
+ # <tt>people/index.html.erb</tt> template, you'll actually be calling
# <tt>I18n.translate("people.index.foo")</tt>. This makes it less repetitive
# to translate many keys within the same partials and gives you a simple framework
- # for scoping them consistently. If you don't prepend the key with a period,
+ # for scoping them consistently. If you don't prepend the key with a period,
# nothing is converted.
#
- # Third, it'll mark the translation as safe HTML if the key has the suffix
- # "_html" or the last element of the key is the word "html". For example,
- # calling translate("footer_html") or translate("footer.html") will return
+ # Third, it'll mark the translation as safe HTML if the key has the suffix
+ # "_html" or the last element of the key is the word "html". For example,
+ # calling translate("footer_html") or translate("footer.html") will return
# a safe HTML string that won't be escaped by other HTML helper methods. This
# naming convention helps to identify translations that include HTML tags so that
# you know what kind of output to expect when you call translate in a template.
def translate(key, options = {})
- translation = I18n.translate(scope_key_by_partial(key), options.merge!(:raise => true))
+ options.merge!(:rescue_format => :html) unless options.key?(:rescue_format)
+ translation = I18n.translate(scope_key_by_partial(key), options)
if html_safe_translation_key?(key) && translation.respond_to?(:html_safe)
translation.html_safe
else
translation
end
- rescue I18n::MissingTranslationData => e
- keys = I18n.normalize_keys(e.locale, e.key, e.options[:scope])
- content_tag('span', keys.join(', '), :class => 'translation_missing')
end
alias :t :translate
@@ -45,8 +63,8 @@ module ActionView
private
def scope_key_by_partial(key)
if key.to_s.first == "."
- if @_virtual_path
- @_virtual_path.gsub(%r{/_?}, ".") + key.to_s
+ if (path = @_template && @_template.virtual_path)
+ path.gsub(%r{/_?}, ".") + key.to_s
else
raise "Cannot use t(#{key.inspect}) shortcut because path is not available"
end
diff --git a/actionpack/lib/action_view/helpers/url_helper.rb b/actionpack/lib/action_view/helpers/url_helper.rb
index a5c6718c58..2cd2dca711 100644
--- a/actionpack/lib/action_view/helpers/url_helper.rb
+++ b/actionpack/lib/action_view/helpers/url_helper.rb
@@ -1,6 +1,7 @@
require 'action_view/helpers/javascript_helper'
require 'active_support/core_ext/array/access'
require 'active_support/core_ext/hash/keys'
+require 'active_support/core_ext/string/output_safety'
require 'action_dispatch'
module ActionView
@@ -13,7 +14,7 @@ module ActionView
module UrlHelper
# This helper may be included in any class that includes the
# URL helpers of a routes (routes.url_helpers). Some methods
- # provided here will only work in the4 context of a request
+ # provided here will only work in the context of a request
# (link_to_unless_current, for instance), which must be provided
# as a method called #request on the context.
@@ -22,6 +23,10 @@ module ActionView
include ActionDispatch::Routing::UrlFor
include TagHelper
+ def _routes_context
+ controller
+ end
+
# Need to map default url options to controller one.
# def default_url_options(*args) #:nodoc:
# controller.send(:default_url_options, *args)
@@ -91,7 +96,7 @@ module ActionView
# # => javascript:history.back()
def url_for(options = {})
options ||= {}
- url = case options
+ case options
when String
options
when Hash
@@ -102,8 +107,6 @@ module ActionView
else
polymorphic_path(options)
end
-
- url
end
# Creates a link tag of the given +name+ using a URL created by the set
@@ -235,16 +238,11 @@ module ActionView
html_options = convert_options_to_data_attributes(options, html_options)
url = url_for(options)
- if html_options
- html_options = html_options.stringify_keys
- href = html_options['href']
- tag_options = tag_options(html_options)
- else
- tag_options = nil
- end
+ href = html_options['href']
+ tag_options = tag_options(html_options)
- href_attr = "href=\"#{html_escape(url)}\"" unless href
- "<a #{href_attr}#{tag_options}>#{html_escape(name || url)}</a>".html_safe
+ href_attr = "href=\"#{ERB::Util.html_escape(url)}\"" unless href
+ "<a #{href_attr}#{tag_options}>#{ERB::Util.html_escape(name || url)}</a>".html_safe
end
end
@@ -255,8 +253,9 @@ module ActionView
# using the +link_to+ method with the <tt>:method</tt> modifier as described in
# the +link_to+ documentation.
#
- # The generated form element has a class name of <tt>button_to</tt>
- # to allow styling of the form itself and its children. You can control
+ # By default, the generated form element has a class name of <tt>button_to</tt>
+ # to allow styling of the form itself and its children. This can be changed
+ # using the <tt>:form_class</tt> modifier within +html_options+. You can control
# the form submission and input element behavior using +html_options+.
# This method accepts the <tt>:method</tt> and <tt>:confirm</tt> modifiers
# described in the +link_to+ documentation. If no <tt>:method</tt> modifier
@@ -269,13 +268,16 @@ module ActionView
# The +options+ hash accepts the same options as url_for.
#
# There are a few special +html_options+:
- # * <tt>:method</tt> - Specifies the anchor name to be appended to the path.
- # * <tt>:disabled</tt> - Specifies the anchor name to be appended to the path.
+ # * <tt>:method</tt> - Symbol of HTTP verb. Supported verbs are <tt>:post</tt>, <tt>:get</tt>,
+ # <tt>:delete</tt> and <tt>:put</tt>. By default it will be <tt>:post</tt>.
+ # * <tt>:disabled</tt> - If set to true, it will generate a disabled button.
# * <tt>:confirm</tt> - This will use the unobtrusive JavaScript driver to
# prompt with the question specified. If the user accepts, the link is
# processed normally, otherwise no action is taken.
# * <tt>:remote</tt> - If set to true, will allow the Unobtrusive JavaScript drivers to control the
# submit behaviour. By default this behaviour is an ajax submit.
+ # * <tt>:form_class</tt> - This controls the class of the form within which the submit button will
+ # be placed
#
# ==== Examples
# <%= button_to "New", :action => "new" %>
@@ -284,6 +286,12 @@ module ActionView
# # </form>"
#
#
+ # <%= button_to "New", :action => "new", :form_class => "new-thing" %>
+ # # => "<form method="post" action="/controller/new" class="new-thing">
+ # # <div><input value="New" type="submit" /></div>
+ # # </form>"
+ #
+ #
# <%= button_to "Delete Image", { :action => "delete", :id => @image.id },
# :confirm => "Are you sure?", :method => :delete %>
# # => "<form method="post" action="/images/delete/1" class="button_to">
@@ -313,6 +321,7 @@ module ActionView
end
form_method = method.to_s == 'get' ? 'get' : 'post'
+ form_class = html_options.delete('form_class') || 'button_to'
remote = html_options.delete('remote')
@@ -328,7 +337,7 @@ module ActionView
html_options.merge!("type" => "submit", "value" => name)
- ("<form method=\"#{form_method}\" action=\"#{html_escape(url)}\" #{"data-remote=\"true\"" if remote} class=\"button_to\"><div>" +
+ ("<form method=\"#{form_method}\" action=\"#{ERB::Util.html_escape(url)}\" #{"data-remote=\"true\"" if remote} class=\"#{ERB::Util.html_escape(form_class)}\"><div>" +
method_tag + tag("input", html_options) + request_token_tag + "</div></form>").html_safe
end
@@ -367,8 +376,8 @@ module ActionView
# "Go Back" link instead of a link to the comments page, we could do something like this...
#
# <%=
- # link_to_unless_current("Comment", { :controller => 'comments', :action => 'new}) do
- # link_to("Go back", { :controller => 'posts', :action => 'index' })
+ # link_to_unless_current("Comment", { :controller => "comments", :action => "new" }) do
+ # link_to("Go back", { :controller => "posts", :action => "index" })
# end
# %>
def link_to_unless_current(name, options = {}, html_options = {}, &block)
@@ -474,43 +483,41 @@ module ActionView
# :subject => "This is an example email"
# # => <a href="mailto:me@domain.com?cc=ccaddress@domain.com&subject=This%20is%20an%20example%20email">My email</a>
def mail_to(email_address, name = nil, html_options = {})
- email_address = html_escape(email_address)
+ email_address = ERB::Util.html_escape(email_address)
html_options = html_options.stringify_keys
encode = html_options.delete("encode").to_s
- cc, bcc, subject, body = html_options.delete("cc"), html_options.delete("bcc"), html_options.delete("subject"), html_options.delete("body")
- extras = []
- extras << "cc=#{Rack::Utils.escape(cc).gsub("+", "%20")}" unless cc.nil?
- extras << "bcc=#{Rack::Utils.escape(bcc).gsub("+", "%20")}" unless bcc.nil?
- extras << "body=#{Rack::Utils.escape(body).gsub("+", "%20")}" unless body.nil?
- extras << "subject=#{Rack::Utils.escape(subject).gsub("+", "%20")}" unless subject.nil?
- extras = extras.empty? ? '' : '?' + html_escape(extras.join('&'))
+ extras = %w{ cc bcc body subject }.map { |item|
+ option = html_options.delete(item) || next
+ "#{item}=#{Rack::Utils.escape(option).gsub("+", "%20")}"
+ }.compact
+ extras = extras.empty? ? '' : '?' + ERB::Util.html_escape(extras.join('&'))
email_address_obfuscated = email_address.dup
- email_address_obfuscated.gsub!(/@/, html_options.delete("replace_at")) if html_options.has_key?("replace_at")
- email_address_obfuscated.gsub!(/\./, html_options.delete("replace_dot")) if html_options.has_key?("replace_dot")
-
- string = ''
-
- if encode == "javascript"
- "document.write('#{content_tag("a", name || email_address_obfuscated.html_safe, html_options.merge("href" => "mailto:#{email_address}#{extras}".html_safe))}');".each_byte do |c|
+ email_address_obfuscated.gsub!(/@/, html_options.delete("replace_at")) if html_options.key?("replace_at")
+ email_address_obfuscated.gsub!(/\./, html_options.delete("replace_dot")) if html_options.key?("replace_dot")
+ case encode
+ when "javascript"
+ string = ''
+ html = content_tag("a", name || email_address_obfuscated.html_safe, html_options.merge("href" => "mailto:#{email_address}#{extras}".html_safe))
+ html = escape_javascript(html)
+ "document.write('#{html}');".each_byte do |c|
string << sprintf("%%%x", c)
end
"<script type=\"#{Mime::JS}\">eval(decodeURIComponent('#{string}'))</script>".html_safe
- elsif encode == "hex"
- email_address_encoded = ''
- email_address_obfuscated.each_byte do |c|
- email_address_encoded << sprintf("&#%d;", c)
- end
-
- protocol = 'mailto:'
- protocol.each_byte { |c| string << sprintf("&#%d;", c) }
-
- email_address.each_byte do |c|
+ when "hex"
+ email_address_encoded = email_address_obfuscated.unpack('C*').map {|c|
+ sprintf("&#%d;", c)
+ }.join
+
+ string = 'mailto:'.unpack('C*').map { |c|
+ sprintf("&#%d;", c)
+ }.join + email_address.unpack('C*').map { |c|
char = c.chr
- string << (char =~ /\w/ ? sprintf("%%%x", c) : char)
- end
+ char =~ /\w/ ? sprintf("%%%x", c) : char
+ }.join
+
content_tag "a", name || email_address_encoded.html_safe, html_options.merge("href" => "#{string}#{extras}".html_safe)
else
content_tag "a", name || email_address_obfuscated.html_safe, html_options.merge("href" => "mailto:#{email_address}#{extras}".html_safe)
@@ -586,34 +593,31 @@ module ActionView
private
def convert_options_to_data_attributes(options, html_options)
- html_options = {} if html_options.nil?
- html_options = html_options.stringify_keys
+ if html_options.nil?
+ link_to_remote_options?(options) ? {'data-remote' => 'true'} : {}
+ else
+ html_options = html_options.stringify_keys
+ html_options['data-remote'] = 'true' if link_to_remote_options?(options) || link_to_remote_options?(html_options)
- if (options.is_a?(Hash) && options.key?('remote') && options.delete('remote')) || (html_options.is_a?(Hash) && html_options.key?('remote') && html_options.delete('remote'))
- html_options['data-remote'] = 'true'
- end
+ disable_with = html_options.delete("disable_with")
+ confirm = html_options.delete('confirm')
+ method = html_options.delete('method')
- confirm = html_options.delete("confirm")
+ html_options["data-disable-with"] = disable_with if disable_with
+ html_options["data-confirm"] = confirm if confirm
+ add_method_to_attributes!(html_options, method) if method
- if html_options.key?("popup")
- ActiveSupport::Deprecation.warn(":popup has been deprecated", caller)
+ html_options
end
-
- method, href = html_options.delete("method"), html_options['href']
-
- add_confirm_to_attributes!(html_options, confirm) if confirm
- add_method_to_attributes!(html_options, method) if method
-
- html_options
end
- def add_confirm_to_attributes!(html_options, confirm)
- html_options["data-confirm"] = confirm if confirm
+ def link_to_remote_options?(options)
+ options.is_a?(Hash) && options.key?('remote') && options.delete('remote')
end
def add_method_to_attributes!(html_options, method)
- html_options["rel"] = "nofollow" if method && method.to_s.downcase != "get"
- html_options["data-method"] = method if method
+ html_options["rel"] = "nofollow" if method.to_s.downcase != "get"
+ html_options["data-method"] = method
end
def options_for_javascript(options)
diff --git a/actionpack/lib/action_view/locale/en.yml b/actionpack/lib/action_view/locale/en.yml
index 9004e52c5b..eb816b9446 100644
--- a/actionpack/lib/action_view/locale/en.yml
+++ b/actionpack/lib/action_view/locale/en.yml
@@ -5,7 +5,7 @@
format:
# Sets the separator between the units, for more precision (e.g. 1.0 / 2.0 == 0.5)
separator: "."
- # Delimets thousands (e.g. 1,000,000 is a million) (always in groups of three)
+ # Delimits thousands (e.g. 1,000,000 is a million) (always in groups of three)
delimiter: ","
# Number of decimals, behind the separator (the number 1 with a precision of 2 gives: 1.00)
precision: 3
diff --git a/actionpack/lib/action_view/log_subscriber.rb b/actionpack/lib/action_view/log_subscriber.rb
index 443a0eafd1..29ffbd6fdd 100644
--- a/actionpack/lib/action_view/log_subscriber.rb
+++ b/actionpack/lib/action_view/log_subscriber.rb
@@ -7,7 +7,7 @@ module ActionView
message = "Rendered #{from_rails_root(event.payload[:identifier])}"
message << " within #{from_rails_root(event.payload[:layout])}" if event.payload[:layout]
message << (" (%.1fms)" % event.duration)
- info(message)
+ info(message)
end
alias :render_partial :render_template
alias :render_collection :render_template
diff --git a/actionpack/lib/action_view/lookup_context.rb b/actionpack/lib/action_view/lookup_context.rb
index 3ea8b86af1..06c607931d 100644
--- a/actionpack/lib/action_view/lookup_context.rb
+++ b/actionpack/lib/action_view/lookup_context.rb
@@ -10,7 +10,7 @@ module ActionView
# this key is generated just once during the request, it speeds up all cache accesses.
class LookupContext #:nodoc:
mattr_accessor :fallbacks
- @@fallbacks = [FileSystemResolver.new(""), FileSystemResolver.new("/")]
+ @@fallbacks = FallbackFileSystemResolver.instances
mattr_accessor :registered_details
self.registered_details = []
@@ -61,6 +61,7 @@ module ActionView
def initialize(view_paths, details = {})
@details, @details_key = { :handlers => default_handlers }, nil
@frozen_formats, @skip_default_locale = false, false
+ @cache = true
self.view_paths = view_paths
self.registered_detail_setters.each do |key, setter|
@@ -77,17 +78,17 @@ module ActionView
@view_paths = ActionView::Base.process_view_paths(paths)
end
- def find(name, prefix = nil, partial = false)
- @view_paths.find(*args_for_lookup(name, prefix, partial))
+ def find(name, prefixes = [], partial = false, keys = [])
+ @view_paths.find(*args_for_lookup(name, prefixes, partial, keys))
end
alias :find_template :find
- def find_all(name, prefix = nil, partial = false)
- @view_paths.find_all(*args_for_lookup(name, prefix, partial))
+ def find_all(name, prefixes = [], partial = false, keys = [])
+ @view_paths.find_all(*args_for_lookup(name, prefixes, partial, keys))
end
- def exists?(name, prefix = nil, partial = false)
- @view_paths.exists?(*args_for_lookup(name, prefix, partial))
+ def exists?(name, prefixes = [], partial = false, keys = [])
+ @view_paths.exists?(*args_for_lookup(name, prefixes, partial, keys))
end
alias :template_exists? :exists?
@@ -106,18 +107,26 @@ module ActionView
protected
- def args_for_lookup(name, prefix, partial) #:nodoc:
- name, prefix = normalize_name(name, prefix)
- [name, prefix, partial || false, @details, details_key]
+ def args_for_lookup(name, prefixes, partial, keys) #:nodoc:
+ name, prefixes = normalize_name(name, prefixes)
+ [name, prefixes, partial || false, @details, details_key, keys]
end
# Support legacy foo.erb names even though we now ignore .erb
# as well as incorrectly putting part of the path in the template
# name instead of the prefix.
- def normalize_name(name, prefix) #:nodoc:
+ def normalize_name(name, prefixes) #:nodoc:
name = name.to_s.gsub(handlers_regexp, '')
parts = name.split('/')
- return parts.pop, [prefix, *parts].compact.join("/")
+ name = parts.pop
+
+ prefixes = if prefixes.blank?
+ [parts.join('/')]
+ else
+ prefixes.map { |prefix| [prefix, *parts].compact.join('/') }
+ end
+
+ return name, prefixes
end
def default_handlers #:nodoc:
@@ -130,10 +139,20 @@ module ActionView
end
module Details
+ attr_accessor :cache
+
# Calculate the details key. Remove the handlers from calculation to improve performance
# since the user cannot modify it explicitly.
def details_key #:nodoc:
- @details_key ||= DetailsKey.get(@details)
+ @details_key ||= DetailsKey.get(@details) if @cache
+ end
+
+ # Temporary skip passing the details_key forward.
+ def disable_cache
+ old_value, @cache = @cache, false
+ yield
+ ensure
+ @cache = old_value
end
# Freeze the current formats in the lookup context. By freezing them, you are guaranteeing
@@ -145,11 +164,11 @@ module ActionView
@frozen_formats = true
end
- # Overload formats= to reject [:"*/*"] values.
+ # Overload formats= to reject ["*/*"] values.
def formats=(values)
if values && values.size == 1
value = values.first
- values = nil if value == :"*/*"
+ values = nil if value == "*/*"
values << :html if value == :js
end
super(values)
@@ -167,11 +186,11 @@ module ActionView
end
# Overload locale= to also set the I18n.locale. If the current I18n.config object responds
- # to i18n_config, it means that it's has a copy of the original I18n configuration and it's
+ # to original_config, it means that it's has a copy of the original I18n configuration and it's
# acting as proxy, which we need to skip.
def locale=(value)
if value
- config = I18n.config.respond_to?(:i18n_config) ? I18n.config.i18n_config : I18n.config
+ config = I18n.config.respond_to?(:original_config) ? I18n.config.original_config : I18n.config
config.locale = value
end
@@ -226,4 +245,4 @@ module ActionView
include Details
include ViewPaths
end
-end \ No newline at end of file
+end
diff --git a/actionpack/lib/action_view/render/partials.rb b/actionpack/lib/action_view/partials.rb
index 459aae94a2..c181689e62 100644
--- a/actionpack/lib/action_view/render/partials.rb
+++ b/actionpack/lib/action_view/partials.rb
@@ -12,19 +12,48 @@ module ActionView
#
# <%= render :partial => "account" %>
#
- # This would render "advertiser/_account.erb" and pass the instance variable @account in as a local variable
+ # This would render "advertiser/_account.html.erb" and pass the instance variable @account in as a local variable
# +account+ to the template for display.
#
# In another template for Advertiser#buy, we could have:
#
# <%= render :partial => "account", :locals => { :account => @buyer } %>
#
- # <% for ad in @advertisements %>
+ # <% @advertisements.each do |ad| %>
# <%= render :partial => "ad", :locals => { :ad => ad } %>
# <% end %>
#
- # This would first render "advertiser/_account.erb" with @buyer passed in as the local variable +account+, then
- # render "advertiser/_ad.erb" and pass the local variable +ad+ to the template for display.
+ # This would first render "advertiser/_account.html.erb" with @buyer passed in as the local variable +account+, then
+ # render "advertiser/_ad.html.erb" and pass the local variable +ad+ to the template for display.
+ #
+ # == The :as and :object options
+ #
+ # By default <tt>ActionView::Partials::PartialRenderer</tt> has its object in a local variable with the same
+ # name as the template. So, given
+ #
+ # <%= render :partial => "contract" %>
+ #
+ # within contract we'll get <tt>@contract</tt> in the local variable +contract+, as if we had written
+ #
+ # <%= render :partial => "contract", :locals => { :contract => @contract } %>
+ #
+ # With the <tt>:as</tt> option we can specify a different name for said local variable. For example, if we
+ # wanted it to be +agreement+ instead of +contract+ we'd do:
+ #
+ # <%= render :partial => "contract", :as => 'agreement' %>
+ #
+ # The <tt>:object</tt> option can be used to directly specify which object is rendered into the partial;
+ # useful when the template's object is elsewhere, in a different ivar or in a local variable for instance.
+ #
+ # Revisiting a previous example we could have written this code:
+ #
+ # <%= render :partial => "account", :object => @buyer %>
+ #
+ # <% @advertisements.each do |ad| %>
+ # <%= render :partial => "ad", :object => ad %>
+ # <% end %>
+ #
+ # The <tt>:object</tt> and <tt>:as</tt> options can be used together.
#
# == Rendering a collection of partials
#
@@ -35,10 +64,22 @@ module ActionView
#
# <%= render :partial => "ad", :collection => @advertisements %>
#
- # This will render "advertiser/_ad.erb" and pass the local variable +ad+ to the template for display. An
+ # This will render "advertiser/_ad.html.erb" and pass the local variable +ad+ to the template for display. An
# iteration counter will automatically be made available to the template with a name of the form
# +partial_name_counter+. In the case of the example above, the template would be fed +ad_counter+.
#
+ # The <tt>:as</tt> option may be used when rendering partials.
+ #
+ # You can specify a partial to be rendered between elements via the <tt>:spacer_template</tt> option.
+ # The following example will render <tt>advertiser/_ad_divider.html.erb</tt> between each ad partial:
+ #
+ # <%= render :partial => "ad", :collection => @advertisements, :spacer_template => "ad_divider" %>
+ #
+ # If the given <tt>:collection</tt> is nil or empty, <tt>render</tt> will return nil. This will allow you
+ # to specify a text which will displayed instead by using this form:
+ #
+ # <%= render(:partial => "ad", :collection => @advertisements) || "There's no ad to be displayed" %>
+ #
# NOTE: Due to backwards compatibility concerns, the collection can't be one of hashes. Normally you'd also
# just keep domain objects, like Active Records, in there.
#
@@ -48,7 +89,7 @@ module ActionView
#
# <%= render :partial => "advertisement/ad", :locals => { :ad => @advertisement } %>
#
- # This will render the partial "advertisement/_ad.erb" regardless of which controller this is being called from.
+ # This will render the partial "advertisement/_ad.html.erb" regardless of which controller this is being called from.
#
# == Rendering objects with the RecordIdentifier
#
@@ -56,7 +97,7 @@ module ActionView
# you're following its conventions for RecordIdentifier#partial_path. Examples:
#
# # @account is an Account instance, so it uses the RecordIdentifier to replace
- # # <%= render :partial => "accounts/account", :locals => { :account => @buyer } %>
+ # # <%= render :partial => "accounts/account", :locals => { :account => @account} %>
# <%= render :partial => @account %>
#
# # @posts is an array of Post instances, so it uses the RecordIdentifier to replace
@@ -174,165 +215,12 @@ module ActionView
# <%- end -%>
# <% end %>
module Partials
- extend ActiveSupport::Concern
-
- class PartialRenderer
- PARTIAL_NAMES = Hash.new {|h,k| h[k] = {} }
-
- def initialize(view_context, options, block)
- @view = view_context
- @partial_names = PARTIAL_NAMES[@view.controller.class.name]
-
- setup(options, block)
- end
-
- def setup(options, block)
- partial = options[:partial]
-
- @options = options
- @locals = options[:locals] || {}
- @block = block
-
- if String === partial
- @object = options[:object]
- @path = partial
- @collection = collection
- else
- @object = partial
-
- if @collection = collection
- paths = @collection_paths = @collection.map { |o| partial_path(o) }
- @path = paths.uniq.size == 1 ? paths.first : nil
- else
- @path = partial_path
- end
- end
- end
-
- def render
- identifier = ((@template = find_template) ? @template.identifier : @path)
-
- if @collection
- ActiveSupport::Notifications.instrument("render_collection.action_view",
- :identifier => identifier || "collection", :count => @collection.size) do
- render_collection
- end
- else
- content = ActiveSupport::Notifications.instrument("render_partial.action_view",
- :identifier => identifier) do
- render_partial
- end
-
- if !@block && (layout = @options[:layout])
- content = @view._render_layout(find_template(layout), @locals){ content }
- end
-
- content
- end
- end
-
- def render_collection
- return nil if @collection.blank?
-
- if @options.key?(:spacer_template)
- spacer = find_template(@options[:spacer_template]).render(@view, @locals)
- end
-
- result = @template ? collection_with_template : collection_without_template
- result.join(spacer).html_safe
- end
-
- def collection_with_template(template = @template)
- segments, locals, template = [], @locals, @template
-
- if @options[:as]
- as = @options[:as]
- counter = "#{as}_counter".to_sym
- else
- as = template.variable_name
- counter = template.counter_name
- end
-
- locals[counter] = -1
-
- @collection.each do |object|
- locals[counter] += 1
- locals[as] = object
- segments << template.render(@view, locals)
- end
-
- segments
- end
-
- def collection_without_template(collection_paths = @collection_paths)
- segments, locals = [], @locals
- index, template = -1, nil
-
- if @options[:as]
- as = @options[:as]
- counter = "#{as}_counter"
- end
-
- @collection.each_with_index do |object, i|
- template = find_template(collection_paths[i])
- locals[as || template.variable_name] = object
- locals[counter || template.counter_name] = (index += 1)
-
- segments << template.render(@view, locals)
- end
-
- @template = template
- segments
- end
-
- def render_partial(object = @object)
- locals, view, template = @locals, @view, @template
-
- object ||= locals[template.variable_name]
- locals[@options[:as] || template.variable_name] = object
-
- template.render(view, locals) do |*name|
- view._layout_for(*name, &@block)
- end
- end
-
- private
-
- def collection
- if @object.respond_to?(:to_ary)
- @object
- elsif @options.key?(:collection)
- @options[:collection] || []
- end
- end
-
- def find_template(path=@path)
- return path unless path.is_a?(String)
- prefix = @view.controller_path unless path.include?(?/)
- @view.find_template(path, prefix, true)
- end
-
- def partial_path(object = @object)
- @partial_names[object.class.name] ||= begin
- object = object.to_model if object.respond_to?(:to_model)
-
- object.class.model_name.partial_path.dup.tap do |partial|
- path = @view.controller_path
- partial.insert(0, "#{File.dirname(path)}/") if partial.include?(?/) && path.include?(?/)
- end
- end
- end
- end
-
def _render_partial(options, &block) #:nodoc:
- if defined?(@renderer)
- @renderer.setup(options, block)
- else
- @renderer = PartialRenderer.new(self, options, block)
- end
-
- @renderer.render
+ _partial_renderer.setup(options, block).render
end
+ def _partial_renderer #:nodoc:
+ @_partial_renderer ||= PartialRenderer.new(self)
+ end
end
end
diff --git a/actionpack/lib/action_view/paths.rb b/actionpack/lib/action_view/path_set.rb
index 9857d688d2..e3de3e1eac 100644
--- a/actionpack/lib/action_view/paths.rb
+++ b/actionpack/lib/action_view/path_set.rb
@@ -10,16 +10,16 @@ module ActionView #:nodoc:
METHOD
end
- def find(path, prefix = nil, partial = false, details = {}, key = nil)
- template = find_all(path, prefix, partial, details, key).first
- raise MissingTemplate.new(self, "#{prefix}/#{path}", details, partial) unless template
- template
+ def find(*args)
+ find_all(*args).first || raise(MissingTemplate.new(self, *args))
end
- def find_all(*args)
- each do |resolver|
- templates = resolver.find_all(*args)
- return templates unless templates.empty?
+ def find_all(path, prefixes = [], *args)
+ prefixes.each do |prefix|
+ each do |resolver|
+ templates = resolver.find_all(path, prefix, *args)
+ return templates unless templates.empty?
+ end
end
[]
end
diff --git a/actionpack/lib/action_view/railtie.rb b/actionpack/lib/action_view/railtie.rb
index 33dfcbb803..501ec07b09 100644
--- a/actionpack/lib/action_view/railtie.rb
+++ b/actionpack/lib/action_view/railtie.rb
@@ -8,10 +8,10 @@ module ActionView
config.action_view.stylesheet_expansions = {}
config.action_view.javascript_expansions = { :defaults => ['prototype', 'effects', 'dragdrop', 'controls', 'rails'] }
- initializer "action_view.cache_asset_timestamps" do |app|
+ initializer "action_view.cache_asset_ids" do |app|
unless app.config.cache_classes
ActiveSupport.on_load(:action_view) do
- ActionView::Helpers::AssetTagHelper.cache_asset_timestamps = false
+ ActionView::Helpers::AssetTagHelper::AssetPaths.cache_asset_ids = false
end
end
end
@@ -35,5 +35,13 @@ module ActionView
end
end
end
+
+ initializer "action_view.caching" do |app|
+ ActiveSupport.on_load(:action_view) do
+ if app.config.action_view.cache_template_loading.nil?
+ ActionView::Resolver.caching = app.config.cache_classes
+ end
+ end
+ end
end
-end \ No newline at end of file
+end
diff --git a/actionpack/lib/action_view/render/layouts.rb b/actionpack/lib/action_view/render/layouts.rb
deleted file mode 100644
index a474783a20..0000000000
--- a/actionpack/lib/action_view/render/layouts.rb
+++ /dev/null
@@ -1,79 +0,0 @@
-module ActionView
- # = Action View Layouts
- module Layouts
- # Returns the contents that are yielded to a layout, given a name or a block.
- #
- # You can think of a layout as a method that is called with a block. If the user calls
- # <tt>yield :some_name</tt>, the block, by default, returns <tt>content_for(:some_name)</tt>.
- # If the user calls simply +yield+, the default block returns <tt>content_for(:layout)</tt>.
- #
- # The user can override this default by passing a block to the layout:
- #
- # # The template
- # <%= render :layout => "my_layout" do %>
- # Content
- # <% end %>
- #
- # # The layout
- # <html>
- # <%= yield %>
- # </html>
- #
- # In this case, instead of the default block, which would return <tt>content_for(:layout)</tt>,
- # this method returns the block that was passed in to <tt>render :layout</tt>, and the response
- # would be
- #
- # <html>
- # Content
- # </html>
- #
- # Finally, the block can take block arguments, which can be passed in by +yield+:
- #
- # # The template
- # <%= render :layout => "my_layout" do |customer| %>
- # Hello <%= customer.name %>
- # <% end %>
- #
- # # The layout
- # <html>
- # <%= yield Struct.new(:name).new("David") %>
- # </html>
- #
- # In this case, the layout would receive the block passed into <tt>render :layout</tt>,
- # and the struct specified would be passed into the block as an argument. The result
- # would be
- #
- # <html>
- # Hello David
- # </html>
- #
- def _layout_for(name = nil, &block) #:nodoc:
- if !block || name
- @_content_for[name || :layout].html_safe
- else
- capture(&block)
- end
- end
-
- # This is the method which actually finds the layout using details in the lookup
- # context object. If no layout is found, it checks if at least a layout with
- # the given name exists across all details before raising the error.
- def find_layout(layout)
- begin
- with_layout_format do
- layout =~ /^\// ?
- with_fallbacks { find_template(layout) } : find_template(layout)
- end
- rescue ActionView::MissingTemplate => e
- update_details(:formats => nil) do
- raise unless template_exists?(layout)
- end
- end
- end
-
- # Contains the logic that actually renders the layout.
- def _render_layout(layout, locals, &block) #:nodoc:
- layout.render(self, locals){ |*name| _layout_for(*name, &block) }
- end
- end
-end
diff --git a/actionpack/lib/action_view/render/rendering.rb b/actionpack/lib/action_view/render/rendering.rb
deleted file mode 100644
index 5320245173..0000000000
--- a/actionpack/lib/action_view/render/rendering.rb
+++ /dev/null
@@ -1,67 +0,0 @@
-require 'active_support/core_ext/object/try'
-
-module ActionView
- # = Action View Rendering
- module Rendering
- # Returns the result of a render that's dictated by the options hash. The primary options are:
- #
- # * <tt>:partial</tt> - See ActionView::Partials.
- # * <tt>:update</tt> - Calls update_page with the block given.
- # * <tt>:file</tt> - Renders an explicit template file (this used to be the old default), add :locals to pass in those.
- # * <tt>:inline</tt> - Renders an inline template similar to how it's done in the controller.
- # * <tt>:text</tt> - Renders the text passed in out.
- #
- # If no options hash is passed or :update specified, the default is to render a partial and use the second parameter
- # as the locals hash.
- def render(options = {}, locals = {}, &block)
- case options
- when Hash
- if block_given?
- _render_partial(options.merge(:partial => options[:layout]), &block)
- elsif options.key?(:partial)
- _render_partial(options)
- else
- template = _determine_template(options)
- lookup_context.freeze_formats(template.formats, true)
- _render_template(template, options[:layout], options)
- end
- when :update
- update_page(&block)
- else
- _render_partial(:partial => options, :locals => locals)
- end
- end
-
- # Determine the template to be rendered using the given options.
- def _determine_template(options) #:nodoc:
- if options.key?(:inline)
- handler = Template.handler_class_for_extension(options[:type] || "erb")
- Template.new(options[:inline], "inline template", handler, {})
- elsif options.key?(:text)
- Template::Text.new(options[:text], formats.try(:first))
- elsif options.key?(:file)
- with_fallbacks { find_template(options[:file], options[:prefix]) }
- elsif options.key?(:template)
- options[:template].respond_to?(:render) ?
- options[:template] : find_template(options[:template], options[:prefix])
- end
- end
-
- # Renders the given template. An string representing the layout can be
- # supplied as well.
- def _render_template(template, layout = nil, options = {}) #:nodoc:
- locals = options[:locals] || {}
- layout = find_layout(layout) if layout
-
- ActiveSupport::Notifications.instrument("render_template.action_view",
- :identifier => template.identifier, :layout => layout.try(:virtual_path)) do
-
- content = template.render(self, locals) { |*name| _layout_for(*name) }
- @_content_for[:layout] = content if layout
-
- content = _render_layout(layout, locals) if layout
- content
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/renderer/abstract_renderer.rb b/actionpack/lib/action_view/renderer/abstract_renderer.rb
new file mode 100644
index 0000000000..4a52b3172e
--- /dev/null
+++ b/actionpack/lib/action_view/renderer/abstract_renderer.rb
@@ -0,0 +1,37 @@
+module ActionView
+ class AbstractRenderer #:nodoc:
+ delegate :find_template, :template_exists?, :with_fallbacks, :update_details,
+ :with_layout_format, :formats, :freeze_formats, :to => :@lookup_context
+
+ def initialize(view)
+ @view = view
+ @lookup_context = view.lookup_context
+ end
+
+ def render
+ raise NotImplementedError
+ end
+
+ # Checks if the given path contains a format and if so, change
+ # the lookup context to take this new format into account.
+ def wrap_formats(value)
+ return yield unless value.is_a?(String)
+
+ if value.sub!(formats_regexp, "")
+ update_details(:formats => [$1.to_sym]){ yield }
+ else
+ yield
+ end
+ end
+
+ def formats_regexp
+ @@formats_regexp ||= /\.(#{Mime::SET.symbols.join('|')})$/
+ end
+
+ protected
+
+ def instrument(name, options={})
+ ActiveSupport::Notifications.instrument("render_#{name}.action_view", options){ yield }
+ end
+ end
+end
diff --git a/actionpack/lib/action_view/renderer/partial_renderer.rb b/actionpack/lib/action_view/renderer/partial_renderer.rb
new file mode 100644
index 0000000000..94c0a8a8fb
--- /dev/null
+++ b/actionpack/lib/action_view/renderer/partial_renderer.rb
@@ -0,0 +1,167 @@
+require 'action_view/renderer/abstract_renderer'
+
+module ActionView
+ class PartialRenderer < AbstractRenderer #:nodoc:
+ PARTIAL_NAMES = Hash.new {|h,k| h[k] = {} }
+
+ def initialize(view)
+ super
+ @partial_names = PARTIAL_NAMES[@view.controller.class.name]
+ end
+
+ def setup(options, block)
+ partial = options[:partial]
+
+ @options = options
+ @locals = options[:locals] || {}
+ @block = block
+
+ if String === partial
+ @object = options[:object]
+ @path = partial
+ @collection = collection
+ else
+ @object = partial
+
+ if @collection = collection_from_object || collection
+ paths = @collection_data = @collection.map { |o| partial_path(o) }
+ @path = paths.uniq.size == 1 ? paths.first : nil
+ else
+ @path = partial_path
+ end
+ end
+
+ if @path
+ @variable, @variable_counter = retrieve_variable(@path)
+ else
+ paths.map! { |path| retrieve_variable(path).unshift(path) }
+ end
+
+ self
+ end
+
+ def render
+ wrap_formats(@path) do
+ identifier = ((@template = find_partial) ? @template.identifier : @path)
+
+ if @collection
+ instrument(:collection, :identifier => identifier || "collection", :count => @collection.size) do
+ render_collection
+ end
+ else
+ instrument(:partial, :identifier => identifier) do
+ render_partial
+ end
+ end
+ end
+ end
+
+ def render_collection
+ return nil if @collection.blank?
+
+ if @options.key?(:spacer_template)
+ spacer = find_template(@options[:spacer_template]).render(@view, @locals)
+ end
+
+ result = @template ? collection_with_template : collection_without_template
+ result.join(spacer).html_safe
+ end
+
+ def render_partial
+ locals, view, block = @locals, @view, @block
+ object, as = @object, @variable
+
+ if !block && (layout = @options[:layout])
+ layout = find_template(layout)
+ end
+
+ object ||= locals[as]
+ locals[as] = object
+
+ content = @template.render(view, locals) do |*name|
+ view._layout_for(*name, &block)
+ end
+
+ content = layout.render(view, locals){ content } if layout
+ content
+ end
+
+ private
+
+ def collection
+ if @options.key?(:collection)
+ collection = @options[:collection]
+ collection.respond_to?(:to_ary) ? collection.to_ary : []
+ end
+ end
+
+ def collection_from_object
+ if @object.respond_to?(:to_ary)
+ @object.to_ary
+ end
+ end
+
+ def find_partial
+ if path = @path
+ locals = @locals.keys
+ locals << @variable
+ locals << @variable_counter if @collection
+ find_template(path, locals)
+ end
+ end
+
+ def find_template(path=@path, locals=@locals.keys)
+ prefixes = path.include?(?/) ? [] : @view.controller_prefixes
+ @lookup_context.find_template(path, prefixes, true, locals)
+ end
+
+ def collection_with_template
+ segments, locals, template = [], @locals, @template
+ as, counter = @variable, @variable_counter
+
+ locals[counter] = -1
+
+ @collection.each do |object|
+ locals[counter] += 1
+ locals[as] = object
+ segments << template.render(@view, locals)
+ end
+
+ segments
+ end
+
+ def collection_without_template
+ segments, locals, collection_data = [], @locals, @collection_data
+ index, template, cache = -1, nil, {}
+ keys = @locals.keys
+
+ @collection.each_with_index do |object, i|
+ path, *data = collection_data[i]
+ template = (cache[path] ||= find_template(path, keys + data))
+ locals[data[0]] = object
+ locals[data[1]] = (index += 1)
+ segments << template.render(@view, locals)
+ end
+
+ @template = template
+ segments
+ end
+
+ def partial_path(object = @object)
+ @partial_names[object.class.name] ||= begin
+ object = object.to_model if object.respond_to?(:to_model)
+
+ object.class.model_name.partial_path.dup.tap do |partial|
+ path = @view.controller_prefixes.first
+ partial.insert(0, "#{File.dirname(path)}/") if partial.include?(?/) && path.include?(?/)
+ end
+ end
+ end
+
+ def retrieve_variable(path)
+ variable = @options[:as].try(:to_sym) || path[%r'_?(\w+)(\.\w+)*$', 1].to_sym
+ variable_counter = :"#{variable}_counter" if @collection
+ [variable, variable_counter]
+ end
+ end
+end
diff --git a/actionpack/lib/action_view/renderer/template_renderer.rb b/actionpack/lib/action_view/renderer/template_renderer.rb
new file mode 100644
index 0000000000..9ae1636131
--- /dev/null
+++ b/actionpack/lib/action_view/renderer/template_renderer.rb
@@ -0,0 +1,98 @@
+require 'set'
+require 'active_support/core_ext/object/try'
+require 'active_support/core_ext/array/wrap'
+require 'action_view/renderer/abstract_renderer'
+
+module ActionView
+ class TemplateRenderer < AbstractRenderer #:nodoc:
+ attr_reader :rendered
+
+ def initialize(view)
+ super
+ @rendered = Set.new
+ end
+
+ def render(options)
+ wrap_formats(options[:template] || options[:file]) do
+ template = determine_template(options)
+ render_template(template, options[:layout], options[:locals])
+ end
+ end
+
+ def render_once(options)
+ paths, locals = options[:once], options[:locals] || {}
+ layout, keys = options[:layout], locals.keys
+ prefixes = options.fetch(:prefixes, @view.controller_prefixes)
+
+ raise "render :once expects a String or an Array to be given" unless paths
+
+ render_with_layout(layout, locals) do
+ contents = []
+ Array.wrap(paths).each do |path|
+ template = find_template(path, prefixes, false, keys)
+ contents << render_template(template, nil, locals) if @rendered.add?(template)
+ end
+ contents.join("\n")
+ end
+ end
+
+ # Determine the template to be rendered using the given options.
+ def determine_template(options) #:nodoc:
+ keys = options[:locals].try(:keys) || []
+
+ if options.key?(:text)
+ Template::Text.new(options[:text], formats.try(:first))
+ elsif options.key?(:file)
+ with_fallbacks { find_template(options[:file], options[:prefixes], false, keys) }
+ elsif options.key?(:inline)
+ handler = Template.handler_for_extension(options[:type] || "erb")
+ Template.new(options[:inline], "inline template", handler, :locals => keys)
+ elsif options.key?(:template)
+ options[:template].respond_to?(:render) ?
+ options[:template] : find_template(options[:template], options[:prefixes], false, keys)
+ end
+ end
+
+ # Renders the given template. An string representing the layout can be
+ # supplied as well.
+ def render_template(template, layout_name = nil, locals = {}) #:nodoc:
+ freeze_formats(template.formats, true)
+ view, locals = @view, locals || {}
+
+ render_with_layout(layout_name, locals) do |layout|
+ instrument(:template, :identifier => template.identifier, :layout => layout.try(:virtual_path)) do
+ template.render(view, locals) { |*name| view._layout_for(*name) }
+ end
+ end
+ end
+
+ def render_with_layout(path, locals) #:nodoc:
+ layout = path && find_layout(path, locals.keys)
+ content = yield(layout)
+
+ if layout
+ view = @view
+ view.store_content_for(:layout, content)
+ layout.render(view, locals){ |*name| view._layout_for(*name) }
+ else
+ content
+ end
+ end
+
+ # This is the method which actually finds the layout using details in the lookup
+ # context object. If no layout is found, it checks if at least a layout with
+ # the given name exists across all details before raising the error.
+ def find_layout(layout, keys)
+ begin
+ with_layout_format do
+ layout =~ /^\// ?
+ with_fallbacks { find_template(layout, nil, false, keys) } : find_template(layout, nil, false, keys)
+ end
+ rescue ActionView::MissingTemplate => e
+ update_details(:formats => nil) do
+ raise unless template_exists?(layout)
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_view/rendering.rb b/actionpack/lib/action_view/rendering.rb
new file mode 100644
index 0000000000..baa5d2c3fd
--- /dev/null
+++ b/actionpack/lib/action_view/rendering.rb
@@ -0,0 +1,106 @@
+require 'active_support/core_ext/object/try'
+
+module ActionView
+ # = Action View Rendering
+ module Rendering
+ # Returns the result of a render that's dictated by the options hash. The primary options are:
+ #
+ # * <tt>:partial</tt> - See ActionView::Partials.
+ # * <tt>:update</tt> - Calls update_page with the block given.
+ # * <tt>:file</tt> - Renders an explicit template file (this used to be the old default), add :locals to pass in those.
+ # * <tt>:inline</tt> - Renders an inline template similar to how it's done in the controller.
+ # * <tt>:text</tt> - Renders the text passed in out.
+ # * <tt>:once</tt> - Accepts a string or an array of strings and Rails will ensure they each of them are rendered just once.
+ #
+ # If no options hash is passed or :update specified, the default is to render a partial and use the second parameter
+ # as the locals hash.
+ def render(options = {}, locals = {}, &block)
+ case options
+ when Hash
+ if block_given?
+ _render_partial(options.merge(:partial => options[:layout]), &block)
+ elsif options.key?(:partial)
+ _render_partial(options)
+ elsif options.key?(:once)
+ _render_once(options)
+ else
+ _render_template(options)
+ end
+ when :update
+ update_page(&block)
+ else
+ _render_partial(:partial => options, :locals => locals)
+ end
+ end
+
+ # Returns the contents that are yielded to a layout, given a name or a block.
+ #
+ # You can think of a layout as a method that is called with a block. If the user calls
+ # <tt>yield :some_name</tt>, the block, by default, returns <tt>content_for(:some_name)</tt>.
+ # If the user calls simply +yield+, the default block returns <tt>content_for(:layout)</tt>.
+ #
+ # The user can override this default by passing a block to the layout:
+ #
+ # # The template
+ # <%= render :layout => "my_layout" do %>
+ # Content
+ # <% end %>
+ #
+ # # The layout
+ # <html>
+ # <%= yield %>
+ # </html>
+ #
+ # In this case, instead of the default block, which would return <tt>content_for(:layout)</tt>,
+ # this method returns the block that was passed in to <tt>render :layout</tt>, and the response
+ # would be
+ #
+ # <html>
+ # Content
+ # </html>
+ #
+ # Finally, the block can take block arguments, which can be passed in by +yield+:
+ #
+ # # The template
+ # <%= render :layout => "my_layout" do |customer| %>
+ # Hello <%= customer.name %>
+ # <% end %>
+ #
+ # # The layout
+ # <html>
+ # <%= yield Struct.new(:name).new("David") %>
+ # </html>
+ #
+ # In this case, the layout would receive the block passed into <tt>render :layout</tt>,
+ # and the struct specified would be passed into the block as an argument. The result
+ # would be
+ #
+ # <html>
+ # Hello David
+ # </html>
+ #
+ def _layout_for(*args, &block)
+ name = args.first
+
+ if name.is_a?(Symbol)
+ @_content_for[name].html_safe
+ elsif block
+ capture(*args, &block)
+ else
+ @_content_for[:layout].html_safe
+ end
+ end
+
+ def _render_once(options) #:nodoc:
+ _template_renderer.render_once(options)
+ end
+
+ def _render_template(options) #:nodoc:
+ _template_renderer.render(options)
+ end
+
+ def _template_renderer #:nodoc:
+ @_template_renderer ||= TemplateRenderer.new(self)
+ end
+ end
+end
diff --git a/actionpack/lib/action_view/template.rb b/actionpack/lib/action_view/template.rb
index 40ff1f2182..96d506fac5 100644
--- a/actionpack/lib/action_view/template.rb
+++ b/actionpack/lib/action_view/template.rb
@@ -1,5 +1,6 @@
require 'active_support/core_ext/array/wrap'
require 'active_support/core_ext/object/blank'
+require 'active_support/core_ext/object/try'
require 'active_support/core_ext/kernel/singleton_class'
module ActionView
@@ -97,9 +98,12 @@ module ActionView
extend Template::Handlers
- attr_reader :source, :identifier, :handler, :virtual_path, :formats,
- :original_encoding
+ attr_accessor :locals, :formats, :virtual_path
+ attr_reader :source, :identifier, :handler, :original_encoding, :updated_at
+
+ # This finalizer is needed (and exactly with a proc inside another proc)
+ # otherwise templates leak in development.
Finalizer = proc do |method_name, mod|
proc do
mod.module_eval do
@@ -109,50 +113,80 @@ module ActionView
end
def initialize(source, identifier, handler, details)
- @source = source
- @identifier = identifier
- @handler = handler
- @original_encoding = nil
-
- @virtual_path = details[:virtual_path]
- @method_names = {}
+ format = details[:format] || (handler.default_format if handler.respond_to?(:default_format))
- format = details[:format] || :html
- @formats = Array.wrap(format).map(&:to_sym)
+ @source = source
+ @identifier = identifier
+ @handler = handler
+ @compiled = false
+ @original_encoding = nil
+ @locals = details[:locals] || []
+ @virtual_path = details[:virtual_path]
+ @updated_at = details[:updated_at] || Time.now
+ @formats = Array.wrap(format).map { |f| f.is_a?(Mime::Type) ? f.ref : f }
end
+ # Render a template. If the template was not compiled yet, it is done
+ # exactly before rendering.
+ #
+ # This method is instrumented as "!render_template.action_view". Notice that
+ # we use a bang in this instrumentation because you don't want to
+ # consume this in production. This is only slow if it's being listened to.
def render(view, locals, &block)
- # Notice that we use a bang in this instrumentation because you don't want to
- # consume this in production. This is only slow if it's being listened to.
+ old_template, view._template = view._template, self
ActiveSupport::Notifications.instrument("!render_template.action_view", :virtual_path => @virtual_path) do
- if view.is_a?(ActionView::CompiledTemplates)
- mod = ActionView::CompiledTemplates
- else
- mod = view.singleton_class
- end
-
- method_name = compile(locals, view, mod)
+ compile!(view)
view.send(method_name, locals, &block)
end
rescue Exception => e
- if e.is_a?(Template::Error)
- e.sub_template_of(self)
- raise e
- else
- raise Template::Error.new(self, view.respond_to?(:assigns) ? view.assigns : {}, e)
- end
+ handle_render_error(view, e)
+ ensure
+ view._template = old_template
end
def mime_type
@mime_type ||= Mime::Type.lookup_by_extension(@formats.first.to_s) if @formats.first
end
- def variable_name
- @variable_name ||= @virtual_path[%r'_?(\w+)(\.\w+)*$', 1].to_sym
+ # Receives a view object and return a template similar to self by using @virtual_path.
+ #
+ # This method is useful if you have a template object but it does not contain its source
+ # anymore since it was already compiled. In such cases, all you need to do is to call
+ # refresh passing in the view object.
+ #
+ # Notice this method raises an error if the template to be refreshed does not have a
+ # virtual path set (true just for inline templates).
+ def refresh(view)
+ raise "A template needs to have a virtual path in order to be refreshed" unless @virtual_path
+ lookup = view.lookup_context
+ pieces = @virtual_path.split("/")
+ name = pieces.pop
+ partial = !!name.sub!(/^_/, "")
+ lookup.disable_cache do
+ lookup.find_template(name, [ pieces.join('/') ], partial, @locals)
+ end
+ end
+
+ # Expires this template by setting his updated_at date to Jan 1st, 1970.
+ def expire!
+ @updated_at = Time.utc(1970)
+ end
+
+ # Receives a view context and renders a template exactly like self by using
+ # the @virtual_path. It raises an error if no @virtual_path was given.
+ def rerender(view)
+ raise "A template needs to have a virtual path in order to be rerendered" unless @virtual_path
+ name = @virtual_path.dup
+ if name.sub!(/(^|\/)_([^\/]*)$/, '\1\2')
+ view.render :partial => name
+ else
+ view.render :template => @virtual_path
+ end
end
- def counter_name
- @counter_name ||= "#{variable_name}_counter".to_sym
+ # Used to store template data by template handlers.
+ def data
+ @data ||= {}
end
def inspect
@@ -164,7 +198,27 @@ module ActionView
end
end
- private
+ protected
+
+ # Compile a template. This method ensures a template is compiled
+ # just once and removes the source after it is compiled.
+ def compile!(view) #:nodoc:
+ return if @compiled
+
+ if view.is_a?(ActionView::CompiledTemplates)
+ mod = ActionView::CompiledTemplates
+ else
+ mod = view.singleton_class
+ end
+
+ compile(view, mod)
+
+ # Just discard the source if we have a virtual path. This
+ # means we can get the template back.
+ @source = nil if @virtual_path
+ @compiled = true
+ end
+
# Among other things, this method is responsible for properly setting
# the encoding of the source. Until this point, we assume that the
# source is BINARY data. If no additional information is supplied,
@@ -185,11 +239,8 @@ module ActionView
# encode the source into Encoding.default_internal. In general,
# this means that templates will be UTF-8 inside of Rails,
# regardless of the original source encoding.
- def compile(locals, view, mod)
- method_name = build_method_name(locals)
- return method_name if view.respond_to?(method_name)
-
- locals_code = locals.keys.map! { |key| "#{key} = local_assigns[:#{key}];" }.join
+ def compile(view, mod) #:nodoc:
+ method_name = self.method_name
if source.encoding_aware?
# Look for # encoding: *. If we find one, we'll encode the
@@ -223,15 +274,16 @@ module ActionView
end
end
- code = @handler.call(self)
+ arity = @handler.respond_to?(:arity) ? @handler.arity : @handler.method(:call).arity
+ code = arity.abs == 1 ? @handler.call(self) : @handler.call(self, view)
# Make sure that the resulting String to be evalled is in the
# encoding of the code
source = <<-end_src
def #{method_name}(local_assigns)
- _old_virtual_path, @_virtual_path = @_virtual_path, #{@virtual_path.inspect};_old_output_buffer = @output_buffer;#{locals_code};#{code}
+ _old_output_buffer = @output_buffer;#{locals_code};#{code}
ensure
- @_virtual_path, @output_buffer = _old_virtual_path, _old_output_buffer
+ @output_buffer = _old_output_buffer
end
end_src
@@ -254,8 +306,6 @@ module ActionView
begin
mod.module_eval(source, identifier, 0)
ObjectSpace.define_finalizer(self, Finalizer[method_name, mod])
-
- method_name
rescue Exception => e # errors from template code
if logger = (view && view.logger)
logger.debug "ERROR: compiling #{method_name} RAISED #{e}"
@@ -267,12 +317,27 @@ module ActionView
end
end
- def build_method_name(locals)
- @method_names[locals.keys.hash] ||= "_#{identifier_method_name}__#{@identifier.hash}_#{__id__}_#{locals.keys.hash}".gsub('-', "_")
+ def handle_render_error(view, e) #:nodoc:
+ if e.is_a?(Template::Error)
+ e.sub_template_of(self)
+ raise e
+ else
+ assigns = view.respond_to?(:assigns) ? view.assigns : {}
+ template = @virtual_path ? refresh(view) : self
+ raise Template::Error.new(template, assigns, e)
+ end
+ end
+
+ def locals_code #:nodoc:
+ @locals.map { |key| "#{key} = local_assigns[:#{key}];" }.join
+ end
+
+ def method_name #:nodoc:
+ @method_name ||= "_#{identifier_method_name}__#{@identifier.hash}_#{__id__}".gsub('-', "_")
end
- def identifier_method_name
- @identifier_method_name ||= inspect.gsub(/[^a-z_]/, '_')
+ def identifier_method_name #:nodoc:
+ inspect.gsub(/[^a-z_]/, '_')
end
end
end
diff --git a/actionpack/lib/action_view/template/error.rb b/actionpack/lib/action_view/template/error.rb
index b1839b65e5..e246646963 100644
--- a/actionpack/lib/action_view/template/error.rb
+++ b/actionpack/lib/action_view/template/error.rb
@@ -27,7 +27,7 @@ module ActionView
class MissingTemplate < ActionViewError #:nodoc:
attr_reader :path
- def initialize(paths, path, details, partial)
+ def initialize(paths, path, prefixes, partial, details, *)
@path = path
display_paths = paths.compact.map{ |p| p.to_s.inspect }.join(", ")
template_type = if partial
@@ -38,20 +38,27 @@ module ActionView
'template'
end
- super("Missing #{template_type} #{path} with #{details.inspect} in view paths #{display_paths}")
+ searched_paths = prefixes.map { |prefix| [prefix, path].join("/") }
+
+ out = "Missing #{template_type} #{searched_paths.join(", ")} with #{details.inspect}. Searched in:\n"
+ out += paths.compact.map { |p| " * #{p.to_s.inspect}\n" }.join
+ super out
end
end
class Template
- # The Template::Error exception is raised when the compilation of the template fails. This exception then gathers a
- # bunch of intimate details and uses it to report a very precise exception message.
+ # The Template::Error exception is raised when the compilation or rendering of the template
+ # fails. This exception then gathers a bunch of intimate details and uses it to report a
+ # precise exception message.
class Error < ActionViewError #:nodoc:
SOURCE_CODE_RADIUS = 3
attr_reader :original_exception, :backtrace
def initialize(template, assigns, original_exception)
+ super(original_exception.message)
@template, @assigns, @original_exception = template, assigns.dup, original_exception
+ @sub_templates = nil
@backtrace = original_exception.backtrace
end
@@ -59,10 +66,6 @@ module ActionView
@template.identifier
end
- def message
- ActiveSupport::Deprecation.silence { original_exception.message }
- end
-
def sub_template_message
if @sub_templates
"Trace of template inclusion: " +
diff --git a/actionpack/lib/action_view/template/handler.rb b/actionpack/lib/action_view/template/handler.rb
index 8ecc911519..636f3ebbad 100644
--- a/actionpack/lib/action_view/template/handler.rb
+++ b/actionpack/lib/action_view/template/handler.rb
@@ -1,4 +1,4 @@
-require "action_dispatch/http/mime_type"
+require 'action_dispatch/http/mime_type'
require 'active_support/core_ext/class/attribute'
# Legacy TemplateHandler stub
@@ -7,6 +7,8 @@ module ActionView
module Handlers #:nodoc:
module Compilable
def self.included(base)
+ ActiveSupport::Deprecation.warn "Including Compilable in your template handler is deprecated. " <<
+ "Since Rails 3, all the API your template handler needs to implement is to respond to #call."
base.extend(ClassMethods)
end
@@ -26,6 +28,12 @@ module ActionView
class_attribute :default_format
self.default_format = Mime::HTML
+ def self.inherited(base)
+ ActiveSupport::Deprecation.warn "Inheriting from ActionView::Template::Handler is deprecated. " <<
+ "Since Rails 3, all the API your template handler needs to implement is to respond to #call."
+ super
+ end
+
def self.call(template)
raise "Need to implement #{self.class.name}#call(template)"
end
@@ -35,7 +43,7 @@ module ActionView
end
end
end
-
+
TemplateHandlers = Template::Handlers
TemplateHandler = Template::Handler
end
diff --git a/actionpack/lib/action_view/template/handlers.rb b/actionpack/lib/action_view/template/handlers.rb
index 84d6474dd1..4438199497 100644
--- a/actionpack/lib/action_view/template/handlers.rb
+++ b/actionpack/lib/action_view/template/handlers.rb
@@ -7,18 +7,14 @@ module ActionView #:nodoc:
autoload :Builder, 'action_view/template/handlers/builder'
def self.extended(base)
- base.register_default_template_handler :erb, ERB
- base.register_template_handler :rjs, RJS
- base.register_template_handler :builder, Builder
-
- # TODO: Depreciate old template extensions
- base.register_template_handler :rhtml, ERB
- base.register_template_handler :rxml, Builder
+ base.register_default_template_handler :erb, ERB.new
+ base.register_template_handler :rjs, RJS.new
+ base.register_template_handler :builder, Builder.new
end
@@template_handlers = {}
@@default_template_handlers = nil
-
+
def self.extensions
@@template_extensions ||= @@template_handlers.keys
end
@@ -48,7 +44,13 @@ module ActionView #:nodoc:
end
def handler_class_for_extension(extension)
- (extension && registered_template_handler(extension.to_sym)) || @@default_template_handlers
+ ActiveSupport::Deprecation.warn "handler_class_for_extension is deprecated. " <<
+ "Please use handler_for_extension instead", caller
+ handler_for_extension(extension)
+ end
+
+ def handler_for_extension(extension)
+ registered_template_handler(extension) || @@default_template_handlers
end
end
end
diff --git a/actionpack/lib/action_view/template/handlers/builder.rb b/actionpack/lib/action_view/template/handlers/builder.rb
index a93cfca8aa..2c52cfd90e 100644
--- a/actionpack/lib/action_view/template/handlers/builder.rb
+++ b/actionpack/lib/action_view/template/handlers/builder.rb
@@ -1,11 +1,11 @@
module ActionView
module Template::Handlers
- class Builder < Template::Handler
- include Compilable
-
+ class Builder
+ # Default format used by Builder.
+ class_attribute :default_format
self.default_format = Mime::XML
- def compile(template)
+ def call(template)
require 'builder'
"xml = ::Builder::XmlMarkup.new(:indent => 2);" +
"self.output_buffer = xml.target!;" +
diff --git a/actionpack/lib/action_view/template/handlers/erb.rb b/actionpack/lib/action_view/template/handlers/erb.rb
index ce609e01af..a36837afc8 100644
--- a/actionpack/lib/action_view/template/handlers/erb.rb
+++ b/actionpack/lib/action_view/template/handlers/erb.rb
@@ -1,6 +1,7 @@
require 'active_support/core_ext/class/attribute_accessors'
require 'active_support/core_ext/string/output_safety'
-require "action_view/template"
+require 'action_view/template'
+require 'action_view/template/handler'
require 'erubis'
module ActionView
@@ -14,13 +15,7 @@ module ActionView
super(value.to_s)
end
alias :append= :<<
-
- def append_if_string=(value)
- if value.is_a?(String) && !value.is_a?(NonConcattingString)
- ActiveSupport::Deprecation.warn("<% %> style block helpers are deprecated. Please use <%= %>", caller)
- self << value
- end
- end
+ alias :safe_append= :safe_concat
end
class Template
@@ -45,45 +40,44 @@ module ActionView
end
end
- def add_stmt(src, code)
+ def add_expr_escaped(src, code)
if code =~ BLOCK_EXPR
- src << '@output_buffer.append_if_string= ' << code
+ src << "@output_buffer.safe_append= " << code
else
- super
+ src << "@output_buffer.safe_concat((" << code << ").to_s);"
end
end
- def add_expr_escaped(src, code)
- src << '@output_buffer.append= ' << escaped_expr(code) << ';'
- end
-
def add_postamble(src)
src << '@output_buffer.to_s'
end
end
- class ERB < Handler
- include Compilable
-
- ##
- # :singleton-method:
+ class ERB
# Specify trim mode for the ERB compiler. Defaults to '-'.
# See ERb documentation for suitable values.
- cattr_accessor :erb_trim_mode
+ class_attribute :erb_trim_mode
self.erb_trim_mode = '-'
+ # Default format used by ERB.
+ class_attribute :default_format
self.default_format = Mime::HTML
- cattr_accessor :erb_implementation
+ # Default implementation used.
+ class_attribute :erb_implementation
self.erb_implementation = Erubis
ENCODING_TAG = Regexp.new("\\A(<%#{ENCODING_FLAG}-?%>)[ \\t]*")
- def self.handles_encoding?
+ def self.call(template)
+ new.call(template)
+ end
+
+ def handles_encoding?
true
end
- def compile(template)
+ def call(template)
if template.source.encoding_aware?
# First, convert to BINARY, so in case the encoding is
# wrong, we can still find an encoding tag
@@ -109,6 +103,7 @@ module ActionView
end
private
+
def valid_encoding(string, encoding)
# If a magic encoding comment was found, tag the
# String with this encoding. This is for a case
diff --git a/actionpack/lib/action_view/template/handlers/rjs.rb b/actionpack/lib/action_view/template/handlers/rjs.rb
index 128be5077c..9d71059134 100644
--- a/actionpack/lib/action_view/template/handlers/rjs.rb
+++ b/actionpack/lib/action_view/template/handlers/rjs.rb
@@ -1,17 +1,13 @@
module ActionView
module Template::Handlers
- class RJS < Template::Handler
- include Compilable
-
+ class RJS
+ # Default format used by RJS.
+ class_attribute :default_format
self.default_format = Mime::JS
- def compile(template)
+ def call(template)
"update_page do |page|;#{template.source}\nend"
end
-
- def default_format
- Mime::JS
- end
end
end
end
diff --git a/actionpack/lib/action_view/template/resolver.rb b/actionpack/lib/action_view/template/resolver.rb
index c9e20ca14e..6c1063592f 100644
--- a/actionpack/lib/action_view/template/resolver.rb
+++ b/actionpack/lib/action_view/template/resolver.rb
@@ -5,9 +5,35 @@ require "action_view/template"
module ActionView
# = Action View Resolver
class Resolver
+ # Keeps all information about view path and builds virtual path.
+ class Path < String
+ attr_reader :name, :prefix, :partial, :virtual
+ alias_method :partial?, :partial
+
+ def initialize(name, prefix, partial)
+ @name, @prefix, @partial = name, prefix, partial
+ rebuild(@name, @prefix, @partial)
+ end
+
+ def rebuild(name, prefix, partial)
+ @virtual = ""
+ @virtual << "#{prefix}/" unless prefix.empty?
+ @virtual << (partial ? "_#{name}" : name)
+
+ self.replace(@virtual)
+ end
+ end
+
+ cattr_accessor :caching
+ self.caching = true
+
+ class << self
+ alias :caching? :caching
+ end
+
def initialize
- @cached = Hash.new { |h1,k1| h1[k1] =
- Hash.new { |h2,k2| h2[k2] = Hash.new { |h3, k3| h3[k3] = {} } } }
+ @cached = Hash.new { |h1,k1| h1[k1] = Hash.new { |h2,k2|
+ h2[k2] = Hash.new { |h3,k3| h3[k3] = Hash.new { |h4,k4| h4[k4] = {} } } } }
end
def clear_cache
@@ -15,68 +41,131 @@ module ActionView
end
# Normalizes the arguments and passes it on to find_template.
- def find_all(name, prefix=nil, partial=false, details={}, key=nil)
- cached(key, prefix, name, partial) do
+ def find_all(name, prefix=nil, partial=false, details={}, key=nil, locals=[])
+ cached(key, [name, prefix, partial], details, locals) do
find_templates(name, prefix, partial, details)
end
end
private
- def caching?
- @caching ||= !defined?(Rails.application) || Rails.application.config.cache_classes
- end
+ delegate :caching?, :to => "self.class"
# This is what child classes implement. No defaults are needed
# because Resolver guarantees that the arguments are present and
# normalized.
def find_templates(name, prefix, partial, details)
- raise NotImplementedError
+ raise NotImplementedError, "Subclasses must implement a find_templates(name, prefix, partial, details) method"
end
- def cached(key, prefix, name, partial)
- return yield unless key && caching?
- @cached[key][prefix][name][partial] ||= yield
+ # Helpers that builds a path. Useful for building virtual paths.
+ def build_path(name, prefix, partial)
+ Path.new(name, prefix, partial)
+ end
+
+ # Handles templates caching. If a key is given and caching is on
+ # always check the cache before hitting the resolver. Otherwise,
+ # it always hits the resolver but check if the resolver is fresher
+ # before returning it.
+ def cached(key, path_info, details, locals) #:nodoc:
+ name, prefix, partial = path_info
+ locals = sort_locals(locals)
+
+ if key && caching?
+ @cached[key][name][prefix][partial][locals] ||= decorate(yield, path_info, details, locals)
+ else
+ fresh = decorate(yield, path_info, details, locals)
+ return fresh unless key
+
+ scope = @cached[key][name][prefix][partial]
+ cache = scope[locals]
+ mtime = cache && cache.map(&:updated_at).max
+
+ if !mtime || fresh.empty? || fresh.any? { |t| t.updated_at > mtime }
+ scope[locals] = fresh
+ else
+ cache
+ end
+ end
+ end
+
+ # Ensures all the resolver information is set in the template.
+ def decorate(templates, path_info, details, locals) #:nodoc:
+ cached = nil
+ templates.each do |t|
+ t.locals = locals
+ t.formats = details[:formats] || [:html] if t.formats.empty?
+ t.virtual_path ||= (cached ||= build_path(*path_info))
+ end
+ end
+
+ if :symbol.respond_to?("<=>")
+ def sort_locals(locals) #:nodoc:
+ locals.sort.freeze
+ end
+ else
+ def sort_locals(locals) #:nodoc:
+ locals = locals.map{ |l| l.to_s }
+ locals.sort!
+ locals.freeze
+ end
end
end
class PathResolver < Resolver
- EXTENSION_ORDER = [:locale, :formats, :handlers]
+ EXTENSIONS = [:locale, :formats, :handlers]
+ DEFAULT_PATTERN = ":prefix/:action{.:locale,}{.:formats,}{.:handlers,}"
- def to_s
- @path.to_s
+ def initialize(pattern=nil)
+ @pattern = pattern || DEFAULT_PATTERN
+ super()
end
- alias :to_path :to_s
- private
+ private
def find_templates(name, prefix, partial, details)
- path = build_path(name, prefix, partial, details)
- query(path, EXTENSION_ORDER.map { |ext| details[ext] }, details[:formats])
- end
-
- def build_path(name, prefix, partial, details)
- path = ""
- path << "#{prefix}/" unless prefix.empty?
- path << (partial ? "_#{name}" : name)
- path
+ path = build_path(name, prefix, partial)
+ extensions = Hash[EXTENSIONS.map { |ext| [ext, details[ext]] }.flatten(0)]
+ query(path, extensions, details[:formats])
end
def query(path, exts, formats)
- query = File.join(@path, path)
+ query = build_query(path, exts)
+ templates = []
+ sanitizer = Hash.new { |h,k| h[k] = Dir["#{File.dirname(k)}/*"] }
- exts.each do |ext|
- query << '{' << ext.map {|e| e && ".#{e}" }.join(',') << ',}'
- end
+ Dir[query].each do |p|
+ next if File.directory?(p) || !sanitizer[p].include?(p)
- Dir[query].reject { |p| File.directory?(p) }.map do |p|
handler, format = extract_handler_and_format(p, formats)
-
contents = File.open(p, "rb") {|io| io.read }
- Template.new(contents, File.expand_path(p), handler,
- :virtual_path => path, :format => format)
+ templates << Template.new(contents, File.expand_path(p), handler,
+ :virtual_path => path.virtual, :format => format, :updated_at => mtime(p))
end
+
+ templates
+ end
+
+ # Helper for building query glob string based on resolver's pattern.
+ def build_query(path, exts)
+ query = @pattern.dup
+ query.gsub!(/\:prefix(\/)?/, path.prefix.empty? ? "" : "#{path.prefix}\\1") # prefix can be empty...
+ query.gsub!(/\:action/, path.partial? ? "_#{path.name}" : path.name)
+
+ exts.each { |ext, variants|
+ query.gsub!(/\:#{ext}/, "{#{variants.compact.uniq.join(',')}}")
+ }
+
+ query.gsub!(/\.{html,/, ".{html,text.html,")
+ query.gsub!(/\.{text,/, ".{text,text.plain,")
+
+ File.expand_path(query, @path)
+ end
+
+ # Returns the file mtime from the filesystem.
+ def mtime(p)
+ File.stat(p).mtime
end
# Extract handler and formats from path. If a format cannot be a found neither
@@ -85,26 +174,76 @@ module ActionView
def extract_handler_and_format(path, default_formats)
pieces = File.basename(path).split(".")
pieces.shift
-
- handler = Template.handler_class_for_extension(pieces.pop)
- format = pieces.last && Mime[pieces.last] && pieces.pop.to_sym
- format ||= handler.default_format if handler.respond_to?(:default_format)
- format ||= default_formats
-
+ handler = Template.handler_for_extension(pieces.pop)
+ format = pieces.last && Mime[pieces.last]
[handler, format]
end
end
+ # A resolver that loads files from the filesystem. It allows to set your own
+ # resolving pattern. Such pattern can be a glob string supported by some variables.
+ #
+ # ==== Examples
+ #
+ # Default pattern, loads views the same way as previous versions of rails, eg. when you're
+ # looking for `users/new` it will produce query glob: `users/new{.{en},}{.{html,js},}{.{erb,haml,rjs},}`
+ #
+ # FileSystemResolver.new("/path/to/views", ":prefix/:action{.:locale,}{.:formats,}{.:handlers,}")
+ #
+ # This one allows you to keep files with different formats in seperated subdirectories,
+ # eg. `users/new.html` will be loaded from `users/html/new.erb` or `users/new.html.erb`,
+ # `users/new.js` from `users/js/new.erb` or `users/new.js.erb`, etc.
+ #
+ # FileSystemResolver.new("/path/to/views", ":prefix/{:formats/,}:action{.:locale,}{.:formats,}{.:handlers,}")
+ #
+ # If you don't specify pattern then the default will be used.
+ #
+ # In order to use any of the customized resolvers above in a Rails application, you just need
+ # to configure ActionController::Base.view_paths in an initializer, for example:
+ #
+ # ActionController::Base.view_paths = FileSystemResolver.new(
+ # Rails.root.join("app/views"),
+ # ":prefix{/:locale}/:action{.:formats,}{.:handlers,}"
+ # )
+ #
+ # ==== Pattern format and variables
+ #
+ # Pattern have to be a valid glob string, and it allows you to use the
+ # following variables:
+ #
+ # * <tt>:prefix</tt> - usualy the controller path
+ # * <tt>:action</tt> - name of the action
+ # * <tt>:locale</tt> - possible locale versions
+ # * <tt>:formats</tt> - possible request formats (for example html, json, xml...)
+ # * <tt>:handlers</tt> - possible handlers (for example erb, haml, builder...)
+ #
class FileSystemResolver < PathResolver
- def initialize(path)
+ def initialize(path, pattern=nil)
raise ArgumentError, "path already is a Resolver class" if path.is_a?(Resolver)
- super()
+ super(pattern)
@path = File.expand_path(path)
end
+ def to_s
+ @path.to_s
+ end
+ alias :to_path :to_s
+
def eql?(resolver)
self.class.equal?(resolver.class) && to_path == resolver.to_path
end
alias :== :eql?
end
+
+ # The same as FileSystemResolver but does not allow templates to store
+ # a virtual path since it is invalid for such resolvers.
+ class FallbackFileSystemResolver < FileSystemResolver
+ def self.instances
+ [new(""), new("/")]
+ end
+
+ def decorate(*)
+ super.each { |t| t.virtual_path = nil }
+ end
+ end
end
diff --git a/actionpack/lib/action_view/template/text.rb b/actionpack/lib/action_view/template/text.rb
index 51be831dfb..4261c3b5e2 100644
--- a/actionpack/lib/action_view/template/text.rb
+++ b/actionpack/lib/action_view/template/text.rb
@@ -25,10 +25,6 @@ module ActionView #:nodoc:
def formats
[@mime_type.to_sym]
end
-
- def partial?
- false
- end
end
end
end
diff --git a/actionpack/lib/action_view/test_case.rb b/actionpack/lib/action_view/test_case.rb
index 137281e5e9..3e2ddffa16 100644
--- a/actionpack/lib/action_view/test_case.rb
+++ b/actionpack/lib/action_view/test_case.rb
@@ -20,12 +20,12 @@ module ActionView
end
def initialize
+ super
self.class.controller_path = ""
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
@request.env.delete('PATH_INFO')
-
@params = {}
end
end
@@ -44,7 +44,7 @@ module ActionView
include ActionView::Helpers
attr_accessor :controller, :output_buffer, :rendered
-
+
module ClassMethods
def tests(helper_class)
self.helper_class = helper_class
@@ -74,6 +74,11 @@ module ActionView
@helper_class ||= determine_default_helper_class(name)
end
+ def new(*)
+ include_helper_modules!
+ super
+ end
+
private
def include_helper_modules!
@@ -89,7 +94,6 @@ module ActionView
@output_buffer = ActiveSupport::SafeBuffer.new
@rendered = ''
- self.class.send(:include_helper_modules!)
make_test_case_available_to_view!
say_no_to_protect_against_forgery!
end
@@ -99,7 +103,7 @@ module ActionView
end
def render(options = {}, local_assigns = {}, &block)
- view.assign(_assigns)
+ view.assign(view_assigns)
@rendered << output = view.render(options, local_assigns, &block)
output
end
@@ -123,6 +127,7 @@ module ActionView
def say_no_to_protect_against_forgery!
_helpers.module_eval do
+ remove_method :protect_against_forgery? if method_defined?(:protect_against_forgery?)
def protect_against_forgery?
false
end
@@ -132,8 +137,10 @@ module ActionView
def make_test_case_available_to_view!
test_case_instance = self
_helpers.module_eval do
- define_method(:_test_case) { test_case_instance }
- private :_test_case
+ unless private_method_defined?(:_test_case)
+ define_method(:_test_case) { test_case_instance }
+ private :_test_case
+ end
end
end
@@ -149,47 +156,59 @@ module ActionView
# The instance of ActionView::Base that is used by +render+.
def view
@view ||= begin
- view = ActionView::Base.new(ActionController::Base.view_paths, {}, @controller)
- view.singleton_class.send :include, _helpers
- view.singleton_class.send :include, @controller._routes.url_helpers
- view.singleton_class.send :delegate, :alert, :notice, :to => "request.flash"
- view.extend(Locals)
- view.locals = self.locals
- view.output_buffer = self.output_buffer
- view
- end
+ view = @controller.view_context
+ view.singleton_class.send :include, _helpers
+ view.extend(Locals)
+ view.locals = self.locals
+ view.output_buffer = self.output_buffer
+ view
+ end
end
alias_method :_view, :view
- EXCLUDE_IVARS = %w{
+ INTERNAL_IVARS = %w{
+ @__name__
+ @__io__
@_assertion_wrapped
+ @_assertions
@_result
+ @_routes
@controller
@layouts
@locals
@method_name
@output_buffer
@partials
+ @passed
@rendered
@request
@routes
@templates
+ @options
@test_passed
@view
@view_context_class
}
- def _instance_variables
- instance_variables.map(&:to_s) - EXCLUDE_IVARS
+ def _user_defined_ivars
+ instance_variables.map(&:to_s) - INTERNAL_IVARS
+ end
+
+ # Returns a Hash of instance variables and their values, as defined by
+ # the user in the test case, which are then assigned to the view being
+ # rendered. This is generally intended for internal use and extension
+ # frameworks.
+ def view_assigns
+ Hash[_user_defined_ivars.map do |var|
+ [var[1, var.length].to_sym, instance_variable_get(var)]
+ end]
end
def _assigns
- _instance_variables.inject({}) do |hash, var|
- name = var[1..-1].to_sym
- hash[name] = instance_variable_get(var)
- hash
- end
+ ActiveSupport::Deprecation.warn "ActionView::TestCase#_assigns is deprecated and will be removed in future versions. " <<
+ "Please use view_assigns instead."
+ view_assigns
end
def _routes
@@ -198,7 +217,7 @@ module ActionView
def method_missing(selector, *args)
if @controller.respond_to?(:_routes) &&
- @controller._routes.named_routes.helpers.include?(selector)
+ @controller._routes.named_routes.helpers.include?(selector)
@controller.__send__(selector, *args)
else
super
diff --git a/actionpack/lib/action_view/testing/resolvers.rb b/actionpack/lib/action_view/testing/resolvers.rb
index 578c56c6c4..773dfcbb1d 100644
--- a/actionpack/lib/action_view/testing/resolvers.rb
+++ b/actionpack/lib/action_view/testing/resolvers.rb
@@ -8,36 +8,43 @@ module ActionView #:nodoc:
class FixtureResolver < PathResolver
attr_reader :hash
- def initialize(hash = {})
- super()
+ def initialize(hash = {}, pattern=nil)
+ super(pattern)
@hash = hash
end
+ def to_s
+ @hash.keys.join(', ')
+ end
+
private
def query(path, exts, formats)
- query = Regexp.escape(path)
- exts.each do |ext|
- query << '(' << ext.map {|e| e && Regexp.escape(".#{e}") }.join('|') << '|)'
+ query = ""
+ EXTENSIONS.each do |ext|
+ query << '(' << exts[ext].map {|e| e && Regexp.escape(".#{e}") }.join('|') << '|)'
end
+ query = /^(#{Regexp.escape(path)})#{query}$/
templates = []
- @hash.select { |k,v| k =~ /^#{query}$/ }.each do |path, source|
- handler, format = extract_handler_and_format(path, formats)
- templates << Template.new(source, path, handler,
- :virtual_path => path, :format => format)
+ @hash.each do |_path, array|
+ source, updated_at = array
+ next unless _path =~ query
+ handler, format = extract_handler_and_format(_path, formats)
+ templates << Template.new(source, _path, handler,
+ :virtual_path => path.virtual, :format => format, :updated_at => updated_at)
end
-
+
templates.sort_by {|t| -t.identifier.match(/^#{query}$/).captures.reject(&:blank?).size }
end
end
- class NullResolver < ActionView::PathResolver
+ class NullResolver < PathResolver
def query(path, exts, formats)
handler, format = extract_handler_and_format(path, formats)
[ActionView::Template.new("Template generated by Null Resolver", path, handler, :virtual_path => path, :format => format)]
end
end
-
+
end