diff options
author | Gonçalo Silva <goncalossilva@gmail.com> | 2011-03-24 17:21:17 +0000 |
---|---|---|
committer | Gonçalo Silva <goncalossilva@gmail.com> | 2011-03-24 17:21:17 +0000 |
commit | 9887f238871bb2dd73de6ce8855615bcc5d8d079 (patch) | |
tree | 74fa9ff9524a51701cfa23f708b3f777c65b7fe5 /actionpack/lib | |
parent | aff821508a16245ebc03510ba29c70379718dfb7 (diff) | |
parent | 5214e73850916de3c9127d35a4ecee0424d364a3 (diff) | |
download | rails-9887f238871bb2dd73de6ce8855615bcc5d8d079.tar.gz rails-9887f238871bb2dd73de6ce8855615bcc5d8d079.tar.bz2 rails-9887f238871bb2dd73de6ce8855615bcc5d8d079.zip |
Merge branch 'master' of https://github.com/rails/rails
Diffstat (limited to 'actionpack/lib')
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=" ") @@ -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 = /:|(�*58)|(p)|(%|%)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 <font> 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(/</, "<") 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 =~ /(^[^\/:]*):|(�*58)|(p)|(%|%)3A/ && !allowed_protocols.include?(value.split(protocol_separator).first)) + uri_attributes.include?(attr_name) && + (value =~ /(^[^\/:]*):|(�*58)|(p)|(%|%)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 => "☃".html_safe) + :name => "utf8", :value => "✓".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 => "£", :separator => ",", :delimiter => "") # # => £1234567890,50 # number_to_currency(1234567890.50, :unit => "£", :separator => ",", :delimiter => "", :format => "%n %u") # # => 1234567890,50 £ 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><br /><p>bar</p>" + # + # 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 & shut.png" /> # - # tag("img", { :src => "open & shut.png" }, false, false) + # tag("img", {:src => "open & shut.png"}, false, false) # # => <img src="open & shut.png" /> + # + # tag("div", :data => {:name => 'Stephen', :city_state => %w(Chicago IL)}) + # # => <div data-name="Stephen" data-city-state="["Chicago","IL"]" /> 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><a href=\"http://example.com/\">Example</a></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 |