diff options
author | Pratik Naik <pratiknaik@gmail.com> | 2009-11-17 22:47:23 +0000 |
---|---|---|
committer | Pratik Naik <pratiknaik@gmail.com> | 2009-11-17 22:47:23 +0000 |
commit | 5446d5cb05b50a9a3f317ded774be438e0eff909 (patch) | |
tree | 6b0b87efe3e95783763208215a3159fb63217a6d /actionpack/lib | |
parent | 9754debb9a72f9385950e5282f3642b995ab76d8 (diff) | |
parent | f8877d4b2a2a6f68770b376f0b1391a6295f62f2 (diff) | |
download | rails-5446d5cb05b50a9a3f317ded774be438e0eff909.tar.gz rails-5446d5cb05b50a9a3f317ded774be438e0eff909.tar.bz2 rails-5446d5cb05b50a9a3f317ded774be438e0eff909.zip |
Merge remote branch 'mainstream/master'
Conflicts:
activesupport/lib/active_support/core_ext/hash/conversions.rb
Diffstat (limited to 'actionpack/lib')
81 files changed, 2211 insertions, 3027 deletions
diff --git a/actionpack/lib/abstract_controller.rb b/actionpack/lib/abstract_controller.rb index 76c5845f5b..1a6c4278c9 100644 --- a/actionpack/lib/abstract_controller.rb +++ b/actionpack/lib/abstract_controller.rb @@ -6,6 +6,7 @@ module AbstractController autoload :Callbacks, "abstract_controller/callbacks" autoload :Helpers, "abstract_controller/helpers" autoload :Layouts, "abstract_controller/layouts" + autoload :LocalizedCache, "abstract_controller/localized_cache" autoload :Logger, "abstract_controller/logger" autoload :RenderingController, "abstract_controller/rendering_controller" # === Exceptions diff --git a/actionpack/lib/abstract_controller/callbacks.rb b/actionpack/lib/abstract_controller/callbacks.rb index 379eaf6d8e..ee496dadc5 100644 --- a/actionpack/lib/abstract_controller/callbacks.rb +++ b/actionpack/lib/abstract_controller/callbacks.rb @@ -1,13 +1,11 @@ -require "active_support/new_callbacks" - module AbstractController module Callbacks extend ActiveSupport::Concern - # Uses ActiveSupport::NewCallbacks as the base functionality. For + # Uses ActiveSupport::Callbacks as the base functionality. For # more details on the whole callback system, read the documentation - # for ActiveSupport::NewCallbacks. - include ActiveSupport::NewCallbacks + # for ActiveSupport::Callbacks. + include ActiveSupport::Callbacks included do define_callbacks :process_action, :terminator => "response_body" @@ -16,7 +14,7 @@ module AbstractController # Override AbstractController::Base's process_action to run the # process_action callbacks around the normal behavior. def process_action(method_name) - _run_process_action_callbacks(method_name) do + run_callbacks(:process_action, method_name) do super end end diff --git a/actionpack/lib/abstract_controller/helpers.rb b/actionpack/lib/abstract_controller/helpers.rb index f3072fad74..d3b492ad09 100644 --- a/actionpack/lib/abstract_controller/helpers.rb +++ b/actionpack/lib/abstract_controller/helpers.rb @@ -1,3 +1,5 @@ +require 'active_support/dependencies' + module AbstractController module Helpers extend ActiveSupport::Concern @@ -57,23 +59,48 @@ module AbstractController end end - # Make a number of helper modules part of this class' default - # helpers. + # The +helper+ class method can take a series of helper module names, a block, or both. # # ==== Parameters - # *args<Array[Module]>:: Modules to be included - # block<Block>:: Evalulate the block in the context - # of the helper module. Any methods defined in the block - # will be helpers. + # *args<Array[Module, Symbol, String, :all]> + # block<Block>:: A block defining helper methods + # + # ==== Examples + # When the argument is a module it will be included directly in the template class. + # helper FooHelper # => includes FooHelper + # + # When the argument is a string or symbol, the method will provide the "_helper" suffix, require the file + # and include the module in the template class. The second form illustrates how to include custom helpers + # when working with namespaced controllers, or other cases where the file containing the helper definition is not + # in one of Rails' standard load paths: + # helper :foo # => requires 'foo_helper' and includes FooHelper + # helper 'resources/foo' # => requires 'resources/foo_helper' and includes Resources::FooHelper + # + # Additionally, the +helper+ class method can receive and evaluate a block, making the methods defined available + # to the template. + # + # # One line + # helper { def hello() "Hello, world!" end } + # + # # Multi-line + # helper do + # def foo(bar) + # "#{bar} is the very best" + # end + # end + # + # Finally, all the above styles can be mixed together, and the +helper+ method can be invoked with a mix of + # +symbols+, +strings+, +modules+ and blocks. + # + # helper(:three, BlindHelper) { def mice() 'mice' end } + # def helper(*args, &block) self._helper_serial = AbstractController::Helpers.next_serial + 1 - args.flatten.each do |arg| - case arg - when Module - add_template_helper(arg) - end + _modules_for_helpers(args).each do |mod| + add_template_helper(mod) end + _helpers.module_eval(&block) if block_given? end @@ -87,6 +114,38 @@ module AbstractController def add_template_helper(mod) _helpers.module_eval { include mod } end + + # Returns a list of modules, normalized from the acceptable kinds of + # helpers with the following behavior: + # + # String or Symbol:: :FooBar or "FooBar" becomes "foo_bar_helper", + # and "foo_bar_helper.rb" is loaded using require_dependency. + # + # Module:: No further processing + # + # After loading the appropriate files, the corresponding modules + # are returned. + # + # ==== Parameters + # args<Array[String, Symbol, Module]>:: A list of helpers + # + # ==== Returns + # Array[Module]:: A normalized list of modules for the list of + # helpers provided. + def _modules_for_helpers(args) + args.flatten.map! do |arg| + case arg + when String, Symbol + file_name = "#{arg.to_s.underscore}_helper" + require_dependency(file_name, "Missing helper file helpers/%s.rb") + file_name.camelize.constantize + when Module + arg + else + raise ArgumentError, "helper must be a String, Symbol, or Module" + end + end + end end end end diff --git a/actionpack/lib/abstract_controller/layouts.rb b/actionpack/lib/abstract_controller/layouts.rb index 796ef40584..c71cef42b2 100644 --- a/actionpack/lib/abstract_controller/layouts.rb +++ b/actionpack/lib/abstract_controller/layouts.rb @@ -89,7 +89,7 @@ module AbstractController # ==== Returns # String:: A template name def _implied_layout_name - name.underscore + name && name.underscore end # Takes the specified layout and creates a _layout method to be called @@ -100,7 +100,7 @@ module AbstractController # name, return that string. Otherwise, use the superclass' # layout (which might also be implied) def _write_layout_method - case @_layout + case defined?(@_layout) ? @_layout : nil when String self.class_eval %{def _layout(details) #{@_layout.inspect} end} when Symbol @@ -119,17 +119,19 @@ module AbstractController when true raise ArgumentError, "Layouts must be specified as a String, Symbol, false, or nil" when nil - self.class_eval <<-RUBY, __FILE__, __LINE__ + 1 - def _layout(details) - self.class.cache_layout(details) do - if template_exists?("#{_implied_layout_name}", details, :_prefix => "layouts") - "#{_implied_layout_name}" - else - super + if name + self.class_eval <<-RUBY, __FILE__, __LINE__ + 1 + def _layout(details) + self.class.cache_layout(details) do + if template_exists?("#{_implied_layout_name}", details, :_prefix => "layouts") + "#{_implied_layout_name}" + else + super + end end end - end - RUBY + RUBY + end end self.class_eval { private :_layout } end @@ -157,6 +159,7 @@ module AbstractController end private + # This will be overwritten by _write_layout_method def _layout(details) end @@ -171,6 +174,34 @@ module AbstractController name && _find_layout(name, details) end + # Determine the layout for a given name and details, taking into account + # the name type. + # + # ==== Parameters + # name<String|TrueClass|FalseClass|Symbol>:: The name of the template + # details<Hash{Symbol => Object}>:: 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, details) + case name + when String then _layout_for_name(name, details) + when true then _default_layout(details, true) + when :default then _default_layout(details, false) + when false, nil then nil + else + raise ArgumentError, + "String, true, or false, expected for `layout'; you passed #{name.inspect}" + end + end + + def _determine_template(options) + super + + return unless (options.keys & [:text, :inline, :partial]).empty? || options.key?(:layout) + layout = options.key?(:layout) ? options[:layout] : :default + options[:_layout] = _layout_for_option(layout, options[:_template].details) + end + # Take in the name and details and find a Template. # # ==== Parameters diff --git a/actionpack/lib/abstract_controller/localized_cache.rb b/actionpack/lib/abstract_controller/localized_cache.rb new file mode 100644 index 0000000000..ee7b43cb9f --- /dev/null +++ b/actionpack/lib/abstract_controller/localized_cache.rb @@ -0,0 +1,49 @@ +module AbstractController + class HashKey + @hash_keys = Hash.new {|h,k| h[k] = Hash.new {|h,k| h[k] = {} } } + + def self.get(klass, formats, locale) + @hash_keys[klass][formats][locale] ||= new(klass, formats, locale) + end + + attr_accessor :hash + def initialize(klass, formats, locale) + @formats, @locale = formats, locale + @hash = [formats, locale].hash + end + + alias_method :eql?, :equal? + + def inspect + "#<HashKey -- formats: #{@formats.inspect} locale: #{@locale.inspect}>" + end + end + + module LocalizedCache + extend ActiveSupport::Concern + + module ClassMethods + def clear_template_caches! + ActionView::Partials::PartialRenderer::TEMPLATES.clear + template_cache.clear + super + end + + def template_cache + @template_cache ||= Hash.new {|h,k| h[k] = {} } + end + end + + def render(options) + Thread.current[:format_locale_key] = HashKey.get(self.class, formats, I18n.locale) + super + end + + private + + def with_template_cache(name) + self.class.template_cache[Thread.current[:format_locale_key]][name] ||= super + end + + end +end diff --git a/actionpack/lib/abstract_controller/logger.rb b/actionpack/lib/abstract_controller/logger.rb index f4d017b8e5..27ba5be45f 100644 --- a/actionpack/lib/abstract_controller/logger.rb +++ b/actionpack/lib/abstract_controller/logger.rb @@ -1,4 +1,5 @@ require 'active_support/core_ext/logger' +require 'active_support/benchmarkable' module AbstractController module Logger @@ -6,22 +7,7 @@ module AbstractController included do cattr_accessor :logger - end - - module ClassMethods #:nodoc: - # Logs a message appending the value measured. - def log_with_time(message, time, log_level=::Logger::DEBUG) - return unless logger && logger.level >= log_level - logger.add(log_level, "#{message} (%.1fms)" % time) - end - - # Silences the logger for the duration of the block. - def silence - old_logger_level, logger.level = logger.level, ::Logger::ERROR if logge - yield - ensure - logger.level = old_logger_level if logger - end + extend ActiveSupport::Benchmarkable end # A class that allows you to defer expensive processing @@ -47,7 +33,7 @@ module AbstractController # Override process_action in the AbstractController::Base # to log details about the method. def process_action(action) - event = ActiveSupport::Orchestra.instrument(:process_action, + result = ActiveSupport::Notifications.instrument(:process_action, :controller => self, :action => action) do super end @@ -56,13 +42,13 @@ module AbstractController log = DelayedLog.new do "\n\nProcessing #{self.class.name}\##{action_name} " \ "to #{request.formats} (for #{request_origin}) " \ - "(%.1fms) [#{request.method.to_s.upcase}]" % event.duration + "[#{request.method.to_s.upcase}]" end logger.info(log) end - event.result + result end private diff --git a/actionpack/lib/abstract_controller/rendering_controller.rb b/actionpack/lib/abstract_controller/rendering_controller.rb index bbf941aa32..7054b9cf26 100644 --- a/actionpack/lib/abstract_controller/rendering_controller.rb +++ b/actionpack/lib/abstract_controller/rendering_controller.rb @@ -8,12 +8,16 @@ module AbstractController included do attr_internal :formats - extlib_inheritable_accessor :_view_paths - self._view_paths ||= ActionView::PathSet.new end + # Initialize controller with nil formats. + def initialize(*) #:nodoc: + @_formats = nil + super + end + # An instance of a view class. The default view class is ActionView::Base # # The view class must have the following methods: @@ -99,6 +103,7 @@ module AbstractController end private + # Take in a set of options and determine the template to render # # ==== Options @@ -109,6 +114,18 @@ module AbstractController # to a directory. # _partial<TrueClass, FalseClass>:: Whether or not the file to look up is a partial def _determine_template(options) + if options.key?(:text) + options[:_template] = ActionView::TextTemplate.new(options[:text], format_for_text) + elsif options.key?(:inline) + handler = ActionView::Template.handler_class_for_extension(options[:type] || "erb") + template = ActionView::Template.new(options[:inline], "inline #{options[:inline].inspect}", handler, {}) + options[:_template] = template + elsif options.key?(:template) + options[:_template_name] = options[:template] + elsif options.key?(:file) + options[:_template_name] = options[:file] + end + name = (options[:_template_name] || action_name).to_s options[:_template] ||= with_template_cache(name) do @@ -128,6 +145,10 @@ module AbstractController yield end + def format_for_text + Mime[:text] + end + module ClassMethods def clear_template_caches! end diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb index 6702cb47f8..03a40e4fce 100644 --- a/actionpack/lib/action_controller.rb +++ b/actionpack/lib/action_controller.rb @@ -2,6 +2,8 @@ module ActionController autoload :Base, "action_controller/base" autoload :Benchmarking, "action_controller/metal/benchmarking" autoload :ConditionalGet, "action_controller/metal/conditional_get" + autoload :Configuration, "action_controller/metal/configuration" + autoload :Head, "action_controller/metal/head" autoload :Helpers, "action_controller/metal/helpers" autoload :HideActions, "action_controller/metal/hide_actions" autoload :Layouts, "action_controller/metal/layouts" @@ -18,22 +20,20 @@ module ActionController autoload :Testing, "action_controller/metal/testing" autoload :UrlFor, "action_controller/metal/url_for" - # Ported modules - # require 'action_controller/routing' autoload :Caching, 'action_controller/caching' autoload :Dispatcher, 'action_controller/dispatch/dispatcher' autoload :Integration, 'action_controller/deprecated/integration_test' autoload :IntegrationTest, 'action_controller/deprecated/integration_test' autoload :MimeResponds, 'action_controller/metal/mime_responds' autoload :PerformanceTest, 'action_controller/deprecated/performance_test' - autoload :PolymorphicRoutes, 'action_controller/routing/generation/polymorphic_routes' + autoload :PolymorphicRoutes, 'action_controller/polymorphic_routes' autoload :RecordIdentifier, 'action_controller/record_identifier' - autoload :Resources, 'action_controller/routing/resources' + autoload :Routing, 'action_controller/deprecated' autoload :SessionManagement, 'action_controller/metal/session_management' autoload :TestCase, 'action_controller/testing/test_case' autoload :TestProcess, 'action_controller/testing/process' - autoload :UrlRewriter, 'action_controller/routing/generation/url_rewriter' - autoload :UrlWriter, 'action_controller/routing/generation/url_rewriter' + autoload :UrlRewriter, 'action_controller/url_rewriter' + autoload :UrlWriter, 'action_controller/url_rewriter' autoload :Verification, 'action_controller/metal/verification' autoload :Flash, 'action_controller/metal/flash' @@ -54,8 +54,6 @@ module ActionController autoload :RenderError, 'action_controller/metal/exceptions' autoload :SessionOverflowError, 'action_controller/metal/exceptions' autoload :UnknownHttpMethod, 'action_controller/metal/exceptions' - - autoload :Routing, 'action_controller/routing' end autoload :HTML, 'action_controller/vendor/html-scanner' diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index 5338a70104..4c026fe5f7 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -15,6 +15,7 @@ module ActionController include ActionController::ConditionalGet include ActionController::RackConvenience include ActionController::Benchmarking + include ActionController::Configuration # Legacy modules include SessionManagement diff --git a/actionpack/lib/action_controller/caching.rb b/actionpack/lib/action_controller/caching.rb index 38cf1da6a8..3caf759032 100644 --- a/actionpack/lib/action_controller/caching.rb +++ b/actionpack/lib/action_controller/caching.rb @@ -3,26 +3,31 @@ require 'uri' require 'set' module ActionController #:nodoc: - # 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. + # 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. # - # You can read more about each approach and the sweeping assistance by clicking the modules below. - # - # Note: To turn off all caching and sweeping, set Base.perform_caching = false. + # You can read more about each approach and the sweeping assistance by clicking the + # modules below. # + # Note: To turn off all caching and sweeping, set + # config.action_controller.perform_caching = false. # # == Caching stores # - # All the caching stores from ActiveSupport::Cache are available to be used as backends for Action Controller caching. This setting only - # affects action and fragment caching as page caching is always written to disk. + # 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 + # as page caching is always written to disk. # # Configuration examples (MemoryStore is the default): # - # ActionController::Base.cache_store = :memory_store - # ActionController::Base.cache_store = :file_store, "/path/to/cache/directory" - # ActionController::Base.cache_store = :drb_store, "druby://localhost:9192" - # ActionController::Base.cache_store = :mem_cache_store, "localhost" - # ActionController::Base.cache_store = MyOwnStore.new("parameter") + # config.action_controller.cache_store = :memory_store + # config.action_controller.cache_store = :file_store, "/path/to/cache/directory" + # config.action_controller.cache_store = :drb_store, "druby://localhost:9192" + # config.action_controller.cache_store = :mem_cache_store, "localhost" + # config.action_controller.cache_store = :mem_cache_store, Memcached::Rails.new("localhost:11211") + # config.action_controller.cache_store = MyOwnStore.new("parameter") module Caching extend ActiveSupport::Concern @@ -46,25 +51,31 @@ module ActionController #:nodoc: @@perform_caching = true cattr_accessor :perform_caching + end - def self.cache_configured? + module ClassMethods + def cache_configured? perform_caching && cache_store end end - protected - # Convenience accessor - def cache(key, options = {}, &block) - if cache_configured? - cache_store.fetch(ActiveSupport::Cache.expand_cache_key(key, :controller), options, &block) - else - yield - end - end + def caching_allowed? + request.get? && response.status == 200 + end - private - def cache_configured? - self.class.cache_configured? + protected + # Convenience accessor + def cache(key, options = {}, &block) + if cache_configured? + cache_store.fetch(ActiveSupport::Cache.expand_cache_key(key, :controller), options, &block) + else + yield end + end + + private + def cache_configured? + self.class.cache_configured? + end end end diff --git a/actionpack/lib/action_controller/caching/actions.rb b/actionpack/lib/action_controller/caching/actions.rb index cb0c3a1384..35111a4b92 100644 --- a/actionpack/lib/action_controller/caching/actions.rb +++ b/actionpack/lib/action_controller/caching/actions.rb @@ -2,9 +2,12 @@ require 'set' 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: + # 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: # # class ListsController < ApplicationController # before_filter :authenticate, :except => :public @@ -12,45 +15,64 @@ module ActionController #:nodoc: # caches_action :index, :show, :feed # 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. + # 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 the subdomain-as-account-key pattern. + # 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 + # 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> - # 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>. + # Different representations of the same resource, e.g. + # <tt>http://david.somewhere.com/lists</tt> and + # <tt>http://david.somewhere.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. + # 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. # - # And you can also use :if (or :unless) 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 :expires_in. # # class ListsController < ApplicationController # before_filter :authenticate, :except => :public # caches_page :public - # caches_action :index, :if => Proc.new { |c| !c.request.format.json? } # cache if is not a JSON request - # caches_action :show, :cache_path => { :project => 1 }, :expires_in => 1.hour - # caches_action :feed, :cache_path => Proc.new { |controller| - # controller.params[:user_id] ? - # controller.send(:user_list_url, controller.params[:user_id], controller.params[:id]) : - # controller.send(:list_url, controller.params[:id]) } + # caches_action :index, :if => proc do |c| + # !c.request.format.json? # cache if is not a JSON request + # end + # + # 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]) + # else + # controller.send(:list_url, controller.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 :layout => false, it will only cache your action + # content. It is useful when your layout has dynamic information. # module Actions - def self.included(base) #:nodoc: - base.extend(ClassMethods) - base.class_eval do - attr_accessor :rendered_action_cache, :action_cache_path - end - end + extend ActiveSupport::Concern module ClassMethods # Declares that +actions+ should be cached. @@ -58,120 +80,83 @@ module ActionController #:nodoc: def caches_action(*actions) return unless cache_configured? options = actions.extract_options! - filter_options = { :only => actions, :if => options.delete(:if), :unless => options.delete(:unless) } - - cache_filter = ActionCacheFilter.new(:layout => options.delete(:layout), :cache_path => options.delete(:cache_path), :store_options => options) + filter_options = options.extract!(:if, :unless).merge(:only => actions) + cache_options = options.extract!(:layout, :cache_path).merge(:store_options => options) - around_filter cache_filter, filter_options + around_filter ActionCacheFilter.new(cache_options), filter_options end end - protected - def expire_action(options = {}) - return unless cache_configured? + def _render_cache_fragment(cache, extension, layout) + render :text => cache, :layout => layout, :content_type => Mime[extension || :html] + end - if options[:action].is_a?(Array) - options[:action].dup.each do |action| - expire_fragment(ActionCachePath.path_for(self, options.merge({ :action => action }), false)) - end - else - expire_fragment(ActionCachePath.path_for(self, options, false)) - end + def _save_fragment(name, layout, options) + return unless caching_allowed? + + content = layout ? view_context.content_for(:layout) : response_body + write_fragment(name, content, options) + end + + protected + def expire_action(options = {}) + return unless cache_configured? + + actions = options[:action] + if actions.is_a?(Array) + actions.each {|action| expire_action(options.merge(:action => action)) } + else + expire_fragment(ActionCachePath.new(self, options, false).path) end + end class ActionCacheFilter #:nodoc: def initialize(options, &block) - @options = options + @cache_path, @store_options, @layout = + options.values_at(:cache_path, :store_options, :layout) end def filter(controller) - should_continue = before(controller) - yield if should_continue - after(controller) - end - - def before(controller) - cache_path = ActionCachePath.new(controller, path_options_for(controller, @options.slice(:cache_path))) - - if cache = controller.read_fragment(cache_path.path, @options[:store_options]) - controller.rendered_action_cache = true - set_content_type!(controller, cache_path.extension) - options = { :text => cache } - options.merge!(:layout => true) if cache_layout? - controller.__send__(:render, options) - false + path_options = if @cache_path.respond_to?(:call) + controller.instance_exec(controller, &@cache_path) else - controller.action_cache_path = cache_path - end - end - - def after(controller) - return if controller.rendered_action_cache || !caching_allowed(controller) - action_content = cache_layout? ? content_for_layout(controller) : controller.response.body - controller.write_fragment(controller.action_cache_path.path, action_content, @options[:store_options]) - end - - private - def set_content_type!(controller, extension) - controller.response.content_type = Mime::Type.lookup_by_extension(extension).to_s if extension + @cache_path end - def path_options_for(controller, options) - ((path_options = options[:cache_path]).respond_to?(:call) ? path_options.call(controller) : path_options) || {} - end - - def caching_allowed(controller) - controller.request.get? && controller.response.status.to_i == 200 - end - - def cache_layout? - @options[:layout] == false - end + cache_path = ActionCachePath.new(controller, path_options || {}) - def content_for_layout(controller) - template = controller.view_context - template.layout && template.instance_variable_get('@cached_content_for_layout') + if cache = controller.read_fragment(cache_path.path, @store_options) + controller._render_cache_fragment(cache, cache_path.extension, @layout == false) + else + yield + controller._save_fragment(cache_path.path, @layout == false, @store_options) end + end end class ActionCachePath attr_reader :path, :extension - class << self - def path_for(controller, options, infer_extension = true) - new(controller, options, infer_extension).path - end - end - - # 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 expiring the cache - - # expire_action should expire the same files regardless of the request format. + # 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 + # expiring the cache - expire_action should expire the same files regardless of the + # request format. def initialize(controller, options = {}, infer_extension = true) if infer_extension - extract_extension(controller.request) - options = options.reverse_merge(:format => @extension) if options.is_a?(Hash) + @extension = controller.params[:format] + options.reverse_merge!(:format => @extension) if options.is_a?(Hash) end - path = controller.url_for(options).split('://').last - normalize!(path) - add_extension!(path, @extension) - @path = URI.unescape(path) + path = controller.url_for(options).split(%r{://}).last + @path = normalize!(path) end - private - def normalize!(path) - path << 'index' if path[-1] == ?/ - end - - def add_extension!(path, extension) - path << ".#{extension}" if extension and !path.ends_with?(extension) - end - - def extract_extension(request) - # Don't want just what comes after the last '.' to accommodate multi part extensions - # such as tar.gz. - @extension = request.path[/^[^.]+\.(.+)$/, 1] || request.cache_format - end + private + def normalize!(path) + path << 'index' if path[-1] == ?/ + path << ".#{extension}" if extension and !path.ends_with?(extension) + URI.unescape(path) + end end end end diff --git a/actionpack/lib/action_controller/caching/fragments.rb b/actionpack/lib/action_controller/caching/fragments.rb index 59e24619e3..8c1167d526 100644 --- a/actionpack/lib/action_controller/caching/fragments.rb +++ b/actionpack/lib/action_controller/caching/fragments.rb @@ -51,40 +51,32 @@ module ActionController #:nodoc: # 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) - event = ActiveSupport::Orchestra.instrument(:write_fragment, :key => key) do + + ActiveSupport::Notifications.instrument(:write_fragment, :key => key) do cache_store.write(key, content, options) end - - self.class.log_with_time("Cached fragment miss: #{key}", event.duration) content end # 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? - key = fragment_cache_key(key) - event = ActiveSupport::Orchestra.instrument(:read_fragment, :key => key) do + + ActiveSupport::Notifications.instrument(:read_fragment, :key => key) do cache_store.read(key, options) end - - self.class.log_with_time("Cached fragment hit: #{key}", event.duration) - event.result end # 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) - event = ActiveSupport::Orchestra.instrument(:fragment_exist?, :key => key) do + + ActiveSupport::Notifications.instrument(:fragment_exist?, :key => key) do cache_store.exist?(key, options) end - - self.class.log_with_time("Cached fragment exists?: #{key}", event.duration) - event.result end # Removes fragments from the cache. @@ -106,11 +98,10 @@ module ActionController #:nodoc: # method (or <tt>delete_matched</tt>, for Regexp keys.) def expire_fragment(key, options = nil) return unless cache_configured? - key = fragment_cache_key(key) unless key.is_a?(Regexp) message = nil - event = ActiveSupport::Orchestra.instrument(:expire_fragment, :key => key) do + ActiveSupport::Notifications.instrument(:expire_fragment, :key => key) do if key.is_a?(Regexp) message = "Expired fragments matching: #{key.source}" cache_store.delete_matched(key, options) @@ -119,9 +110,6 @@ module ActionController #:nodoc: cache_store.delete(key, options) end end - - self.class.log_with_time(message, event.duration) - event.result end end end diff --git a/actionpack/lib/action_controller/caching/pages.rb b/actionpack/lib/action_controller/caching/pages.rb index 4fb154470f..d46f528c7e 100644 --- a/actionpack/lib/action_controller/caching/pages.rb +++ b/actionpack/lib/action_controller/caching/pages.rb @@ -64,12 +64,9 @@ module ActionController #:nodoc: return unless perform_caching path = page_cache_path(path) - event = ActiveSupport::Orchestra.instrument(:expire_page, :path => path) do + ActiveSupport::Notifications.instrument(:expire_page, :path => path) do File.delete(path) if File.exist?(path) end - - log_with_time("Expired page: #{path}", event.duration) - event.result end # Manually cache the +content+ in the key determined by +path+. Example: @@ -78,13 +75,10 @@ module ActionController #:nodoc: return unless perform_caching path = page_cache_path(path) - event = ActiveSupport::Orchestra.instrument(:cache_page, :path => path) do + ActiveSupport::Notifications.instrument(:cache_page, :path => path) do FileUtils.makedirs(File.dirname(path)) File.open(path, "wb+") { |f| f.write(content) } end - - log_with_time("Cached page: #{path}", event.duration) - event.result end # Caches the +actions+ using the page-caching approach that'll store the cache in a path within the page_cache_directory that diff --git a/actionpack/lib/action_controller/caching/sweeping.rb b/actionpack/lib/action_controller/caching/sweeping.rb index c1be264ffb..871f41bfdd 100644 --- a/actionpack/lib/action_controller/caching/sweeping.rb +++ b/actionpack/lib/action_controller/caching/sweeping.rb @@ -70,7 +70,7 @@ module ActionController #:nodoc: protected # gets the action cache path for the given options. def action_path_for(options) - ActionController::Caching::Actions::ActionCachePath.path_for(controller, options) + Actions::ActionCachePath.new(controller, options).path end # Retrieve instance variables set in the controller. diff --git a/actionpack/lib/action_controller/deprecated.rb b/actionpack/lib/action_controller/deprecated.rb index d98e9ac7bd..589061e77c 100644 --- a/actionpack/lib/action_controller/deprecated.rb +++ b/actionpack/lib/action_controller/deprecated.rb @@ -1,2 +1,4 @@ ActionController::AbstractRequest = ActionController::Request = ActionDispatch::Request ActionController::AbstractResponse = ActionController::Response = ActionDispatch::Response +ActionController::Routing = ActionDispatch::Routing +ActionController::Routing::Routes = ActionDispatch::Routing::RouteSet.new diff --git a/actionpack/lib/action_controller/dispatch/dispatcher.rb b/actionpack/lib/action_controller/dispatch/dispatcher.rb index 008fb54715..e04da42637 100644 --- a/actionpack/lib/action_controller/dispatch/dispatcher.rb +++ b/actionpack/lib/action_controller/dispatch/dispatcher.rb @@ -50,7 +50,7 @@ module ActionController def new # DEPRECATE Rails application fallback - Rails.application.new + Rails.application end end end diff --git a/actionpack/lib/action_controller/legacy/layout.rb b/actionpack/lib/action_controller/legacy/layout.rb deleted file mode 100644 index 53762158fc..0000000000 --- a/actionpack/lib/action_controller/legacy/layout.rb +++ /dev/null @@ -1,256 +0,0 @@ -require 'active_support/core_ext/enumerable' -require 'active_support/core_ext/class' -require 'active_support/core_ext/class/delegating_attributes' -require 'active_support/core_ext/class/inheritable_attributes' - -module ActionController #:nodoc: - # MegasuperultraHAX - # plz refactor ActionMailer - class Base - @@exempt_from_layout = [ActionView::TemplateHandlers::RJS] - cattr_accessor :exempt_from_layout - end - - module Layout #:nodoc: - def self.included(base) - base.extend(ClassMethods) - base.class_inheritable_accessor :layout_name, :layout_conditions - end - - # Layouts reverse the common pattern of including shared headers and footers in many templates to isolate changes in - # repeated setups. The inclusion pattern has pages that look like this: - # - # <%= render "shared/header" %> - # Hello World - # <%= render "shared/footer" %> - # - # This approach is a decent way of keeping common structures isolated from the changing content, but it's verbose - # and if you ever want to change the structure of these two includes, you'll have to change all the templates. - # - # With layouts, you can flip it around and have the common structure know where to insert changing content. This means - # that the header and footer are only mentioned in one place, like this: - # - # // The header part of this layout - # <%= yield %> - # // The footer part of this layout - # - # And then you have content pages that look like this: - # - # hello world - # - # At rendering time, the content page is computed and then inserted in the layout, like this: - # - # // The header part of this layout - # hello world - # // The footer part of this layout - # - # == Accessing shared variables - # - # Layouts have access to variables specified in the content pages and vice versa. This allows you to have layouts with - # references that won't materialize before rendering time: - # - # <h1><%= @page_title %></h1> - # <%= yield %> - # - # ...and content pages that fulfill these references _at_ rendering time: - # - # <% @page_title = "Welcome" %> - # Off-world colonies offers you a chance to start a new life - # - # The result after rendering is: - # - # <h1>Welcome</h1> - # Off-world colonies offers you a chance to start a new life - # - # == Automatic layout assignment - # - # If there is a template in <tt>app/views/layouts/</tt> with the same name as the current controller then it will be automatically - # set as that controller's layout unless explicitly told otherwise. Say you have a WeblogController, for example. If a template named - # <tt>app/views/layouts/weblog.erb</tt> or <tt>app/views/layouts/weblog.builder</tt> exists then it will be automatically set as - # the layout for your WeblogController. You can create a layout with the name <tt>application.erb</tt> or <tt>application.builder</tt> - # and this will be set as the default controller if there is no layout with the same name as the current controller and there is - # no layout explicitly assigned with the +layout+ method. Nested controllers use the same folder structure for automatic layout. - # assignment. So an Admin::WeblogController will look for a template named <tt>app/views/layouts/admin/weblog.erb</tt>. - # Setting a layout explicitly will always override the automatic behaviour for the controller where the layout is set. - # Explicitly setting the layout in a parent class, though, will not override the child class's layout assignment if the child - # class has a layout with the same name. - # - # == Inheritance for layouts - # - # Layouts are shared downwards in the inheritance hierarchy, but not upwards. Examples: - # - # class BankController < ActionController::Base - # layout "bank_standard" - # - # class InformationController < BankController - # - # class VaultController < BankController - # layout :access_level_layout - # - # class EmployeeController < BankController - # layout nil - # - # The InformationController uses "bank_standard" inherited from the BankController, the VaultController overwrites - # and picks the layout dynamically, and the EmployeeController doesn't want to use a layout at all. - # - # == Types of layouts - # - # Layouts are basically just regular templates, but the name of this template needs not be specified statically. Sometimes - # you want to alternate layouts depending on runtime information, such as whether someone is logged in or not. This can - # be done either by specifying a method reference as a symbol or using an inline method (as a proc). - # - # The method reference is the preferred approach to variable layouts and is used like this: - # - # class WeblogController < ActionController::Base - # layout :writers_and_readers - # - # def index - # # fetching posts - # end - # - # private - # def writers_and_readers - # logged_in? ? "writer_layout" : "reader_layout" - # end - # - # Now when a new request for the index action is processed, the layout will vary depending on whether the person accessing - # is logged in or not. - # - # If you want to use an inline method, such as a proc, do something like this: - # - # class WeblogController < ActionController::Base - # layout proc{ |controller| controller.logged_in? ? "writer_layout" : "reader_layout" } - # - # 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" - # - # 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. - # - # == Conditional layouts - # - # If you have a layout that by default is applied to all the actions of a controller, you still have the option of rendering - # a given action or set of actions without a layout, or restricting a layout to only a single action or a set of actions. The - # <tt>:only</tt> and <tt>:except</tt> options can be passed to the layout call. For example: - # - # class WeblogController < ActionController::Base - # layout "weblog_standard", :except => :rss - # - # # ... - # - # end - # - # This will assign "weblog_standard" as the WeblogController's layout except for the +rss+ action, which will not wrap a layout - # around the rendered view. - # - # Both the <tt>:only</tt> and <tt>:except</tt> condition can accept an arbitrary number of method references, so - # #<tt>:except => [ :rss, :text_only ]</tt> is valid, as is <tt>:except => :rss</tt>. - # - # == Using a different layout in the action render call - # - # If most of your actions use the same layout, it makes perfect sense to define a controller-wide layout as described above. - # Sometimes you'll have exceptions where one action wants to use a different layout than the rest of the controller. - # You can do this by passing a <tt>:layout</tt> option to the <tt>render</tt> call. For example: - # - # class WeblogController < ActionController::Base - # layout "weblog_standard" - # - # def help - # render :action => "help", :layout => "help" - # end - # end - # - # This will render the help action with the "help" layout instead of the controller-wide "weblog_standard" layout. - module ClassMethods - extend ActiveSupport::Memoizable - - # If a layout is specified, all rendered actions will have their result rendered - # when the layout <tt>yield</tt>s. This layout can itself depend on instance variables assigned during action - # performance and have access to them as any normal template would. - def layout(template_name, conditions = {}, auto = false) - add_layout_conditions(conditions) - self.layout_name = template_name - end - - def memoized_default_layout(formats) #:nodoc: - self.layout_name || begin - layout = default_layout_name - layout.is_a?(String) ? find_layout(layout, formats) : layout - rescue ActionView::MissingTemplate - end - end - - def default_layout(*args) - memoized_default_layout(*args) - @_memoized_default_layout ||= ::ActiveSupport::ConcurrentHash.new - @_memoized_default_layout[args] ||= memoized_default_layout(*args) - end - - def memoized_find_layout(layout, formats) #:nodoc: - return layout if layout.nil? || layout.respond_to?(:render) - prefix = layout.to_s =~ /layouts\// ? nil : "layouts" - find_template(layout.to_s, {:formats => formats}, :_prefix => prefix) - end - - def find_layout(*args) - @_memoized_find_layout ||= ::ActiveSupport::ConcurrentHash.new - @_memoized_find_layout[args] ||= memoized_find_layout(*args) - end - - def layout_list #:nodoc: - Array(view_paths).sum([]) { |path| Dir["#{path}/layouts/**/*"] } - end - memoize :layout_list - - def default_layout_name - layout_match = name.underscore.sub(/_controller$/, '') - if layout_list.grep(%r{layouts/#{layout_match}(\.[a-z][0-9a-z]*)+$}).empty? - superclass.default_layout_name if superclass.respond_to?(:default_layout_name) - else - layout_match - end - end - memoize :default_layout_name - - private - def add_layout_conditions(conditions) - # :except => :foo == :except => [:foo] == :except => "foo" == :except => ["foo"] - conditions.each {|k, v| conditions[k] = Array(v).map {|a| a.to_s} } - write_inheritable_hash(:layout_conditions, conditions) - end - end - - def active_layout(name) - name = self.class.default_layout(formats) if name == true - - layout_name = case name - when Symbol then __send__(name) - when Proc then name.call(self) - else name - end - - self.class.find_layout(layout_name, formats) - end - - def _pick_layout(layout_name = nil, implicit = false) - return unless layout_name || implicit - layout_name = true if layout_name.nil? - active_layout(layout_name) if action_has_layout? && layout_name - end - - private - def action_has_layout? - if conditions = self.class.layout_conditions - if only = conditions[:only] - return only.include?(action_name) - elsif except = conditions[:except] - return !except.include?(action_name) - end - end - true - end - - end -end diff --git a/actionpack/lib/action_controller/metal.rb b/actionpack/lib/action_controller/metal.rb index e9007d3631..60b3f9a89b 100644 --- a/actionpack/lib/action_controller/metal.rb +++ b/actionpack/lib/action_controller/metal.rb @@ -49,7 +49,7 @@ module ActionController # and response object available. You might wish to control the # environment and response manually for performance reasons. - attr_internal :status, :headers, :content_type, :app, :response + attr_internal :status, :headers, :content_type, :response def initialize(*) @_headers = {} @@ -69,7 +69,7 @@ module ActionController end # :api: private - def call(name, env) + def dispatch(name, env) @_env = env process(name) to_a @@ -95,7 +95,7 @@ module ActionController end def call(env) - controller = @controller.new.call(@action, env) + @controller.new.dispatch(@action, env) end end @@ -109,6 +109,10 @@ module ActionController middleware_stack end + def self.call(env) + action(env['action_dispatch.request.path_parameters'][:action]).call(env) + end + # Return a rack endpoint for the given action. Memoize the endpoint, so # multiple calls into MyController.action will return the same object # for the same action. diff --git a/actionpack/lib/action_controller/metal/benchmarking.rb b/actionpack/lib/action_controller/metal/benchmarking.rb index d4cb1e122d..e58df69172 100644 --- a/actionpack/lib/action_controller/metal/benchmarking.rb +++ b/actionpack/lib/action_controller/metal/benchmarking.rb @@ -1,4 +1,4 @@ -require 'benchmark' +require 'active_support/core_ext/benchmark' module ActionController #:nodoc: # The benchmarking module times the performance of actions and reports to the logger. If the Active Record @@ -6,25 +6,6 @@ module ActionController #:nodoc: module Benchmarking #:nodoc: extend ActiveSupport::Concern - module ClassMethods - # Log and benchmark the workings of a single block and silence whatever logging that may have happened inside it - # (unless <tt>use_silence</tt> is set to false). - # - # The benchmark is only recorded if the current level of the logger matches the <tt>log_level</tt>, which makes it - # easy to include benchmarking statements in production software that will remain inexpensive because the benchmark - # will only be conducted if the log level is low enough. - def benchmark(title, log_level = Logger::DEBUG, use_silence = true) - if logger && logger.level == log_level - result = nil - ms = Benchmark.ms { result = use_silence ? silence { yield } : yield } - logger.add(log_level, "#{title} (#{('%.1f' % ms)}ms)") - result - else - yield - end - end - end - protected def render(*args, &block) if logger @@ -45,7 +26,7 @@ module ActionController #:nodoc: else super end - end + end private def process_action(*args) diff --git a/actionpack/lib/action_controller/metal/compatibility.rb b/actionpack/lib/action_controller/metal/compatibility.rb index 22f9ab219c..c251d79f4e 100644 --- a/actionpack/lib/action_controller/metal/compatibility.rb +++ b/actionpack/lib/action_controller/metal/compatibility.rb @@ -25,9 +25,11 @@ module ActionController # cattr_reader :protected_instance_variables cattr_accessor :protected_instance_variables - self.protected_instance_variables = %w(@assigns @performed_redirect @performed_render @variables_added @request_origin @url @parent_controller - @action_name @before_filter_chain_aborted @action_cache_path @_headers @_params - @_flash @_response) + self.protected_instance_variables = %w(@assigns @performed_redirect @performed_render + @variables_added @request_origin @url + @parent_controller @action_name + @before_filter_chain_aborted @_headers @_params + @_flash @_response) # Indicates whether or not optimise the generated named # route helper methods diff --git a/actionpack/lib/action_controller/metal/conditional_get.rb b/actionpack/lib/action_controller/metal/conditional_get.rb index 8575d30335..5156fbc1d5 100644 --- a/actionpack/lib/action_controller/metal/conditional_get.rb +++ b/actionpack/lib/action_controller/metal/conditional_get.rb @@ -3,6 +3,7 @@ module ActionController extend ActiveSupport::Concern include RackConvenience + 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. @@ -27,43 +28,9 @@ module ActionController response.etag = options[:etag] if options[:etag] response.last_modified = options[:last_modified] if options[:last_modified] + response.cache_control[:public] = true if options[:public] - if options[:public] - response.cache_control[:public] = true - end - - if request.fresh?(response) - head :not_modified - end - end - - # 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 - # significant headers: - # - # head :created, :location => person_path(@person) - # - # It can also be used to return exceptional conditions: - # - # return head(:method_not_allowed) unless request.post? - # return head(:bad_request) unless valid_request? - # render - def head(*args) - if args.length > 2 - raise ArgumentError, "too many arguments to head" - elsif args.empty? - raise ArgumentError, "too few arguments to head" - end - options = args.extract_options! - status = args.shift || options.delete(:status) || :ok - location = options.delete(:location) - - options.each do |key, value| - headers[key.to_s.dasherize.split(/-/).map { |v| v.capitalize }.join("-")] = value.to_s - end - - render :nothing => true, :status => status, :location => location + head :not_modified if request.fresh?(response) end # Sets the etag and/or last_modified on the response and checks it against @@ -113,7 +80,7 @@ module ActionController # Sets a HTTP 1.1 Cache-Control header of "no-cache" so no caching should occur by the browser or # intermediate caches (like caching proxy servers). def expires_now #:doc: - response.headers["Cache-Control"] = "no-cache" + response.cache_control.replace(:no_cache => true) end end end diff --git a/actionpack/lib/action_controller/metal/configuration.rb b/actionpack/lib/action_controller/metal/configuration.rb new file mode 100644 index 0000000000..5c829853b7 --- /dev/null +++ b/actionpack/lib/action_controller/metal/configuration.rb @@ -0,0 +1,28 @@ +module ActionController + module Configuration + extend ActiveSupport::Concern + + def config + @config ||= self.class.config + end + + def config=(config) + @config = config + end + + module ClassMethods + def default_config + @default_config ||= {} + end + + def config + self.config ||= default_config + end + + def config=(config) + @config = ActiveSupport::OrderedHash.new + @config.merge!(config) + end + end + end +end
\ No newline at end of file diff --git a/actionpack/lib/action_controller/metal/cookies.rb b/actionpack/lib/action_controller/metal/cookies.rb index d4806623c3..6855ca1478 100644 --- a/actionpack/lib/action_controller/metal/cookies.rb +++ b/actionpack/lib/action_controller/metal/cookies.rb @@ -44,24 +44,31 @@ module ActionController #:nodoc: # * <tt>:httponly</tt> - Whether this cookie is accessible via scripting or # only HTTP. Defaults to +false+. module Cookies - def self.included(base) - base.helper_method :cookies + extend ActiveSupport::Concern + + include RackConvenience + + included do + helper_method :cookies end - protected - # Returns the cookie container, which operates as described above. - def cookies - @cookies ||= CookieJar.new(self) - end + protected + # Returns the cookie container, which operates as described above. + def cookies + @cookies ||= CookieJar.build(request, response) + end end class CookieJar < Hash #:nodoc: - def initialize(controller) - @controller, @cookies = controller, controller.request.cookies - super() - update(@cookies) + def self.build(request, response) + new.tap do |hash| + hash.update(request.cookies) + hash.response = response + end end + attr_accessor :response + # Returns the value of the cookie by +name+, or +nil+ if no such cookie exists. def [](name) super(name.to_s) @@ -72,13 +79,16 @@ module ActionController #:nodoc: def []=(key, options) if options.is_a?(Hash) options.symbolize_keys! + value = options[:value] else - options = { :value => options } + value = options + options = { :value => value } end - options[:path] = "/" unless options.has_key?(:path) - super(key.to_s, options[:value]) - @controller.response.set_cookie(key, options) + super(key.to_s, value) + + options[:path] ||= "/" + response.set_cookie(key, options) end # Removes the cookie on the client machine by setting the value to an empty string @@ -86,9 +96,10 @@ module ActionController #:nodoc: # an options hash to delete cookies with extra data such as a <tt>:path</tt>. def delete(key, options = {}) options.symbolize_keys! - options[:path] = "/" unless options.has_key?(:path) - super(key.to_s) - @controller.response.delete_cookie(key, options) + options[:path] ||= "/" + value = super(key.to_s) + response.delete_cookie(key, options) + value end end end diff --git a/actionpack/lib/action_controller/metal/filter_parameter_logging.rb b/actionpack/lib/action_controller/metal/filter_parameter_logging.rb index 4259d9de19..a53c052075 100644 --- a/actionpack/lib/action_controller/metal/filter_parameter_logging.rb +++ b/actionpack/lib/action_controller/metal/filter_parameter_logging.rb @@ -4,10 +4,6 @@ module ActionController include AbstractController::Logger - included do - include InstanceMethodsForNewBase - end - module ClassMethods # Replace sensitive parameter data from the request log. # Filters parameters that have any of the arguments as a substring. @@ -17,8 +13,6 @@ module ActionController # can be replaced using String#replace or similar method. # # Examples: - # filter_parameter_logging - # => Does nothing, just slows the logging process down # # filter_parameter_logging :password # => replaces the value to all keys matching /password/i with "[FILTERED]" @@ -33,64 +27,51 @@ module ActionController # => reverses the value to all keys matching /secret/i, and # replaces the value to all keys matching /foo|bar/i with "[FILTERED]" def filter_parameter_logging(*filter_words, &block) - parameter_filter = Regexp.new(filter_words.collect{ |s| s.to_s }.join('|'), true) if filter_words.length > 0 + raise "You must filter at least one word from logging" if filter_words.empty? + + parameter_filter = Regexp.new(filter_words.join('|'), true) - define_method(:filter_parameters) do |unfiltered_parameters| - filtered_parameters = {} + define_method(:filter_parameters) do |original_params| + filtered_params = {} - unfiltered_parameters.each do |key, value| + original_params.each do |key, value| if key =~ parameter_filter - filtered_parameters[key] = '[FILTERED]' + value = '[FILTERED]' elsif value.is_a?(Hash) - filtered_parameters[key] = filter_parameters(value) + value = filter_parameters(value) elsif value.is_a?(Array) - filtered_parameters[key] = value.collect do |item| - filter_parameters(item) - end + value = value.map { |item| filter_parameters(item) } elsif block_given? key = key.dup value = value.dup if value.duplicable? yield key, value - filtered_parameters[key] = value - else - filtered_parameters[key] = value end + + filtered_params[key] = value end - filtered_parameters + filtered_params end protected :filter_parameters end end - module InstanceMethodsForNewBase - # TODO : Fix the order of information inside such that it's exactly same as the old base - def process(*) - ret = super - - if logger - parameters = respond_to?(:filter_parameters) ? filter_parameters(params) : params.dup - parameters = parameters.except!(:controller, :action, :format, :_method, :only_path) + INTERNAL_PARAMS = [:controller, :action, :format, :_method, :only_path] - unless parameters.empty? - # TODO : Move DelayedLog to AS - log = AbstractController::Logger::DelayedLog.new { " Parameters: #{parameters.inspect}" } - logger.info(log) - end - end - - ret + def process(*) + response = super + if logger + parameters = filter_parameters(params).except!(*INTERNAL_PARAMS) + logger.info { " Parameters: #{parameters.inspect}" } unless parameters.empty? end + response end - private + protected - # TODO : This method is not needed for the new base - def log_processing_for_parameters - parameters = respond_to?(:filter_parameters) ? filter_parameters(params) : params.dup - parameters = parameters.except!(:controller, :action, :format, :_method) - - logger.info " Parameters: #{parameters.inspect}" unless parameters.empty? + def filter_parameters(params) + params.dup end + end end diff --git a/actionpack/lib/action_controller/metal/flash.rb b/actionpack/lib/action_controller/metal/flash.rb index 8753253dc6..feb066a6f6 100644 --- a/actionpack/lib/action_controller/metal/flash.rb +++ b/actionpack/lib/action_controller/metal/flash.rb @@ -49,7 +49,7 @@ module ActionController #:nodoc: class FlashHash < Hash def initialize #:nodoc: super - @used = {} + @used = Set.new end def []=(k, v) #:nodoc: @@ -65,7 +65,7 @@ module ActionController #:nodoc: alias :merge! :update def replace(h) #:nodoc: - @used = {} + @used = Set.new super end @@ -104,8 +104,8 @@ module ActionController #:nodoc: # This method is called automatically by filters, so you generally don't need to care about it. def sweep #:nodoc: keys.each do |k| - unless @used[k] - use(k) + unless @used.include?(k) + @used << k else delete(k) @used.delete(k) @@ -113,47 +113,45 @@ module ActionController #:nodoc: end # clean up after keys that could have been left over by calling reject! or shift on the flash - (@used.keys - keys).each{ |k| @used.delete(k) } + (@used - keys).each{ |k| @used.delete(k) } end - def store(session, key = "flash") + def store(session) return if self.empty? - session[key] = self + session["flash"] = self end - private - # Used internally by the <tt>keep</tt> and <tt>discard</tt> methods - # use() # marks the entire flash as used - # use('msg') # marks the "msg" entry as used - # use(nil, false) # marks the entire flash as unused (keeps it around for one more action) - # use('msg', false) # marks the "msg" entry as unused (keeps it around for one more action) - # Returns the single value for the key you asked to be marked (un)used or the FlashHash itself - # if no key is passed. - def use(key = nil, used = true) - Array(key || keys).each { |k| @used[k] = used } - return key ? self[key] : self - end + private + # Used internally by the <tt>keep</tt> and <tt>discard</tt> methods + # use() # marks the entire flash as used + # use('msg') # marks the "msg" entry as used + # use(nil, false) # marks the entire flash as unused (keeps it around for one more action) + # use('msg', false) # marks the "msg" entry as unused (keeps it around for one more action) + # Returns the single value for the key you asked to be marked (un)used or the FlashHash itself + # if no key is passed. + def use(key = nil, used = true) + Array(key || keys).each { |k| used ? @used << k : @used.delete(k) } + return key ? self[key] : self + end end protected def process_action(method_name) super - if defined? @_flash - @_flash.store(session) - remove_instance_variable(:@_flash) - end + @_flash.store(session) if @_flash + @_flash = nil end def reset_session super - remove_instance_variable(:@_flash) if defined?(@_flash) + @_flash = nil end # Access the contents of the flash. Use <tt>flash["notice"]</tt> to # read a notice you put there or <tt>flash["notice"] = "hello"</tt> # to put a new one. def flash #:doc: - if !defined?(@_flash) + unless @_flash @_flash = session["flash"] || FlashHash.new @_flash.sweep end diff --git a/actionpack/lib/action_controller/metal/head.rb b/actionpack/lib/action_controller/metal/head.rb new file mode 100644 index 0000000000..68fa0a0402 --- /dev/null +++ b/actionpack/lib/action_controller/metal/head.rb @@ -0,0 +1,27 @@ +module ActionController + module Head + # 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 + # significant headers: + # + # head :created, :location => person_path(@person) + # + # It can also be used to return exceptional conditions: + # + # return head(:method_not_allowed) unless request.post? + # return head(:bad_request) unless valid_request? + # render + def head(status, options = {}) + options, status = status, nil if status.is_a?(Hash) + status ||= options.delete(:status) || :ok + location = options.delete(:location) + + options.each do |key, value| + headers[key.to_s.dasherize.split(/-/).map { |v| v.capitalize }.join("-")] = value.to_s + end + + render :nothing => true, :status => status, :location => location + end + end +end
\ No newline at end of file diff --git a/actionpack/lib/action_controller/metal/helpers.rb b/actionpack/lib/action_controller/metal/helpers.rb index 7c52779064..b4325e24ad 100644 --- a/actionpack/lib/action_controller/metal/helpers.rb +++ b/actionpack/lib/action_controller/metal/helpers.rb @@ -1,5 +1,3 @@ -require 'active_support/dependencies' - 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 @@ -54,7 +52,7 @@ module ActionController included do # Set the default directory for helpers extlib_inheritable_accessor(:helpers_dir) do - defined?(RAILS_ROOT) ? "#{RAILS_ROOT}/app/helpers" : "app/helpers" + defined?(Rails) ? "#{Rails.root}/app/helpers" : "app/helpers" end end @@ -64,46 +62,6 @@ module ActionController super end - # 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 - # - # ==== Examples - # When the argument is a string or symbol, the method will provide the "_helper" suffix, require the file - # and include the module in the template class. The second form illustrates how to include custom helpers - # when working with namespaced controllers, or other cases where the file containing the helper definition is not - # in one of Rails' standard load paths: - # helper :foo # => requires 'foo_helper' and includes FooHelper - # helper 'resources/foo' # => requires 'resources/foo_helper' and includes Resources::FooHelper - # - # When the argument is a module it will be included directly in the template class. - # helper FooHelper # => includes FooHelper - # - # When the argument is the symbol <tt>:all</tt>, the controller will include all helpers beneath - # <tt>ActionController::Base.helpers_dir</tt> (defaults to <tt>app/helpers/**/*.rb</tt> under RAILS_ROOT). - # helper :all - # - # Additionally, the +helper+ class method can receive and evaluate a block, making the methods defined available - # to the template. - # # One line - # helper { def hello() "Hello, world!" end } - # # Multi-line - # helper do - # def foo(bar) - # "#{bar} is the very best" - # end - # end - # - # Finally, all the above styles can be mixed together, and the +helper+ method can be invoked with a mix of - # +symbols+, +strings+, +modules+ and blocks. - # helper(:three, BlindHelper) { def mice() 'mice' end } - # - def helper(*args, &block) - super(*_modules_for_helpers(args), &block) - 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: @@ -123,15 +81,8 @@ module ActionController end private - # Returns a list of modules, normalized from the acceptable kinds of - # helpers with the following behavior: - # String or Symbol:: :FooBar or "FooBar" becomes "foo_bar_helper", - # and "foo_bar_helper.rb" is loaded using require_dependency. - # :all:: Loads all modules in the #helpers_dir - # Module:: No further processing - # - # After loading the appropriate files, the corresponding modules - # are returned. + # Overwrite _modules_for_helpers to accept :all as argument, which loads + # all helpers in helpers_dir. # # ==== Parameters # args<Array[String, Symbol, Module, all]>:: A list of helpers @@ -140,20 +91,8 @@ module ActionController # Array[Module]:: A normalized list of modules for the list of # helpers provided. def _modules_for_helpers(args) - args.flatten.map! do |arg| - case arg - when :all - _modules_for_helpers all_application_helpers - when String, Symbol - file_name = "#{arg.to_s.underscore}_helper" - require_dependency(file_name, "Missing helper file helpers/%s.rb") - file_name.camelize.constantize - when Module - arg - else - raise ArgumentError, "helper must be a String, Symbol, or Module" - end - end + args += all_application_helpers if args.delete(:all) + super(args) end def default_helper_module! diff --git a/actionpack/lib/action_controller/metal/layouts.rb b/actionpack/lib/action_controller/metal/layouts.rb index cac529b1ae..cc7088248a 100644 --- a/actionpack/lib/action_controller/metal/layouts.rb +++ b/actionpack/lib/action_controller/metal/layouts.rb @@ -167,26 +167,5 @@ module ActionController controller_path end end - - private - def _determine_template(options) - super - - return if (options.key?(:text) || options.key?(:inline) || options.key?(:partial)) && !options.key?(:layout) - layout = options.key?(:layout) ? options[:layout] : :default - options[:_layout] = _layout_for_option(layout, options[:_template].details) - end - - def _layout_for_option(name, details) - case name - when String then _layout_for_name(name, details) - when true then _default_layout(details, true) - when :default then _default_layout(details, false) - when false, nil then nil - else - raise ArgumentError, - "String, true, or false, expected for `layout'; you passed #{name.inspect}" - end - end end end diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index 3026067868..468c5f4fae 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -3,7 +3,8 @@ module ActionController #:nodoc: extend ActiveSupport::Concern included do - class_inheritable_reader :mimes_for_respond_to + extlib_inheritable_accessor :responder, :mimes_for_respond_to, :instance_writer => false + self.responder = ActionController::Responder clear_respond_to end @@ -46,7 +47,7 @@ module ActionController #:nodoc: # Clear all mimes in respond_to. # def clear_respond_to - write_inheritable_attribute(:mimes_for_respond_to, ActiveSupport::OrderedHash.new) + self.mimes_for_respond_to = ActiveSupport::OrderedHash.new end end @@ -221,10 +222,6 @@ module ActionController #:nodoc: end end - def responder - ActionController::Responder - end - protected # Collect mimes declared in the class method respond_to valid for the diff --git a/actionpack/lib/action_controller/metal/rack_convenience.rb b/actionpack/lib/action_controller/metal/rack_convenience.rb index a80569c530..131d20114d 100644 --- a/actionpack/lib/action_controller/metal/rack_convenience.rb +++ b/actionpack/lib/action_controller/metal/rack_convenience.rb @@ -8,7 +8,7 @@ module ActionController attr_internal :request end - def call(name, env) + def dispatch(action, env) @_request = ActionDispatch::Request.new(env) @_response = ActionDispatch::Response.new @_response.request = request diff --git a/actionpack/lib/action_controller/metal/redirector.rb b/actionpack/lib/action_controller/metal/redirector.rb index f79fd54acd..b55f5e7bfc 100644 --- a/actionpack/lib/action_controller/metal/redirector.rb +++ b/actionpack/lib/action_controller/metal/redirector.rb @@ -16,7 +16,7 @@ module ActionController logger.info("Redirected to #{url}") if logger && logger.info? self.status = status self.location = url.gsub(/[\r\n]/, '') - self.response_body = "<html><body>You are being <a href=\"#{CGI.escapeHTML(url)}\">redirected</a>.</body></html>" + self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.h(url)}\">redirected</a>.</body></html>" end end end diff --git a/actionpack/lib/action_controller/metal/rendering_controller.rb b/actionpack/lib/action_controller/metal/rendering_controller.rb index 4da32ca1b3..237299cd30 100644 --- a/actionpack/lib/action_controller/metal/rendering_controller.rb +++ b/actionpack/lib/action_controller/metal/rendering_controller.rb @@ -1,54 +1,18 @@ module ActionController - class HashKey - @hash_keys = Hash.new {|h,k| h[k] = Hash.new {|h,k| h[k] = {} } } - - def self.get(klass, formats, locale) - @hash_keys[klass][formats][locale] ||= new(klass, formats, locale) - end - - attr_accessor :hash - def initialize(klass, formats, locale) - @formats, @locale = formats, locale - @hash = [formats, locale].hash - end - - alias_method :eql?, :equal? - - def inspect - "#<HashKey -- formats: #{@formats} locale: #{@locale}>" - end - end - module RenderingController extend ActiveSupport::Concern - include AbstractController::RenderingController - - module ClassMethods - def clear_template_caches! - ActionView::Partials::PartialRenderer::TEMPLATES.clear - template_cache.clear - super - end - - def template_cache - @template_cache ||= Hash.new {|h,k| h[k] = {} } - end + included do + include AbstractController::RenderingController + include AbstractController::LocalizedCache end def process_action(*) self.formats = request.formats.map {|x| x.to_sym} - - super - end - - def _determine_template(*) super end def render(options) - Thread.current[:format_locale_key] = HashKey.get(self.class, formats, I18n.locale) - super self.content_type ||= options[:_template].mime_type.to_s response_body @@ -70,29 +34,19 @@ module ActionController controller_path end - def with_template_cache(name) - self.class.template_cache[Thread.current[:format_locale_key]][name] ||= super - end - def _determine_template(options) - if options.key?(:text) - options[:_template] = ActionView::TextTemplate.new(options[:text], formats.first) - elsif options.key?(:inline) - handler = ActionView::Template.handler_class_for_extension(options[:type] || "erb") - template = ActionView::Template.new(options[:inline], "inline #{options[:inline].inspect}", handler, {}) - options[:_template] = template - elsif options.key?(:template) - options[:_template_name] = options[:template] - elsif options.key?(:file) - options[:_template_name] = options[:file] - elsif !options.key?(:partial) - options[:_template_name] = (options[:action] || action_name).to_s + if (options.keys & [:partial, :file, :template, :text, :inline]).empty? + options[:_template_name] ||= options[:action] options[:_prefix] = _prefix end super end + def format_for_text + formats.first + end + def _process_options(options) status, content_type, location = options.values_at(:status, :content_type, :location) self.status = status if status diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index ad06657f86..113c20a758 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -5,7 +5,6 @@ module ActionController #:nodoc: module RequestForgeryProtection extend ActiveSupport::Concern - # TODO : Remove the defined? check when new base is the main base include AbstractController::Helpers, Session included do @@ -21,26 +20,26 @@ module ActionController #:nodoc: helper_method :protect_against_forgery? 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. + # 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. # # 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. + # 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. # - # 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>. Same applies to manually constructed Ajax requests. To - # make the token available through a global variable to scripts on a certain page, you could add something like this to a view: + # 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>. # - # <%= javascript_tag "window._token = '#{form_authenticity_token}'" %> - # - # 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: + # 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 @@ -57,7 +56,8 @@ module ActionController #:nodoc: # * 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" + # * Make sure the session cookies that Rails creates are non-persistent. Check in Firefox and look + # for "Expires: at end of session" # module ClassMethods # Turn on request forgery protection. Bear in mind that only non-GET, HTML/JavaScript requests are checked. @@ -76,10 +76,7 @@ module ActionController #:nodoc: # * <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, :only => options.delete(:only), :except => options.delete(:except) - if options[:secret] || options[:digest] - ActiveSupport::Deprecation.warn("protect_from_forgery only takes :only and :except options now. :digest and :secret have no effect", caller) - end + before_filter :verify_authenticity_token, options end end @@ -88,31 +85,24 @@ module ActionController #:nodoc: def verify_authenticity_token verified_request? || raise(ActionController::InvalidAuthenticityToken) 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? def verified_request? - !protect_against_forgery? || - request.method == :get || - request.xhr? || - !verifiable_request_format? || + !protect_against_forgery? || request.forgery_whitelisted? || form_authenticity_token == params[request_forgery_protection_token] end - - def verifiable_request_format? - !request.content_type.nil? && request.content_type.verify_request? - end - + # Sets the token value for the current session. def form_authenticity_token session[:_csrf_token] ||= ActiveSupport::SecureRandom.base64(32) end def protect_against_forgery? - allow_forgery_protection && request_forgery_protection_token + allow_forgery_protection end end end diff --git a/actionpack/lib/action_controller/metal/responder.rb b/actionpack/lib/action_controller/metal/responder.rb index a16ed97131..e8e88e7479 100644 --- a/actionpack/lib/action_controller/metal/responder.rb +++ b/actionpack/lib/action_controller/metal/responder.rb @@ -14,12 +14,11 @@ module ActionController #:nodoc: # # When a request comes, for example with format :xml, three steps happen: # - # 1) respond_with searches for a template at people/index.xml; + # 1) responder searches for a template at people/index.xml; # - # 2) if the template is not available, it will create a responder, passing - # the controller and the resource and invoke :to_xml on it; + # 2) if the template is not available, it will invoke :to_xml in the given resource; # - # 3) if the responder does not respond_to :to_xml, call to_format on it. + # 3) if the responder does not respond_to :to_xml, call :to_format on it. # # === Builtin HTTP verb semantics # @@ -88,14 +87,16 @@ module ActionController #:nodoc: @resource = resources.is_a?(Array) ? resources.last : resources @resources = resources @options = options + @action = options.delete(:action) @default_response = options.delete(:default_response) end delegate :head, :render, :redirect_to, :to => :controller delegate :get?, :post?, :put?, :delete?, :to => :request - # Undefine :to_json since it's defined on Object - undef_method :to_json + # Undefine :to_json and :to_yaml since it's defined on Object + undef_method(:to_json) if method_defined?(:to_json) + undef_method(:to_yaml) if method_defined?(:to_yaml) # Initializes a new responder an invoke the proper format. If the format is # not defined, call to_format. @@ -111,14 +112,8 @@ module ActionController #:nodoc: # def to_html default_render - rescue ActionView::MissingTemplate - if get? - raise - elsif has_errors? - render :action => default_action - else - redirect_to resource_location - end + rescue ActionView::MissingTemplate => e + navigation_behavior(e) end # All others formats follow the procedure below. First we try to render a @@ -127,9 +122,26 @@ module ActionController #:nodoc: # def to_format default_render - rescue ActionView::MissingTemplate + rescue ActionView::MissingTemplate => e raise unless resourceful? + api_behavior(e) + end + protected + + # This is the common behavior for "navigation" requests, like :html, :iphone and so forth. + def navigation_behavior(error) + if get? + raise error + elsif has_errors? + render :action => default_action + else + redirect_to resource_location + end + end + + # This is the common behavior for "API" requests, like :xml and :json. + def api_behavior(error) if get? display resource elsif has_errors? @@ -141,8 +153,6 @@ module ActionController #:nodoc: end end - protected - # Checks whether the resource responds to the current format or not. # def resourceful? @@ -194,7 +204,7 @@ module ActionController #:nodoc: # the verb is post. # def default_action - request.post? ? :new : :edit + @action || (request.post? ? :new : :edit) end end end diff --git a/actionpack/lib/action_controller/metal/session_management.rb b/actionpack/lib/action_controller/metal/session_management.rb index 654aa08cd3..d70f40ce7a 100644 --- a/actionpack/lib/action_controller/metal/session_management.rb +++ b/actionpack/lib/action_controller/metal/session_management.rb @@ -1,10 +1,8 @@ module ActionController #:nodoc: module SessionManagement #:nodoc: - def self.included(base) - base.class_eval do - extend ClassMethods - end - end + extend ActiveSupport::Concern + + include ActionController::Configuration module ClassMethods # Set the session store to be used for keeping the session data between requests. @@ -35,13 +33,6 @@ module ActionController #:nodoc: session_options.merge!(options) end - # Returns the hash used to configure the session. Example use: - # - # ActionController::Base.session_options[:secure] = true # session only available over HTTPS - def session_options - @session_options ||= {} - end - def session(*args) ActiveSupport::Deprecation.warn( "Disabling sessions for a single controller has been deprecated. " + diff --git a/actionpack/lib/action_controller/metal/streaming.rb b/actionpack/lib/action_controller/metal/streaming.rb index 4761763a26..43c661bef4 100644 --- a/actionpack/lib/action_controller/metal/streaming.rb +++ b/actionpack/lib/action_controller/metal/streaming.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/string/bytesize' - module ActionController #:nodoc: # Methods for sending arbitrary data and for streaming files to the browser, # instead of rendering. diff --git a/actionpack/lib/action_controller/metal/verification.rb b/actionpack/lib/action_controller/metal/verification.rb index d3d78e3749..500cced539 100644 --- a/actionpack/lib/action_controller/metal/verification.rb +++ b/actionpack/lib/action_controller/metal/verification.rb @@ -79,8 +79,8 @@ module ActionController #:nodoc: # do not apply this verification to the actions specified in the associated # array (may also be a single value). def verify(options={}) - before_filter :only => options[:only], :except => options[:except] do |c| - c.__send__ :verify_action, options + before_filter :only => options[:only], :except => options[:except] do + verify_action options end end end diff --git a/actionpack/lib/action_controller/middleware.rb b/actionpack/lib/action_controller/middleware.rb index fac0ed2645..17275793b7 100644 --- a/actionpack/lib/action_controller/middleware.rb +++ b/actionpack/lib/action_controller/middleware.rb @@ -1,34 +1,34 @@ module ActionController class Middleware < Metal class ActionMiddleware - def initialize(controller) - @controller = controller + def initialize(controller, app) + @controller, @app = controller, app end def call(env) - controller = @controller.allocate - controller.send(:initialize) - controller.app = @app - controller._call(env) + @controller.build(@app).dispatch(:index, env) end + end + + class << self + alias build new - def app=(app) - @app = app + def new(app) + ActionMiddleware.new(self, app) end end - - def self.new(app) - middleware = ActionMiddleware.new(self) - middleware.app = app - middleware + + attr_internal :app + + def process(action) + response = super + self.status, self.headers, self.response_body = response if response.is_a?(Array) + response end - - def _call(env) - @_env = env - @_request = ActionDispatch::Request.new(env) - @_response = ActionDispatch::Response.new - @_response.request = @_request - process(:index) + + def initialize(app) + super() + @_app = app end def index diff --git a/actionpack/lib/action_controller/notifications.rb b/actionpack/lib/action_controller/notifications.rb new file mode 100644 index 0000000000..1a4f29e0e2 --- /dev/null +++ b/actionpack/lib/action_controller/notifications.rb @@ -0,0 +1,10 @@ +require 'active_support/notifications' + +ActiveSupport::Notifications.subscribe(/(read|write|cache|expire|exist)_(fragment|page)\??/) do |*args| + event = ActiveSupport::Notifications::Event.new(*args) + + if logger = ActionController::Base.logger + human_name = event.name.to_s.humanize + logger.info("#{human_name} (%.1fms)" % event.duration) + end +end diff --git a/actionpack/lib/action_controller/routing/generation/polymorphic_routes.rb b/actionpack/lib/action_controller/polymorphic_routes.rb index 2adf3575a7..eaed00cfb7 100644 --- a/actionpack/lib/action_controller/routing/generation/polymorphic_routes.rb +++ b/actionpack/lib/action_controller/polymorphic_routes.rb @@ -80,9 +80,8 @@ module ActionController 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) - namespace = extract_namespace(record_or_hash_or_array) + 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 ] @@ -105,8 +104,7 @@ module ActionController end args.delete_if {|arg| arg.is_a?(Symbol) || arg.is_a?(String)} - - named_route = build_named_route_call(record_or_hash_or_array, namespace, inflection, options) + named_route = build_named_route_call(record_or_hash_or_array, inflection, options) url_options = options.except(:action, :routing_type) unless url_options.empty? @@ -138,18 +136,6 @@ module ActionController EOT end - def formatted_polymorphic_url(record_or_hash, options = {}) - ActiveSupport::Deprecation.warn("formatted_polymorphic_url has been deprecated. Please pass :format to the polymorphic_url method instead", caller) - options[:format] = record_or_hash.pop if Array === record_or_hash - polymorphic_url(record_or_hash, options) - end - - def formatted_polymorphic_path(record_or_hash, options = {}) - ActiveSupport::Deprecation.warn("formatted_polymorphic_path has been deprecated. Please pass :format to the polymorphic_path method instead", caller) - options[:format] = record_or_hash.pop if record_or_hash === Array - polymorphic_url(record_or_hash, options.merge(:routing_type => :path)) - end - private def action_prefix(options) options[:action] ? "#{options[:action]}_" : '' @@ -159,7 +145,7 @@ module ActionController options[:routing_type] || :url end - def build_named_route_call(records, namespace, inflection, options = {}) + def build_named_route_call(records, inflection, options = {}) unless records.is_a?(Array) record = extract_record(records) route = '' @@ -169,7 +155,7 @@ module ActionController if parent.is_a?(Symbol) || parent.is_a?(String) string << "#{parent}_" else - string << "#{RecordIdentifier.__send__("plural_class_name", parent)}".singularize + string << RecordIdentifier.__send__("plural_class_name", parent).singularize string << "_" end end @@ -178,12 +164,12 @@ module ActionController if record.is_a?(Symbol) || record.is_a?(String) route << "#{record}_" else - route << "#{RecordIdentifier.__send__("plural_class_name", record)}" + route << RecordIdentifier.__send__("plural_class_name", record) route = route.singularize if inflection == :singular route << "_" end - action_prefix(options) + namespace + route + routing_type(options).to_s + action_prefix(options) + route + routing_type(options).to_s end def extract_record(record_or_hash_or_array) @@ -193,18 +179,5 @@ module ActionController else record_or_hash_or_array end end - - # Remove the first symbols from the array and return the url prefix - # implied by those symbols. - def extract_namespace(record_or_hash_or_array) - return "" unless record_or_hash_or_array.is_a?(Array) - - namespace_keys = [] - while (key = record_or_hash_or_array.first) && key.is_a?(String) || key.is_a?(Symbol) - namespace_keys << record_or_hash_or_array.shift - end - - namespace_keys.map {|k| "#{k}_"}.join - end end end diff --git a/actionpack/lib/action_controller/routing/builder.rb b/actionpack/lib/action_controller/routing/builder.rb deleted file mode 100644 index 42ad12e1ea..0000000000 --- a/actionpack/lib/action_controller/routing/builder.rb +++ /dev/null @@ -1,199 +0,0 @@ -require 'active_support/core_ext/hash/except' - -module ActionController - module Routing - class RouteBuilder #:nodoc: - attr_reader :separators, :optional_separators - attr_reader :separator_regexp, :nonseparator_regexp, :interval_regexp - - def initialize - @separators = Routing::SEPARATORS - @optional_separators = %w( / ) - - @separator_regexp = /[#{Regexp.escape(separators.join)}]/ - @nonseparator_regexp = /\A([^#{Regexp.escape(separators.join)}]+)/ - @interval_regexp = /(.*?)(#{separator_regexp}|$)/ - end - - # Accepts a "route path" (a string defining a route), and returns the array - # of segments that corresponds to it. Note that the segment array is only - # partially initialized--the defaults and requirements, for instance, need - # to be set separately, via the +assign_route_options+ method, and the - # <tt>optional?</tt> method for each segment will not be reliable until after - # +assign_route_options+ is called, as well. - def segments_for_route_path(path) - rest, segments = path, [] - - until rest.empty? - segment, rest = segment_for(rest) - segments << segment - end - segments - end - - # A factory method that returns a new segment instance appropriate for the - # format of the given string. - def segment_for(string) - segment = - case string - when /\A\.(:format)?\// - OptionalFormatSegment.new - when /\A:(\w+)/ - key = $1.to_sym - key == :controller ? ControllerSegment.new(key) : DynamicSegment.new(key) - when /\A\*(\w+)/ - PathSegment.new($1.to_sym, :optional => true) - when /\A\?(.*?)\?/ - StaticSegment.new($1, :optional => true) - when nonseparator_regexp - StaticSegment.new($1) - when separator_regexp - DividerSegment.new($&, :optional => optional_separators.include?($&)) - end - [segment, $~.post_match] - end - - # Split the given hash of options into requirement and default hashes. The - # segments are passed alongside in order to distinguish between default values - # and requirements. - def divide_route_options(segments, options) - options = options.except(:path_prefix, :name_prefix) - - if options[:namespace] - options[:controller] = "#{options.delete(:namespace).sub(/\/$/, '')}/#{options[:controller]}" - end - - requirements = (options.delete(:requirements) || {}).dup - defaults = (options.delete(:defaults) || {}).dup - conditions = (options.delete(:conditions) || {}).dup - - validate_route_conditions(conditions) - - path_keys = segments.collect { |segment| segment.key if segment.respond_to?(:key) }.compact - options.each do |key, value| - hash = (path_keys.include?(key) && ! value.is_a?(Regexp)) ? defaults : requirements - hash[key] = value - end - - [defaults, requirements, conditions] - end - - # Takes a hash of defaults and a hash of requirements, and assigns them to - # the segments. Any unused requirements (which do not correspond to a segment) - # are returned as a hash. - def assign_route_options(segments, defaults, requirements) - route_requirements = {} # Requirements that do not belong to a segment - - segment_named = Proc.new do |key| - segments.detect { |segment| segment.key == key if segment.respond_to?(:key) } - end - - requirements.each do |key, requirement| - segment = segment_named[key] - if segment - raise TypeError, "#{key}: requirements on a path segment must be regular expressions" unless requirement.is_a?(Regexp) - 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 - segment.regexp = requirement - else - route_requirements[key] = requirement - end - end - - defaults.each do |key, default| - segment = segment_named[key] - raise ArgumentError, "#{key}: No matching segment exists; cannot assign default" unless segment - segment.is_optional = true - segment.default = default.to_param if default - end - - assign_default_route_options(segments) - ensure_required_segments(segments) - route_requirements - end - - # Assign default options, such as 'index' as a default for <tt>:action</tt>. This - # method must be run *after* user supplied requirements and defaults have - # been applied to the segments. - def assign_default_route_options(segments) - segments.each do |segment| - next unless segment.is_a? DynamicSegment - case segment.key - when :action - if segment.regexp.nil? || segment.regexp.match('index').to_s == 'index' - segment.default ||= 'index' - segment.is_optional = true - end - when :id - if segment.default.nil? && segment.regexp.nil? || segment.regexp =~ '' - segment.is_optional = true - end - end - end - end - - # Makes sure that there are no optional segments that precede a required - # segment. If any are found that precede a required segment, they are - # made required. - def ensure_required_segments(segments) - allow_optional = true - segments.reverse_each do |segment| - allow_optional &&= segment.optional? - if !allow_optional && segment.optional? - unless segment.optionality_implied? - warn "Route segment \"#{segment.to_s}\" cannot be optional because it precedes a required segment. This segment will be required." - end - segment.is_optional = false - elsif allow_optional && segment.respond_to?(:default) && segment.default - # if a segment has a default, then it is optional - segment.is_optional = true - end - end - end - - # Construct and return a route with the given path and options. - def build(path, options) - # Wrap the path with slashes - path = "/#{path}" unless path[0] == ?/ - path = "#{path}/" unless path[-1] == ?/ - - prefix = options[:path_prefix].to_s.gsub(/^\//,'') - path = "/#{prefix}#{path}" unless prefix.blank? - - segments = segments_for_route_path(path) - defaults, requirements, conditions = divide_route_options(segments, options) - requirements = assign_route_options(segments, defaults, requirements) - - # TODO: Segments should be frozen on initialize - segments.each { |segment| segment.freeze } - - route = Route.new(segments, requirements, conditions) - - if !route.significant_keys.include?(:controller) - raise ArgumentError, "Illegal route: the :controller must be specified!" - end - - route.freeze - end - - private - def validate_route_conditions(conditions) - if method = conditions[:method] - [method].flatten.each do |m| - 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.to_sym) - raise ArgumentError, "Invalid HTTP method specified in route conditions: #{conditions.inspect}" - end - end - end - end - end - end -end diff --git a/actionpack/lib/action_controller/routing/optimisations.rb b/actionpack/lib/action_controller/routing/optimisations.rb deleted file mode 100644 index 714cf97861..0000000000 --- a/actionpack/lib/action_controller/routing/optimisations.rb +++ /dev/null @@ -1,130 +0,0 @@ -module ActionController - module Routing - # Much of the slow performance from routes comes from the - # complexity of expiry, <tt>:requirements</tt> matching, defaults providing - # and figuring out which url pattern to use. With named routes - # we can avoid the expense of finding the right route. So if - # they've provided the right number of arguments, and have no - # <tt>:requirements</tt>, we can just build up a string and return it. - # - # To support building optimisations for other common cases, the - # generation code is separated into several classes - module Optimisation - def generate_optimisation_block(route, kind) - return "" unless route.optimise? - OPTIMISERS.inject("") do |memo, klazz| - memo << klazz.new(route, kind).source_code - memo - end - end - - class Optimiser - attr_reader :route, :kind - GLOBAL_GUARD_CONDITIONS = [ - "(!defined?(default_url_options) || default_url_options.blank?)", - "(!defined?(controller.default_url_options) || controller.default_url_options.blank?)", - "defined?(request)", - "request" - ] - - def initialize(route, kind) - @route = route - @kind = kind - end - - def guard_conditions - ["false"] - end - - def generation_code - 'nil' - end - - def source_code - if applicable? - guard_condition = (GLOBAL_GUARD_CONDITIONS + guard_conditions).join(" && ") - "return #{generation_code} if #{guard_condition}\n" - else - "\n" - end - end - - # Temporarily disabled <tt>:url</tt> optimisation pending proper solution to - # Issues around request.host etc. - def applicable? - true - end - end - - # Given a route - # - # map.person '/people/:id' - # - # If the user calls <tt>person_url(@person)</tt>, we can simply - # return a string like "/people/#{@person.to_param}" - # rather than triggering the expensive logic in +url_for+. - class PositionalArguments < Optimiser - def guard_conditions - number_of_arguments = route.required_segment_keys.size - # if they're using foo_url(:id=>2) it's one - # argument, but we don't want to generate /foos/id2 - if number_of_arguments == 1 - ["args.size == 1", "!args.first.is_a?(Hash)"] - else - ["args.size == #{number_of_arguments}"] - end - end - - def generation_code - elements = [] - idx = 0 - - if kind == :url - elements << '#{request.protocol}' - elements << '#{request.host_with_port}' - end - - elements << '#{ActionController::Base.relative_url_root if ActionController::Base.relative_url_root}' - - # The last entry in <tt>route.segments</tt> appears to *always* be a - # 'divider segment' for '/' but we have assertions to ensure that - # we don't include the trailing slashes, so skip them. - (route.segments.size == 1 ? route.segments : route.segments[0..-2]).each do |segment| - if segment.is_a?(DynamicSegment) - elements << segment.interpolation_chunk("args[#{idx}].to_param") - idx += 1 - else - elements << segment.interpolation_chunk - end - end - %("#{elements * ''}") - end - end - - # This case is mostly the same as the positional arguments case - # above, but it supports additional query parameters as the last - # argument - class PositionalArgumentsWithAdditionalParams < PositionalArguments - def guard_conditions - ["args.size == #{route.segment_keys.size + 1}"] + - UrlRewriter::RESERVED_OPTIONS.collect{ |key| "!args.last.has_key?(:#{key})" } - end - - # This case uses almost the same code as positional arguments, - # but add a question mark and args.last.to_query on the end, - # unless the last arg is empty - def generation_code - super.insert(-2, '#{\'?\' + args.last.to_query unless args.last.empty?}') - end - - # To avoid generating "http://localhost/?host=foo.example.com" we - # can't use this optimisation on routes without any segments - def applicable? - super && route.segment_keys.size > 0 - end - end - - OPTIMISERS = [PositionalArguments, PositionalArgumentsWithAdditionalParams] - end - end -end diff --git a/actionpack/lib/action_controller/routing/recognition_optimisation.rb b/actionpack/lib/action_controller/routing/recognition_optimisation.rb deleted file mode 100644 index 9bfebff0c0..0000000000 --- a/actionpack/lib/action_controller/routing/recognition_optimisation.rb +++ /dev/null @@ -1,167 +0,0 @@ -module ActionController - module Routing - # BEFORE: 0.191446860631307 ms/url - # AFTER: 0.029847304022858 ms/url - # Speed up: 6.4 times - # - # Route recognition is slow due to one-by-one iterating over - # a whole routeset (each map.resources generates at least 14 routes) - # and matching weird regexps on each step. - # - # We optimize this by skipping all URI segments that 100% sure can't - # be matched, moving deeper in a tree of routes (where node == segment) - # until first possible match is accured. In such case, we start walking - # a flat list of routes, matching them with accurate matcher. - # So, first step: search a segment tree for the first relevant index. - # Second step: iterate routes starting with that index. - # - # How tree is walked? We can do a recursive tests, but it's smarter: - # We just create a tree of if-s and elsif-s matching segments. - # - # We have segments of 3 flavors: - # 1) nil (no segment, route finished) - # 2) const-dot-dynamic (like "/posts.:xml", "/preview.:size.jpg") - # 3) const (like "/posts", "/comments") - # 4) dynamic ("/:id", "file.:size.:extension") - # - # We split incoming string into segments and iterate over them. - # When segment is nil, we drop immediately, on a current node index. - # When segment is equal to some const, we step into branch. - # If none constants matched, we step into 'dynamic' branch (it's a last). - # If we can't match anything, we drop to last index on a level. - # - # Note: we maintain the original routes order, so we finish building - # steps on a first dynamic segment. - # - # - # Example. Given the routes: - # 0 /posts/ - # 1 /posts/:id - # 2 /posts/:id/comments - # 3 /posts/blah - # 4 /users/ - # 5 /users/:id - # 6 /users/:id/profile - # - # request_uri = /users/123 - # - # There will be only 4 iterations: - # 1) segm test for /posts prefix, skip all /posts/* routes - # 2) segm test for /users/ - # 3) segm test for /users/:id - # (jump to list index = 5) - # 4) full test for /users/:id => here we are! - class RouteSet - def recognize_path(path, environment={}) - result = recognize_optimized(path, environment) and return result - - # Route was not recognized. Try to find out why (maybe wrong verb). - allows = HTTP_METHODS.select { |verb| routes.find { |r| r.recognize(path, environment.merge(:method => verb)) } } - - if environment[:method] && !HTTP_METHODS.include?(environment[:method]) - raise NotImplemented.new(*allows) - elsif !allows.empty? - raise MethodNotAllowed.new(*allows) - else - raise RoutingError, "No route matches #{path.inspect} with #{environment.inspect}" - end - end - - def segment_tree(routes) - tree = [0] - - i = -1 - routes.each do |route| - i += 1 - # not fast, but runs only once - segments = to_plain_segments(route.segments.inject("") { |str,s| str << s.to_s }) - - node = tree - segments.each do |seg| - seg = :dynamic if seg && seg[0] == ?: - node << [seg, [i]] if node.empty? || node[node.size - 1][0] != seg - node = node[node.size - 1][1] - end - end - tree - end - - def generate_code(list, padding=' ', level = 0) - # a digit - return padding + "#{list[0]}\n" if list.size == 1 && !(Array === list[0]) - - body = padding + "(seg = segments[#{level}]; \n" - - i = 0 - was_nil = false - list.each do |item| - if Array === item - i += 1 - start = (i == 1) - tag, sub = item - if tag == :dynamic - body += padding + "#{start ? 'if' : 'elsif'} true\n" - body += generate_code(sub, padding + " ", level + 1) - break - elsif tag == nil && !was_nil - was_nil = true - body += padding + "#{start ? 'if' : 'elsif'} seg.nil?\n" - body += generate_code(sub, padding + " ", level + 1) - else - body += padding + "#{start ? 'if' : 'elsif'} seg == '#{tag}'\n" - body += generate_code(sub, padding + " ", level + 1) - end - end - end - body += padding + "else\n" - body += padding + " #{list[0]}\n" - body += padding + "end)\n" - body - end - - # this must be really fast - def to_plain_segments(str) - str = str.dup - str.sub!(/^\/+/,'') - str.sub!(/\/+$/,'') - segments = str.split(/\.[^\/]+\/+|\/+|\.[^\/]+\Z/) # cut off ".format" also - segments << nil - segments - end - - private - def write_recognize_optimized! - tree = segment_tree(routes) - body = generate_code(tree) - - remove_recognize_optimized! - - instance_eval %{ - def recognize_optimized(path, env) - segments = to_plain_segments(path) - index = #{body} - return nil unless index - while index < routes.size - result = routes[index].recognize(path, env) and return result - index += 1 - end - nil - end - }, '(recognize_optimized)', 1 - end - - def clear_recognize_optimized! - remove_recognize_optimized! - write_recognize_optimized! - end - - def remove_recognize_optimized! - if respond_to?(:recognize_optimized) - class << self - remove_method :recognize_optimized - end - end - end - end - end -end diff --git a/actionpack/lib/action_controller/routing/resources.rb b/actionpack/lib/action_controller/routing/resources.rb deleted file mode 100644 index 06506435a2..0000000000 --- a/actionpack/lib/action_controller/routing/resources.rb +++ /dev/null @@ -1,685 +0,0 @@ -require 'active_support/core_ext/hash/slice' -require 'active_support/core_ext/object/try' - -module ActionController - # == Overview - # - # ActionController::Resources are a way of defining RESTful \resources. A RESTful \resource, in basic terms, - # is something that can be pointed at and it will respond with a representation of the data requested. - # In real terms this could mean a user with a browser requests an HTML page, or that a desktop application - # requests XML data. - # - # RESTful design is based on the assumption that there are four generic verbs that a user of an - # application can request from a \resource (the noun). - # - # \Resources can be requested using four basic HTTP verbs (GET, POST, PUT, DELETE), the method used - # denotes the type of action that should take place. - # - # === The Different Methods and their Usage - # - # * GET - Requests for a \resource, no saving or editing of a \resource should occur in a GET request. - # * POST - Creation of \resources. - # * PUT - Editing of attributes on a \resource. - # * DELETE - Deletion of a \resource. - # - # === Examples - # - # # A GET request on the Posts resource is asking for all Posts - # GET /posts - # - # # A GET request on a single Post resource is asking for that particular Post - # GET /posts/1 - # - # # A POST request on the Posts resource is asking for a Post to be created with the supplied details - # POST /posts # with => { :post => { :title => "My Whizzy New Post", :body => "I've got a brand new combine harvester" } } - # - # # A PUT request on a single Post resource is asking for a Post to be updated - # PUT /posts # with => { :id => 1, :post => { :title => "Changed Whizzy Title" } } - # - # # A DELETE request on a single Post resource is asking for it to be deleted - # DELETE /posts # with => { :id => 1 } - # - # By using the REST convention, users of our application can assume certain things about how the data - # is requested and how it is returned. Rails simplifies the routing part of RESTful design by - # supplying you with methods to create them in your routes.rb file. - # - # Read more about REST at http://en.wikipedia.org/wiki/Representational_State_Transfer - module Resources - 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 - - def initialize(entities, options) - @plural ||= entities - @singular ||= options[:singular] || plural.to_s.singularize - @path_segment = options.delete(:as) || @plural - - @options = options - - 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 ||= Base.resources_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 ||= 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) - @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 - - # Creates named routes for implementing verb-oriented controllers - # for a collection \resource. - # - # For example: - # - # map.resources :messages - # - # will map the following actions in the corresponding controller: - # - # class MessagesController < ActionController::Base - # # GET messages_url - # def index - # # return all messages - # end - # - # # GET new_message_url - # def new - # # return an HTML form for describing a new message - # end - # - # # POST messages_url - # def create - # # create a new message - # end - # - # # GET message_url(:id => 1) - # def show - # # find and return a specific message - # end - # - # # GET edit_message_url(:id => 1) - # def edit - # # return an HTML form for editing a specific message - # end - # - # # PUT message_url(:id => 1) - # def update - # # find and update a specific message - # end - # - # # DELETE message_url(:id => 1) - # def destroy - # # delete a specific message - # end - # end - # - # Along with the routes themselves, +resources+ generates named routes for use in - # controllers and views. <tt>map.resources :messages</tt> produces the following named routes and helpers: - # - # Named Route Helpers - # ============ ===================================================== - # messages messages_url, hash_for_messages_url, - # messages_path, hash_for_messages_path - # - # message message_url(id), hash_for_message_url(id), - # message_path(id), hash_for_message_path(id) - # - # new_message new_message_url, hash_for_new_message_url, - # new_message_path, hash_for_new_message_path - # - # edit_message edit_message_url(id), hash_for_edit_message_url(id), - # edit_message_path(id), hash_for_edit_message_path(id) - # - # You can use these helpers instead of +url_for+ or methods that take +url_for+ parameters. For example: - # - # redirect_to :controller => 'messages', :action => 'index' - # # and - # <%= link_to "edit this message", :controller => 'messages', :action => 'edit', :id => @message.id %> - # - # now become: - # - # redirect_to messages_url - # # and - # <%= link_to "edit this message", edit_message_url(@message) # calls @message.id automatically - # - # Since web browsers don't support the PUT and DELETE verbs, you will need to add a parameter '_method' to your - # form tags. The form helpers make this a little easier. For an update form with a <tt>@message</tt> object: - # - # <%= form_tag message_path(@message), :method => :put %> - # - # or - # - # <% form_for :message, @message, :url => message_path(@message), :html => {:method => :put} do |f| %> - # - # or - # - # <% form_for @message do |f| %> - # - # which takes into account whether <tt>@message</tt> is a new record or not and generates the - # path and method accordingly. - # - # The +resources+ method accepts the following options to customize the resulting routes: - # * <tt>:collection</tt> - Add named routes for other actions that operate on the collection. - # Takes a hash of <tt>#{action} => #{method}</tt>, where method is <tt>:get</tt>/<tt>:post</tt>/<tt>:put</tt>/<tt>:delete</tt>, - # an array of any of the previous, or <tt>:any</tt> if the method does not matter. - # These routes map to a URL like /messages/rss, with a route of +rss_messages_url+. - # * <tt>:member</tt> - Same as <tt>:collection</tt>, but for actions that operate on a specific member. - # * <tt>:new</tt> - Same as <tt>:collection</tt>, but for actions that operate on the new \resource action. - # * <tt>:controller</tt> - Specify the controller name for the routes. - # * <tt>:singular</tt> - Specify the singular name used in the member routes. - # * <tt>:requirements</tt> - Set custom routing parameter requirements; this is a hash of either - # regular expressions (which must match for the route to match) or extra parameters. For example: - # - # map.resource :profile, :path_prefix => ':name', :requirements => { :name => /[a-zA-Z]+/, :extra => 'value' } - # - # will only match if the first part is alphabetic, and will pass the parameter :extra to the controller. - # * <tt>:conditions</tt> - Specify custom routing recognition conditions. \Resources sets the <tt>:method</tt> value for the method-specific routes. - # * <tt>:as</tt> - Specify a different \resource name to use in the URL path. For example: - # # products_path == '/productos' - # map.resources :products, :as => 'productos' do |product| - # # product_reviews_path(product) == '/productos/1234/comentarios' - # product.resources :product_reviews, :as => 'comentarios' - # end - # - # * <tt>:has_one</tt> - Specify nested \resources, this is a shorthand for mapping singleton \resources beneath the current. - # * <tt>:has_many</tt> - Same has <tt>:has_one</tt>, but for plural \resources. - # - # You may directly specify the routing association with +has_one+ and +has_many+ like: - # - # map.resources :notes, :has_one => :author, :has_many => [:comments, :attachments] - # - # This is the same as: - # - # map.resources :notes do |notes| - # notes.resource :author - # notes.resources :comments - # notes.resources :attachments - # end - # - # * <tt>:path_names</tt> - Specify different path names for the actions. For example: - # # new_products_path == '/productos/nuevo' - # # bids_product_path(1) == '/productos/1/licitacoes' - # map.resources :products, :as => 'productos', :member => { :bids => :get }, :path_names => { :new => 'nuevo', :bids => 'licitacoes' } - # - # You can also set default action names from an environment, like this: - # config.action_controller.resources_path_names = { :new => 'nuevo', :edit => 'editar' } - # - # * <tt>:path_prefix</tt> - Set a prefix to the routes with required route variables. - # - # Weblog comments usually belong to a post, so you might use +resources+ like: - # - # map.resources :articles - # map.resources :comments, :path_prefix => '/articles/:article_id' - # - # You can nest +resources+ calls to set this automatically: - # - # map.resources :articles do |article| - # article.resources :comments - # end - # - # The comment \resources work the same, but must now include a value for <tt>:article_id</tt>. - # - # article_comments_url(@article) - # article_comment_url(@article, @comment) - # - # article_comments_url(:article_id => @article) - # article_comment_url(:article_id => @article, :id => @comment) - # - # If you don't want to load all objects from the database you might want to use the <tt>article_id</tt> directly: - # - # articles_comments_url(@comment.article_id, @comment) - # - # * <tt>:name_prefix</tt> - Define a prefix for all generated routes, usually ending in an underscore. - # Use this if you have named routes that may clash. - # - # map.resources :tags, :path_prefix => '/books/:book_id', :name_prefix => 'book_' - # map.resources :tags, :path_prefix => '/toys/:toy_id', :name_prefix => 'toy_' - # - # You may also use <tt>:name_prefix</tt> to override the generic named routes in a nested \resource: - # - # map.resources :articles do |article| - # article.resources :comments, :name_prefix => nil - # end - # - # This will yield named \resources like so: - # - # comments_url(@article) - # comment_url(@article, @comment) - # - # * <tt>:shallow</tt> - If true, paths for nested resources which reference a specific member - # (ie. those with an :id parameter) will not use the parent path prefix or name prefix. - # - # The <tt>:shallow</tt> option is inherited by any nested resource(s). - # - # For example, 'users', 'posts' and 'comments' all use shallow paths with the following nested resources: - # - # map.resources :users, :shallow => true do |user| - # user.resources :posts do |post| - # post.resources :comments - # end - # end - # # --> GET /users/1/posts (maps to the PostsController#index action as usual) - # # also adds the usual named route called "user_posts" - # # --> GET /posts/2 (maps to the PostsController#show action as if it were not nested) - # # also adds the named route called "post" - # # --> GET /posts/2/comments (maps to the CommentsController#index action) - # # also adds the named route called "post_comments" - # # --> GET /comments/2 (maps to the CommentsController#show action as if it were not nested) - # # also adds the named route called "comment" - # - # You may also use <tt>:shallow</tt> in combination with the +has_one+ and +has_many+ shorthand notations like: - # - # map.resources :users, :has_many => { :posts => :comments }, :shallow => true - # - # * <tt>:only</tt> and <tt>:except</tt> - Specify which of the seven default actions should be routed to. - # - # <tt>:only</tt> and <tt>:except</tt> may be set to <tt>:all</tt>, <tt>:none</tt>, an action name or a - # list of action names. By default, routes are generated for all seven actions. - # - # For example: - # - # map.resources :posts, :only => [:index, :show] do |post| - # post.resources :comments, :except => [:update, :destroy] - # end - # # --> GET /posts (maps to the PostsController#index action) - # # --> POST /posts (fails) - # # --> GET /posts/1 (maps to the PostsController#show action) - # # --> DELETE /posts/1 (fails) - # # --> POST /posts/1/comments (maps to the CommentsController#create action) - # # --> PUT /posts/1/comments/1 (fails) - # - # If <tt>map.resources</tt> is called with multiple resources, they all get the same options applied. - # - # Examples: - # - # map.resources :messages, :path_prefix => "/thread/:thread_id" - # # --> GET /thread/7/messages/1 - # - # map.resources :messages, :collection => { :rss => :get } - # # --> GET /messages/rss (maps to the #rss action) - # # also adds a named route called "rss_messages" - # - # map.resources :messages, :member => { :mark => :post } - # # --> POST /messages/1/mark (maps to the #mark action) - # # also adds a named route called "mark_message" - # - # map.resources :messages, :new => { :preview => :post } - # # --> POST /messages/new/preview (maps to the #preview action) - # # also adds a named route called "preview_new_message" - # - # map.resources :messages, :new => { :new => :any, :preview => :post } - # # --> POST /messages/new/preview (maps to the #preview action) - # # also adds a named route called "preview_new_message" - # # --> /messages/new can be invoked via any request method - # - # map.resources :messages, :controller => "categories", - # :path_prefix => "/category/:category_id", - # :name_prefix => "category_" - # # --> GET /categories/7/messages/1 - # # has named route "category_message" - # - # The +resources+ method sets HTTP method restrictions on the routes it generates. For example, making an - # HTTP POST on <tt>new_message_url</tt> will raise a RoutingError exception. The default route in - # <tt>config/routes.rb</tt> overrides this and allows invalid HTTP methods for \resource routes. - def resources(*entities, &block) - options = entities.extract_options! - entities.each { |entity| map_resource(entity, options.dup, &block) } - end - - # Creates named routes for implementing verb-oriented controllers for a singleton \resource. - # A singleton \resource is global to its current context. For unnested singleton \resources, - # the \resource is global to the current user visiting the application, such as a user's - # <tt>/account</tt> profile. For nested singleton \resources, the \resource is global to its parent - # \resource, such as a <tt>projects</tt> \resource that <tt>has_one :project_manager</tt>. - # The <tt>project_manager</tt> should be mapped as a singleton \resource under <tt>projects</tt>: - # - # map.resources :projects do |project| - # project.resource :project_manager - # end - # - # See +resources+ for general conventions. These are the main differences: - # * A singular name is given to <tt>map.resource</tt>. The default controller name is still taken from the plural name. - # * To specify a custom plural name, use the <tt>:plural</tt> option. There is no <tt>:singular</tt> option. - # * No default index route is created for the singleton \resource controller. - # * When nesting singleton \resources, only the singular name is used as the path prefix (example: 'account/messages/1') - # - # For example: - # - # map.resource :account - # - # maps these actions in the Accounts controller: - # - # class AccountsController < ActionController::Base - # # GET new_account_url - # def new - # # return an HTML form for describing the new account - # end - # - # # POST account_url - # def create - # # create an account - # end - # - # # GET account_url - # def show - # # find and return the account - # end - # - # # GET edit_account_url - # def edit - # # return an HTML form for editing the account - # end - # - # # PUT account_url - # def update - # # find and update the account - # end - # - # # DELETE account_url - # def destroy - # # delete the account - # end - # end - # - # Along with the routes themselves, +resource+ generates named routes for - # use in controllers and views. <tt>map.resource :account</tt> produces - # these named routes and helpers: - # - # Named Route Helpers - # ============ ============================================= - # account account_url, hash_for_account_url, - # account_path, hash_for_account_path - # - # new_account new_account_url, hash_for_new_account_url, - # new_account_path, hash_for_new_account_path - # - # edit_account edit_account_url, hash_for_edit_account_url, - # edit_account_path, hash_for_edit_account_path - 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) - - 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) - - 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 ||= Base.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) - returning({:conditions => conditions.dup}) 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 diff --git a/actionpack/lib/action_controller/routing/route.rb b/actionpack/lib/action_controller/routing/route.rb deleted file mode 100644 index eba05a3c5a..0000000000 --- a/actionpack/lib/action_controller/routing/route.rb +++ /dev/null @@ -1,267 +0,0 @@ -require 'active_support/core_ext/object/misc' - -module ActionController - module Routing - class Route #:nodoc: - attr_accessor :segments, :requirements, :conditions, :optimise - - def initialize(segments = [], requirements = {}, conditions = {}) - @segments = segments - @requirements = requirements - @conditions = conditions - - if !significant_keys.include?(:action) && !requirements[:action] - @requirements[:action] = "index" - @significant_keys << :action - end - - # Routes cannot use the current string interpolation method - # if there are user-supplied <tt>:requirements</tt> as the interpolation - # code won't raise RoutingErrors when generating - has_requirements = @segments.detect { |segment| segment.respond_to?(:regexp) && segment.regexp } - if has_requirements || @requirements.keys.to_set != Routing::ALLOWED_REQUIREMENTS_FOR_OPTIMISATION - @optimise = false - else - @optimise = true - end - end - - # Indicates whether the routes should be optimised with the string interpolation - # version of the named routes methods. - def optimise? - @optimise && ActionController::Base::optimise_named_routes - end - - def segment_keys - segments.collect do |segment| - segment.key if segment.respond_to? :key - end.compact - end - - def required_segment_keys - required_segments = segments.select {|seg| (!seg.optional? && !seg.is_a?(DividerSegment)) || seg.is_a?(PathSegment) } - required_segments.collect { |seg| seg.key if seg.respond_to?(:key)}.compact - end - - # Build a query string from the keys of the given hash. If +only_keys+ - # is given (as an array), only the keys indicated will be used to build - # the query string. The query string will correctly build array parameter - # values. - def build_query_string(hash, only_keys = nil) - elements = [] - - (only_keys || hash.keys).each do |key| - if value = hash[key] - elements << value.to_query(key) - end - end - - elements.empty? ? '' : "?#{elements.sort * '&'}" - end - - # A route's parameter shell contains parameter values that are not in the - # route's path, but should be placed in the recognized hash. - # - # For example, +{:controller => 'pages', :action => 'show'} is the shell for the route: - # - # map.connect '/page/:id', :controller => 'pages', :action => 'show', :id => /\d+/ - # - def parameter_shell - @parameter_shell ||= {}.tap do |shell| - requirements.each do |key, requirement| - shell[key] = requirement unless requirement.is_a? Regexp - end - end - end - - # Return an array containing all the keys that are used in this route. This - # includes keys that appear inside the path, and keys that have requirements - # placed upon them. - def significant_keys - @significant_keys ||= [].tap do |sk| - segments.each { |segment| sk << segment.key if segment.respond_to? :key } - sk.concat requirements.keys - sk.uniq! - end - end - - # Return a hash of key/value pairs representing the keys in the route that - # have defaults, or which are specified by non-regexp requirements. - def defaults - @defaults ||= {}.tap do |hash| - segments.each do |segment| - next unless segment.respond_to? :default - hash[segment.key] = segment.default unless segment.default.nil? - end - requirements.each do |key,req| - next if Regexp === req || req.nil? - hash[key] = req - end - end - end - - def matches_controller_and_action?(controller, action) - prepare_matching! - (@controller_requirement.nil? || @controller_requirement === controller) && - (@action_requirement.nil? || @action_requirement === action) - end - - def to_s - @to_s ||= begin - segs = segments.inject("") { |str,s| str << s.to_s } - "%-6s %-40s %s" % [(conditions[:method] || :any).to_s.upcase, segs, requirements.inspect] - end - end - - # TODO: Route should be prepared and frozen on initialize - def freeze - unless frozen? - write_generation! - write_recognition! - prepare_matching! - - parameter_shell - significant_keys - defaults - to_s - end - - super - end - - def generate(options, hash, expire_on = {}) - path, hash = generate_raw(options, hash, expire_on) - append_query_string(path, hash, extra_keys(options)) - end - - def generate_extras(options, hash, expire_on = {}) - path, hash = generate_raw(options, hash, expire_on) - [path, extra_keys(options)] - end - - private - def requirement_for(key) - return requirements[key] if requirements.key? key - segments.each do |segment| - return segment.regexp if segment.respond_to?(:key) && segment.key == key - end - nil - end - - # Write and compile a +generate+ method for this Route. - def write_generation! - # Build the main body of the generation - body = "expired = false\n#{generation_extraction}\n#{generation_structure}" - - # If we have conditions that must be tested first, nest the body inside an if - body = "if #{generation_requirements}\n#{body}\nend" if generation_requirements - args = "options, hash, expire_on = {}" - - # Nest the body inside of a def block, and then compile it. - raw_method = method_decl = "def generate_raw(#{args})\npath = begin\n#{body}\nend\n[path, hash]\nend" - instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})" - - # expire_on.keys == recall.keys; in other words, the keys in the expire_on hash - # are the same as the keys that were recalled from the previous request. Thus, - # we can use the expire_on.keys to determine which keys ought to be used to build - # the query string. (Never use keys from the recalled request when building the - # query string.) - - raw_method - end - - # Build several lines of code that extract values from the options hash. If any - # of the values are missing or rejected then a return will be executed. - def generation_extraction - segments.collect do |segment| - segment.extraction_code - end.compact * "\n" - end - - # Produce a condition expression that will check the requirements of this route - # upon generation. - def generation_requirements - requirement_conditions = requirements.collect do |key, req| - if req.is_a? Regexp - value_regexp = Regexp.new "\\A#{req.to_s}\\Z" - "hash[:#{key}] && #{value_regexp.inspect} =~ options[:#{key}]" - else - "hash[:#{key}] == #{req.inspect}" - end - end - requirement_conditions * ' && ' unless requirement_conditions.empty? - end - - def generation_structure - segments.last.string_structure segments[0..-2] - end - - # Write and compile a +recognize+ method for this Route. - def write_recognition! - # Create an if structure to extract the params from a match if it occurs. - body = "params = parameter_shell.dup\n#{recognition_extraction * "\n"}\nparams" - body = "if #{recognition_conditions.join(" && ")}\n#{body}\nend" - - # Build the method declaration and compile it - method_decl = "def recognize(path, env = {})\n#{body}\nend" - instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})" - method_decl - end - - # Plugins may override this method to add other conditions, like checks on - # host, subdomain, and so forth. Note that changes here only affect route - # recognition, not generation. - def recognition_conditions - result = ["(match = #{Regexp.new(recognition_pattern).inspect}.match(path))"] - result << "[conditions[:method]].flatten.include?(env[:method])" if conditions[:method] - result - end - - # Build the regular expression pattern that will match this route. - def recognition_pattern(wrap = true) - pattern = '' - segments.reverse_each do |segment| - pattern = segment.build_pattern pattern - end - wrap ? ("\\A" + pattern + "\\Z") : pattern - end - - # Write the code to extract the parameters from a matched route. - def recognition_extraction - next_capture = 1 - extraction = segments.collect do |segment| - x = segment.match_extraction(next_capture) - next_capture += segment.number_of_captures - x - end - extraction.compact - end - - # Generate the query string with any extra keys in the hash and append - # it to the given path, returning the new path. - def append_query_string(path, hash, query_keys = nil) - return nil unless path - query_keys ||= extra_keys(hash) - "#{path}#{build_query_string(hash, query_keys)}" - end - - # Determine which keys in the given hash are "extra". Extra keys are - # those that were not used to generate a particular route. The extra - # keys also do not include those recalled from the prior request, nor - # do they include any keys that were implied in the route (like a - # <tt>:controller</tt> that is required, but not explicitly used in the - # text of the route.) - def extra_keys(hash, recall = {}) - (hash || {}).keys.map { |k| k.to_sym } - (recall || {}).keys - significant_keys - end - - def prepare_matching! - unless defined? @matching_prepared - @controller_requirement = requirement_for(:controller) - @action_requirement = requirement_for(:action) - @matching_prepared = true - end - end - end - end -end diff --git a/actionpack/lib/action_controller/routing/routing_ext.rb b/actionpack/lib/action_controller/routing/routing_ext.rb deleted file mode 100644 index 5e5b22b6c2..0000000000 --- a/actionpack/lib/action_controller/routing/routing_ext.rb +++ /dev/null @@ -1,4 +0,0 @@ -require 'active_support/core_ext/object/conversions' -require 'active_support/core_ext/boolean/conversions' -require 'active_support/core_ext/nil/conversions' -require 'active_support/core_ext/regexp' diff --git a/actionpack/lib/action_controller/routing/segments.rb b/actionpack/lib/action_controller/routing/segments.rb deleted file mode 100644 index 2603855476..0000000000 --- a/actionpack/lib/action_controller/routing/segments.rb +++ /dev/null @@ -1,343 +0,0 @@ -module ActionController - module Routing - class Segment #:nodoc: - RESERVED_PCHAR = ':@&=+$,;%' - SAFE_PCHAR = "#{URI::REGEXP::PATTERN::UNRESERVED}#{RESERVED_PCHAR}" - if RUBY_VERSION >= '1.9' - UNSAFE_PCHAR = Regexp.new("[^#{SAFE_PCHAR}]", false).freeze - else - UNSAFE_PCHAR = Regexp.new("[^#{SAFE_PCHAR}]", false, 'N').freeze - end - - # TODO: Convert :is_optional accessor to read only - attr_accessor :is_optional - alias_method :optional?, :is_optional - - def initialize - @is_optional = false - end - - def number_of_captures - Regexp.new(regexp_chunk).number_of_captures - end - - def extraction_code - nil - end - - # Continue generating string for the prior segments. - def continue_string_structure(prior_segments) - if prior_segments.empty? - interpolation_statement(prior_segments) - else - new_priors = prior_segments[0..-2] - prior_segments.last.string_structure(new_priors) - end - end - - def interpolation_chunk - URI.escape(value, UNSAFE_PCHAR) - end - - # Return a string interpolation statement for this segment and those before it. - def interpolation_statement(prior_segments) - chunks = prior_segments.collect { |s| s.interpolation_chunk } - chunks << interpolation_chunk - "\"#{chunks * ''}\"#{all_optionals_available_condition(prior_segments)}" - end - - def string_structure(prior_segments) - optional? ? continue_string_structure(prior_segments) : interpolation_statement(prior_segments) - end - - # Return an if condition that is true if all the prior segments can be generated. - # If there are no optional segments before this one, then nil is returned. - def all_optionals_available_condition(prior_segments) - optional_locals = prior_segments.collect { |s| s.local_name if s.optional? && s.respond_to?(:local_name) }.compact - optional_locals.empty? ? nil : " if #{optional_locals * ' && '}" - end - - # Recognition - - def match_extraction(next_capture) - nil - end - - # Warning - - # Returns true if this segment is optional? because of a default. If so, then - # no warning will be emitted regarding this segment. - def optionality_implied? - false - end - end - - class StaticSegment < Segment #:nodoc: - attr_reader :value, :raw - alias_method :raw?, :raw - - def initialize(value = nil, options = {}) - super() - @value = value - @raw = options[:raw] if options.key?(:raw) - @is_optional = options[:optional] if options.key?(:optional) - end - - def interpolation_chunk - raw? ? value : super - end - - def regexp_chunk - chunk = Regexp.escape(value) - optional? ? Regexp.optionalize(chunk) : chunk - end - - def number_of_captures - 0 - end - - def build_pattern(pattern) - escaped = Regexp.escape(value) - if optional? && ! pattern.empty? - "(?:#{Regexp.optionalize escaped}\\Z|#{escaped}#{Regexp.unoptionalize pattern})" - elsif optional? - Regexp.optionalize escaped - else - escaped + pattern - end - end - - def to_s - value - end - end - - class DividerSegment < StaticSegment #:nodoc: - def initialize(value = nil, options = {}) - super(value, {:raw => true, :optional => true}.merge(options)) - end - - def optionality_implied? - true - end - end - - class DynamicSegment < Segment #:nodoc: - attr_reader :key - - # TODO: Convert these accessors to read only - attr_accessor :default, :regexp - - def initialize(key = nil, options = {}) - super() - @key = key - @default = options[:default] if options.key?(:default) - @regexp = options[:regexp] if options.key?(:regexp) - @is_optional = true if options[:optional] || options.key?(:default) - end - - def to_s - ":#{key}" - end - - # The local variable name that the value of this segment will be extracted to. - def local_name - "#{key}_value" - end - - def extract_value - "#{local_name} = hash[:#{key}] && hash[:#{key}].to_param #{"|| #{default.inspect}" if default}" - end - - def value_check - if default # Then we know it won't be nil - "#{value_regexp.inspect} =~ #{local_name}" if regexp - elsif optional? - # If we have a regexp check that the value is not given, or that it matches. - # If we have no regexp, return nil since we do not require a condition. - "#{local_name}.nil? || #{value_regexp.inspect} =~ #{local_name}" if regexp - else # Then it must be present, and if we have a regexp, it must match too. - "#{local_name} #{"&& #{value_regexp.inspect} =~ #{local_name}" if regexp}" - end - end - - def expiry_statement - "expired, hash = true, options if !expired && expire_on[:#{key}]" - end - - def extraction_code - s = extract_value - vc = value_check - s << "\nreturn [nil,nil] unless #{vc}" if vc - s << "\n#{expiry_statement}" - end - - def interpolation_chunk(value_code = local_name) - "\#{URI.escape(#{value_code}.to_s, ActionController::Routing::Segment::UNSAFE_PCHAR)}" - end - - def string_structure(prior_segments) - if optional? # We have a conditional to do... - # If we should not appear in the url, just write the code for the prior - # segments. This occurs if our value is the default value, or, if we are - # optional, if we have nil as our value. - "if #{local_name} == #{default.inspect}\n" + - continue_string_structure(prior_segments) + - "\nelse\n" + # Otherwise, write the code up to here - "#{interpolation_statement(prior_segments)}\nend" - else - interpolation_statement(prior_segments) - end - end - - def value_regexp - Regexp.new "\\A#{regexp.to_s}\\Z" if regexp - end - - def regexp_chunk - regexp ? regexp_string : default_regexp_chunk - end - - def regexp_string - regexp_has_modifiers? ? "(#{regexp.to_s})" : "(#{regexp.source})" - end - - def default_regexp_chunk - "([^#{Routing::SEPARATORS.join}]+)" - end - - def number_of_captures - regexp ? regexp.number_of_captures + 1 : 1 - end - - def build_pattern(pattern) - pattern = "#{regexp_chunk}#{pattern}" - optional? ? Regexp.optionalize(pattern) : pattern - end - - def match_extraction(next_capture) - # All non code-related keys (such as :id, :slug) are URI-unescaped as - # path parameters. - default_value = default ? default.inspect : nil - %[ - value = if (m = match[#{next_capture}]) - URI.unescape(m) - else - #{default_value} - end - params[:#{key}] = value if value - ] - end - - def optionality_implied? - [:action, :id].include? key - end - - def regexp_has_modifiers? - regexp.options & (Regexp::IGNORECASE | Regexp::EXTENDED) != 0 - end - end - - class ControllerSegment < DynamicSegment #:nodoc: - def regexp_chunk - possible_names = Routing.possible_controllers.collect { |name| Regexp.escape name } - "(?i-:(#{(regexp || Regexp.union(*possible_names)).source}))" - end - - # Don't URI.escape the controller name since it may contain slashes. - def interpolation_chunk(value_code = local_name) - "\#{#{value_code}.to_s}" - end - - # Make sure controller names like Admin/Content are correctly normalized to - # admin/content - def extract_value - "#{local_name} = (hash[:#{key}] #{"|| #{default.inspect}" if default}).downcase" - end - - def match_extraction(next_capture) - if default - "params[:#{key}] = match[#{next_capture}] ? match[#{next_capture}].downcase : '#{default}'" - else - "params[:#{key}] = match[#{next_capture}].downcase if match[#{next_capture}]" - end - end - end - - class PathSegment < DynamicSegment #:nodoc: - def interpolation_chunk(value_code = local_name) - "\#{#{value_code}}" - end - - def extract_value - "#{local_name} = hash[:#{key}] && Array(hash[:#{key}]).collect { |path_component| URI.escape(path_component.to_param, ActionController::Routing::Segment::UNSAFE_PCHAR) }.to_param #{"|| #{default.inspect}" if default}" - end - - def default - '' - end - - def default=(path) - raise RoutingError, "paths cannot have non-empty default values" unless path.blank? - end - - def match_extraction(next_capture) - "params[:#{key}] = PathSegment::Result.new_escaped((match[#{next_capture}]#{" || " + default.inspect if default}).split('/'))#{" if match[" + next_capture + "]" if !default}" - end - - def default_regexp_chunk - "(.*)" - end - - def number_of_captures - regexp ? regexp.number_of_captures : 1 - end - - def optionality_implied? - true - end - - class Result < ::Array #:nodoc: - def to_s() join '/' end - def self.new_escaped(strings) - new strings.collect {|str| URI.unescape str} - end - end - end - - # The OptionalFormatSegment allows for any resource route to have an optional - # :format, which decreases the amount of routes created by 50%. - class OptionalFormatSegment < DynamicSegment - - def initialize(key = nil, options = {}) - super(:format, {:optional => true}.merge(options)) - end - - def interpolation_chunk - "." + super - end - - def regexp_chunk - '/|(\.[^/?\.]+)?' - end - - def to_s - '(.:format)?' - end - - def extract_value - "#{local_name} = options[:#{key}] && options[:#{key}].to_s.downcase" - end - - #the value should not include the period (.) - def match_extraction(next_capture) - %[ - if (m = match[#{next_capture}]) - params[:#{key}] = URI.unescape(m.from(1)) - end - ] - end - end - - end -end diff --git a/actionpack/lib/action_controller/testing/process.rb b/actionpack/lib/action_controller/testing/process.rb index bbc7f3c8f9..323cce6a2f 100644 --- a/actionpack/lib/action_controller/testing/process.rb +++ b/actionpack/lib/action_controller/testing/process.rb @@ -35,7 +35,7 @@ module ActionController #:nodoc: end def cookies - @response.cookies + @request.cookies.merge(@response.cookies) end def redirect_to_url diff --git a/actionpack/lib/action_controller/testing/test_case.rb b/actionpack/lib/action_controller/testing/test_case.rb index 178e3477a6..01a55fe930 100644 --- a/actionpack/lib/action_controller/testing/test_case.rb +++ b/actionpack/lib/action_controller/testing/test_case.rb @@ -10,6 +10,13 @@ module ActionController self.session_options = TestSession::DEFAULT_OPTIONS.merge(:id => ActiveSupport::SecureRandom.hex(16)) end + class Result < ::Array #:nodoc: + def to_s() join '/' end + def self.new_escaped(strings) + new strings.collect {|str| URI.unescape str} + end + end + def assign_parameters(controller_path, action, parameters = {}) parameters = parameters.symbolize_keys.merge(:controller => controller_path, :action => action) extra_keys = ActionController::Routing::Routes.extra_keys(parameters) @@ -18,7 +25,7 @@ module ActionController if value.is_a? Fixnum value = value.to_s elsif value.is_a? Array - value = ActionController::Routing::PathSegment::Result.new(value) + value = Result.new(value) end if extra_keys.include?(key.to_sym) diff --git a/actionpack/lib/action_controller/translation.rb b/actionpack/lib/action_controller/translation.rb index 9bb63cdb15..65e9eddb0a 100644 --- a/actionpack/lib/action_controller/translation.rb +++ b/actionpack/lib/action_controller/translation.rb @@ -1,12 +1,12 @@ module ActionController module Translation def translate(*args) - I18n.translate *args + I18n.translate(*args) end alias :t :translate def localize(*args) - I18n.localize *args + I18n.localize(*args) end alias :l :localize end diff --git a/actionpack/lib/action_controller/routing/generation/url_rewriter.rb b/actionpack/lib/action_controller/url_rewriter.rb index 52b66c9303..52b66c9303 100644 --- a/actionpack/lib/action_controller/routing/generation/url_rewriter.rb +++ b/actionpack/lib/action_controller/url_rewriter.rb diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb index 11cd812695..259814a322 100644 --- a/actionpack/lib/action_dispatch.rb +++ b/actionpack/lib/action_dispatch.rb @@ -41,6 +41,8 @@ module ActionDispatch autoload :Static, 'action_dispatch/middleware/static' autoload :StringCoercion, 'action_dispatch/middleware/string_coercion' + autoload :Routing, 'action_dispatch/routing' + autoload :Assertions, 'action_dispatch/testing/assertions' autoload :Integration, 'action_dispatch/testing/integration' autoload :IntegrationTest, 'action_dispatch/testing/integration' diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb index e85823d8db..c30897b32a 100644 --- a/actionpack/lib/action_dispatch/http/mime_type.rb +++ b/actionpack/lib/action_dispatch/http/mime_type.rb @@ -24,6 +24,7 @@ module Mime LOOKUP = Hash.new { |h, k| h[k] = Type.new(k) unless k.blank? } def self.[](type) + return type if type.is_a?(Type) Type.lookup_by_extension(type.to_s) end diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index bff030f0e4..6a52854961 100755 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -5,7 +5,7 @@ require 'strscan' require 'active_support/memoizable' require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/hash/indifferent_access' -require 'active_support/core_ext/object/tap' +require 'active_support/core_ext/string/access' module ActionDispatch class Request < Rack::Request @@ -97,6 +97,10 @@ module ActionDispatch end end + def forgery_whitelisted? + method == :get || xhr? || content_type.nil? || !content_type.verify_request? + end + def media_type content_type.to_s end @@ -136,19 +140,16 @@ module ActionDispatch # If-Modified-Since and If-None-Match conditions. If both headers are # supplied, both must match, or the request is not considered fresh. def fresh?(response) - case - when if_modified_since && if_none_match - not_modified?(response.last_modified) && etag_matches?(response.etag) - when if_modified_since - not_modified?(response.last_modified) - when if_none_match - etag_matches?(response.etag) - else - false - end - end + last_modified = if_modified_since + etag = if_none_match - ONLY_ALL = [Mime::ALL].freeze + return false unless last_modified || etag + + success = true + success &&= not_modified?(response.last_modified) if last_modified + success &&= etag_matches?(response.etag) if etag + success + end # Returns the Mime type for the \format used in the request. # @@ -165,7 +166,7 @@ module ActionDispatch @env["action_dispatch.request.formats"] ||= if parameters[:format] - [Mime[parameters[:format]]] + Array.wrap(Mime[parameters[:format]]) elsif xhr? || (accept && !accept.include?(?,)) accepts else @@ -204,10 +205,6 @@ module ActionDispatch end end - def cache_format - parameters[:format] - end - # Returns true if the request's "X-Requested-With" header contains # "XMLHttpRequest". (The Prototype Javascript library sends this header with # every Ajax request.) @@ -492,7 +489,7 @@ EOM def self.extended(object) object.class_eval do attr_accessor :original_path, :content_type - alias_method :local_path, :path + alias_method :local_path, :path if method_defined?(:path) end end diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index 3e3b473178..b3ed7c9d1a 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -270,6 +270,8 @@ module ActionDispatch # :nodoc: if control.empty? headers["Cache-Control"] = DEFAULT_CACHE_CONTROL + elsif @cache_control[:no_cache] + headers["Cache-Control"] = "no-cache" else extras = control[:extras] max_age = control[:max_age] diff --git a/actionpack/lib/action_dispatch/middleware/callbacks.rb b/actionpack/lib/action_dispatch/middleware/callbacks.rb index 56d6da1706..49bc20f11f 100644 --- a/actionpack/lib/action_dispatch/middleware/callbacks.rb +++ b/actionpack/lib/action_dispatch/middleware/callbacks.rb @@ -1,6 +1,6 @@ module ActionDispatch class Callbacks - include ActiveSupport::NewCallbacks + include ActiveSupport::Callbacks define_callbacks :call, :terminator => "result == false", :rescuable => true define_callbacks :prepare, :scope => :name @@ -37,12 +37,12 @@ module ActionDispatch def initialize(app, prepare_each_request = false) @app, @prepare_each_request = app, prepare_each_request - _run_prepare_callbacks + run_callbacks(:prepare) end def call(env) - _run_call_callbacks do - _run_prepare_callbacks if @prepare_each_request + run_callbacks(:call) do + run_callbacks(:prepare) if @prepare_each_request @app.call(env) end end diff --git a/actionpack/lib/action_dispatch/middleware/stack.rb b/actionpack/lib/action_dispatch/middleware/stack.rb index 4f71ea6165..3b27309f58 100644 --- a/actionpack/lib/action_dispatch/middleware/stack.rb +++ b/actionpack/lib/action_dispatch/middleware/stack.rb @@ -1,3 +1,5 @@ +require "active_support/inflector/methods" + module ActionDispatch class MiddlewareStack < Array class Middleware @@ -32,7 +34,7 @@ module ActionDispatch elsif @klass.respond_to?(:call) @klass.call else - @klass.to_s.constantize + ActiveSupport::Inflector.constantize(@klass.to_s) end end @@ -53,7 +55,7 @@ module ActionDispatch when Class klass == middleware else - klass == middleware.to_s.constantize + klass == ActiveSupport::Inflector.constantize(middleware.to_s) end end diff --git a/actionpack/lib/action_controller/routing.rb b/actionpack/lib/action_dispatch/routing.rb index 5b9ded83dd..68ed1e3340 100644 --- a/actionpack/lib/action_controller/routing.rb +++ b/actionpack/lib/action_dispatch/routing.rb @@ -1,18 +1,7 @@ -require 'cgi' -require 'uri' -require 'set' +require 'active_support/core_ext/object/to_param' +require 'active_support/core_ext/regexp' -require 'active_support/core_ext/module/aliasing' -require 'active_support/core_ext/module/attribute_accessors' -require 'action_controller/routing/optimisations' -require 'action_controller/routing/routing_ext' -require 'action_controller/routing/route' -require 'action_controller/routing/segments' -require 'action_controller/routing/builder' -require 'action_controller/routing/route_set' -require 'action_controller/routing/recognition_optimisation' - -module ActionController +module ActionDispatch # == Routing # # The routing module provides URL rewriting in native Ruby. It's a way to @@ -197,7 +186,7 @@ module ActionController # # map.connect '*path' , :controller => 'blog' , :action => 'unrecognized?' # - # will glob all remaining parts of the route that were not recognized earlier. + # will glob all remaining parts of the route that were not recognized earlier. # The globbed values are in <tt>params[:path]</tt> as an array of path segments. # # == Route conditions @@ -269,6 +258,11 @@ module ActionController # Run <tt>rake routes</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' + SEPARATORS = %w( / . ? ) HTTP_METHODS = [:get, :head, :post, :put, :delete, :options] @@ -281,7 +275,7 @@ module ActionController # A helper module to hold URL related helpers. module Helpers - include PolymorphicRoutes + include ActionController::PolymorphicRoutes end class << self @@ -373,13 +367,11 @@ module ActionController end end - Routes = RouteSet.new - ActiveSupport::Inflector.module_eval do # Ensures that routes are reloaded when Rails inflections are updated. def inflections_with_route_reloading(&block) returning(inflections_without_route_reloading(&block)) { - ActionController::Routing::Routes.reload! if block_given? + ActionDispatch::Routing::Routes.reload! if block_given? } end diff --git a/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb b/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb new file mode 100644 index 0000000000..0564ba9797 --- /dev/null +++ b/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb @@ -0,0 +1,878 @@ +module ActionDispatch + module Routing + # Mapper instances are used to build routes. The object passed to the draw + # block in config/routes.rb is a Mapper instance. + # + # Mapper instances have relatively few instance methods, in order to avoid + # clashes with named routes. + # + # == Overview + # + # ActionController::Resources are a way of defining RESTful \resources. A RESTful \resource, in basic terms, + # is something that can be pointed at and it will respond with a representation of the data requested. + # In real terms this could mean a user with a browser requests an HTML page, or that a desktop application + # requests XML data. + # + # RESTful design is based on the assumption that there are four generic verbs that a user of an + # application can request from a \resource (the noun). + # + # \Resources can be requested using four basic HTTP verbs (GET, POST, PUT, DELETE), the method used + # denotes the type of action that should take place. + # + # === The Different Methods and their Usage + # + # * GET - Requests for a \resource, no saving or editing of a \resource should occur in a GET request. + # * POST - Creation of \resources. + # * PUT - Editing of attributes on a \resource. + # * DELETE - Deletion of a \resource. + # + # === Examples + # + # # A GET request on the Posts resource is asking for all Posts + # GET /posts + # + # # A GET request on a single Post resource is asking for that particular Post + # GET /posts/1 + # + # # A POST request on the Posts resource is asking for a Post to be created with the supplied details + # POST /posts # with => { :post => { :title => "My Whizzy New Post", :body => "I've got a brand new combine harvester" } } + # + # # A PUT request on a single Post resource is asking for a Post to be updated + # PUT /posts # with => { :id => 1, :post => { :title => "Changed Whizzy Title" } } + # + # # A DELETE request on a single Post resource is asking for it to be deleted + # DELETE /posts # with => { :id => 1 } + # + # By using the REST convention, users of our application can assume certain things about how the data + # is requested and how it is returned. Rails simplifies the routing part of RESTful design by + # supplying you with methods to create them in your routes.rb file. + # + # Read more about REST at http://en.wikipedia.org/wiki/Representational_State_Transfer + class DeprecatedMapper #:doc: + def initialize(set) #:nodoc: + @set = set + end + + # Create an unnamed route with the provided +path+ and +options+. See + # ActionDispatch::Routing for an introduction to routes. + 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 + + possible_names = Routing.possible_controllers.collect { |n| Regexp.escape(n) } + requirements[:controller] ||= Regexp.union(*possible_names) + + 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) + 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 + + # Enables the use of resources in a module by setting the name_prefix, path_prefix, and namespace for the model. + # Example: + # + # map.namespace(:admin) do |admin| + # admin.resources :products, + # :has_many => [ :tags, :images, :variants ] + # end + # + # This will create +admin_products_url+ pointing to "admin/products", which will look for an Admin::ProductsController. + # It'll also create +admin_product_tags_url+ pointing to "admin/products/#{product_id}/tags", which will look for + # Admin::TagsController. + 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 + + def initialize(entities, options) + @plural ||= entities + @singular ||= options[:singular] || plural.to_s.singularize + @path_segment = options.delete(:as) || @plural + + @options = options + + 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 ||= ActionController::Base.resources_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) + @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 + + # Creates named routes for implementing verb-oriented controllers + # for a collection \resource. + # + # For example: + # + # map.resources :messages + # + # will map the following actions in the corresponding controller: + # + # class MessagesController < ActionController::Base + # # GET messages_url + # def index + # # return all messages + # end + # + # # GET new_message_url + # def new + # # return an HTML form for describing a new message + # end + # + # # POST messages_url + # def create + # # create a new message + # end + # + # # GET message_url(:id => 1) + # def show + # # find and return a specific message + # end + # + # # GET edit_message_url(:id => 1) + # def edit + # # return an HTML form for editing a specific message + # end + # + # # PUT message_url(:id => 1) + # def update + # # find and update a specific message + # end + # + # # DELETE message_url(:id => 1) + # def destroy + # # delete a specific message + # end + # end + # + # Along with the routes themselves, +resources+ generates named routes for use in + # controllers and views. <tt>map.resources :messages</tt> produces the following named routes and helpers: + # + # Named Route Helpers + # ============ ===================================================== + # messages messages_url, hash_for_messages_url, + # messages_path, hash_for_messages_path + # + # message message_url(id), hash_for_message_url(id), + # message_path(id), hash_for_message_path(id) + # + # new_message new_message_url, hash_for_new_message_url, + # new_message_path, hash_for_new_message_path + # + # edit_message edit_message_url(id), hash_for_edit_message_url(id), + # edit_message_path(id), hash_for_edit_message_path(id) + # + # You can use these helpers instead of +url_for+ or methods that take +url_for+ parameters. For example: + # + # redirect_to :controller => 'messages', :action => 'index' + # # and + # <%= link_to "edit this message", :controller => 'messages', :action => 'edit', :id => @message.id %> + # + # now become: + # + # redirect_to messages_url + # # and + # <%= link_to "edit this message", edit_message_url(@message) # calls @message.id automatically + # + # Since web browsers don't support the PUT and DELETE verbs, you will need to add a parameter '_method' to your + # form tags. The form helpers make this a little easier. For an update form with a <tt>@message</tt> object: + # + # <%= form_tag message_path(@message), :method => :put %> + # + # or + # + # <% form_for :message, @message, :url => message_path(@message), :html => {:method => :put} do |f| %> + # + # or + # + # <% form_for @message do |f| %> + # + # which takes into account whether <tt>@message</tt> is a new record or not and generates the + # path and method accordingly. + # + # The +resources+ method accepts the following options to customize the resulting routes: + # * <tt>:collection</tt> - Add named routes for other actions that operate on the collection. + # Takes a hash of <tt>#{action} => #{method}</tt>, where method is <tt>:get</tt>/<tt>:post</tt>/<tt>:put</tt>/<tt>:delete</tt>, + # an array of any of the previous, or <tt>:any</tt> if the method does not matter. + # These routes map to a URL like /messages/rss, with a route of +rss_messages_url+. + # * <tt>:member</tt> - Same as <tt>:collection</tt>, but for actions that operate on a specific member. + # * <tt>:new</tt> - Same as <tt>:collection</tt>, but for actions that operate on the new \resource action. + # * <tt>:controller</tt> - Specify the controller name for the routes. + # * <tt>:singular</tt> - Specify the singular name used in the member routes. + # * <tt>:requirements</tt> - Set custom routing parameter requirements; this is a hash of either + # regular expressions (which must match for the route to match) or extra parameters. For example: + # + # map.resource :profile, :path_prefix => ':name', :requirements => { :name => /[a-zA-Z]+/, :extra => 'value' } + # + # will only match if the first part is alphabetic, and will pass the parameter :extra to the controller. + # * <tt>:conditions</tt> - Specify custom routing recognition conditions. \Resources sets the <tt>:method</tt> value for the method-specific routes. + # * <tt>:as</tt> - Specify a different \resource name to use in the URL path. For example: + # # products_path == '/productos' + # map.resources :products, :as => 'productos' do |product| + # # product_reviews_path(product) == '/productos/1234/comentarios' + # product.resources :product_reviews, :as => 'comentarios' + # end + # + # * <tt>:has_one</tt> - Specify nested \resources, this is a shorthand for mapping singleton \resources beneath the current. + # * <tt>:has_many</tt> - Same has <tt>:has_one</tt>, but for plural \resources. + # + # You may directly specify the routing association with +has_one+ and +has_many+ like: + # + # map.resources :notes, :has_one => :author, :has_many => [:comments, :attachments] + # + # This is the same as: + # + # map.resources :notes do |notes| + # notes.resource :author + # notes.resources :comments + # notes.resources :attachments + # end + # + # * <tt>:path_names</tt> - Specify different path names for the actions. For example: + # # new_products_path == '/productos/nuevo' + # # bids_product_path(1) == '/productos/1/licitacoes' + # map.resources :products, :as => 'productos', :member => { :bids => :get }, :path_names => { :new => 'nuevo', :bids => 'licitacoes' } + # + # You can also set default action names from an environment, like this: + # config.action_controller.resources_path_names = { :new => 'nuevo', :edit => 'editar' } + # + # * <tt>:path_prefix</tt> - Set a prefix to the routes with required route variables. + # + # Weblog comments usually belong to a post, so you might use +resources+ like: + # + # map.resources :articles + # map.resources :comments, :path_prefix => '/articles/:article_id' + # + # You can nest +resources+ calls to set this automatically: + # + # map.resources :articles do |article| + # article.resources :comments + # end + # + # The comment \resources work the same, but must now include a value for <tt>:article_id</tt>. + # + # article_comments_url(@article) + # article_comment_url(@article, @comment) + # + # article_comments_url(:article_id => @article) + # article_comment_url(:article_id => @article, :id => @comment) + # + # If you don't want to load all objects from the database you might want to use the <tt>article_id</tt> directly: + # + # articles_comments_url(@comment.article_id, @comment) + # + # * <tt>:name_prefix</tt> - Define a prefix for all generated routes, usually ending in an underscore. + # Use this if you have named routes that may clash. + # + # map.resources :tags, :path_prefix => '/books/:book_id', :name_prefix => 'book_' + # map.resources :tags, :path_prefix => '/toys/:toy_id', :name_prefix => 'toy_' + # + # You may also use <tt>:name_prefix</tt> to override the generic named routes in a nested \resource: + # + # map.resources :articles do |article| + # article.resources :comments, :name_prefix => nil + # end + # + # This will yield named \resources like so: + # + # comments_url(@article) + # comment_url(@article, @comment) + # + # * <tt>:shallow</tt> - If true, paths for nested resources which reference a specific member + # (ie. those with an :id parameter) will not use the parent path prefix or name prefix. + # + # The <tt>:shallow</tt> option is inherited by any nested resource(s). + # + # For example, 'users', 'posts' and 'comments' all use shallow paths with the following nested resources: + # + # map.resources :users, :shallow => true do |user| + # user.resources :posts do |post| + # post.resources :comments + # end + # end + # # --> GET /users/1/posts (maps to the PostsController#index action as usual) + # # also adds the usual named route called "user_posts" + # # --> GET /posts/2 (maps to the PostsController#show action as if it were not nested) + # # also adds the named route called "post" + # # --> GET /posts/2/comments (maps to the CommentsController#index action) + # # also adds the named route called "post_comments" + # # --> GET /comments/2 (maps to the CommentsController#show action as if it were not nested) + # # also adds the named route called "comment" + # + # You may also use <tt>:shallow</tt> in combination with the +has_one+ and +has_many+ shorthand notations like: + # + # map.resources :users, :has_many => { :posts => :comments }, :shallow => true + # + # * <tt>:only</tt> and <tt>:except</tt> - Specify which of the seven default actions should be routed to. + # + # <tt>:only</tt> and <tt>:except</tt> may be set to <tt>:all</tt>, <tt>:none</tt>, an action name or a + # list of action names. By default, routes are generated for all seven actions. + # + # For example: + # + # map.resources :posts, :only => [:index, :show] do |post| + # post.resources :comments, :except => [:update, :destroy] + # end + # # --> GET /posts (maps to the PostsController#index action) + # # --> POST /posts (fails) + # # --> GET /posts/1 (maps to the PostsController#show action) + # # --> DELETE /posts/1 (fails) + # # --> POST /posts/1/comments (maps to the CommentsController#create action) + # # --> PUT /posts/1/comments/1 (fails) + # + # If <tt>map.resources</tt> is called with multiple resources, they all get the same options applied. + # + # Examples: + # + # map.resources :messages, :path_prefix => "/thread/:thread_id" + # # --> GET /thread/7/messages/1 + # + # map.resources :messages, :collection => { :rss => :get } + # # --> GET /messages/rss (maps to the #rss action) + # # also adds a named route called "rss_messages" + # + # map.resources :messages, :member => { :mark => :post } + # # --> POST /messages/1/mark (maps to the #mark action) + # # also adds a named route called "mark_message" + # + # map.resources :messages, :new => { :preview => :post } + # # --> POST /messages/new/preview (maps to the #preview action) + # # also adds a named route called "preview_new_message" + # + # map.resources :messages, :new => { :new => :any, :preview => :post } + # # --> POST /messages/new/preview (maps to the #preview action) + # # also adds a named route called "preview_new_message" + # # --> /messages/new can be invoked via any request method + # + # map.resources :messages, :controller => "categories", + # :path_prefix => "/category/:category_id", + # :name_prefix => "category_" + # # --> GET /categories/7/messages/1 + # # has named route "category_message" + # + # The +resources+ method sets HTTP method restrictions on the routes it generates. For example, making an + # HTTP POST on <tt>new_message_url</tt> will raise a RoutingError exception. The default route in + # <tt>config/routes.rb</tt> overrides this and allows invalid HTTP methods for \resource routes. + def resources(*entities, &block) + options = entities.extract_options! + entities.each { |entity| map_resource(entity, options.dup, &block) } + end + + # Creates named routes for implementing verb-oriented controllers for a singleton \resource. + # A singleton \resource is global to its current context. For unnested singleton \resources, + # the \resource is global to the current user visiting the application, such as a user's + # <tt>/account</tt> profile. For nested singleton \resources, the \resource is global to its parent + # \resource, such as a <tt>projects</tt> \resource that <tt>has_one :project_manager</tt>. + # The <tt>project_manager</tt> should be mapped as a singleton \resource under <tt>projects</tt>: + # + # map.resources :projects do |project| + # project.resource :project_manager + # end + # + # See +resources+ for general conventions. These are the main differences: + # * A singular name is given to <tt>map.resource</tt>. The default controller name is still taken from the plural name. + # * To specify a custom plural name, use the <tt>:plural</tt> option. There is no <tt>:singular</tt> option. + # * No default index route is created for the singleton \resource controller. + # * When nesting singleton \resources, only the singular name is used as the path prefix (example: 'account/messages/1') + # + # For example: + # + # map.resource :account + # + # maps these actions in the Accounts controller: + # + # class AccountsController < ActionController::Base + # # GET new_account_url + # def new + # # return an HTML form for describing the new account + # end + # + # # POST account_url + # def create + # # create an account + # end + # + # # GET account_url + # def show + # # find and return the account + # end + # + # # GET edit_account_url + # def edit + # # return an HTML form for editing the account + # end + # + # # PUT account_url + # def update + # # find and update the account + # end + # + # # DELETE account_url + # def destroy + # # delete the account + # end + # end + # + # Along with the routes themselves, +resource+ generates named routes for + # use in controllers and views. <tt>map.resource :account</tt> produces + # these named routes and helpers: + # + # Named Route Helpers + # ============ ============================================= + # account account_url, hash_for_account_url, + # account_path, hash_for_account_path + # + # new_account new_account_url, hash_for_new_account_url, + # new_account_path, hash_for_new_account_path + # + # edit_account edit_account_url, hash_for_edit_account_url, + # edit_account_path, hash_for_edit_account_path + 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) + + 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) + + 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 ||= ActionController::Base.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) + returning({:conditions => conditions.dup}) 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 new file mode 100644 index 0000000000..7d770dedd0 --- /dev/null +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -0,0 +1,327 @@ +module ActionDispatch + module Routing + class Mapper + module Resources + def resource(*resources, &block) + options = resources.last.is_a?(Hash) ? resources.pop : {} + + if resources.length > 1 + raise ArgumentError if block_given? + resources.each { |r| resource(r, options) } + return self + end + + resource = resources.pop + + if @scope[:scope_level] == :resources + member do + resource(resource, options, &block) + end + return self + end + + singular = resource.to_s + plural = singular.pluralize + + controller(plural) do + namespace(resource) do + with_scope_level(:resource) do + yield if block_given? + + get "", :to => :show, :as => "#{singular}" + post "", :to => :create + put "", :to => :update + delete "", :to => :destroy + get "new", :to => :new, :as => "new_#{singular}" + get "edit", :to => :edit, :as => "edit_#{singular}" + end + end + end + + self + end + + def resources(*resources, &block) + options = resources.last.is_a?(Hash) ? resources.pop : {} + + if resources.length > 1 + raise ArgumentError if block_given? + resources.each { |r| resources(r, options) } + return self + end + + resource = resources.pop + + if @scope[:scope_level] == :resources + member do + resources(resource, options, &block) + end + return self + end + + plural = resource.to_s + singular = plural.singularize + + controller(resource) do + namespace(resource) do + with_scope_level(:resources) do + yield if block_given? + + member do + get "", :to => :show, :as => "#{singular}" + put "", :to => :update + delete "", :to => :destroy + get "edit", :to => :edit, :as => "edit_#{singular}" + end + + collection do + get "", :to => :index, :as => "#{plural}" + post "", :to => :create + get "new", :to => :new, :as => "new_#{singular}" + end + end + end + end + + self + end + + def collection + unless @scope[:scope_level] == :resources + raise ArgumentError, "can't use collection outside resources scope" + end + + with_scope_level(:collection) do + yield + end + end + + def member + unless @scope[:scope_level] == :resources + raise ArgumentError, "can't use member outside resources scope" + end + + with_scope_level(:member) do + scope(":id") do + yield + end + end + end + + def match(*args) + options = args.last.is_a?(Hash) ? args.pop : {} + args.push(options) + + case options.delete(:on) + when :collection + return collection { match(*args) } + when :member + return member { match(*args) } + end + + if @scope[:scope_level] == :resources + raise ArgumentError, "can't define route directly in resources scope" + end + + super + end + + private + def with_scope_level(kind) + old, @scope[:scope_level] = @scope[:scope_level], kind + yield + ensure + @scope[:scope_level] = old + end + end + + module Scoping + def scope(*args) + options = args.last.is_a?(Hash) ? args.pop : {} + + constraints = options.delete(:constraints) || {} + unless constraints.is_a?(Hash) + block, constraints = constraints, {} + end + constraints, @scope[:constraints] = @scope[:constraints], (@scope[:constraints] || {}).merge(constraints) + blocks, @scope[:blocks] = @scope[:blocks], (@scope[:blocks] || []) + [block] + + options, @scope[:options] = @scope[:options], (@scope[:options] || {}).merge(options) + + path_set = controller_set = false + + case args.first + when String + path_set = true + path = args.first + path, @scope[:path] = @scope[:path], "#{@scope[:path]}#{Rack::Mount::Utils.normalize_path(path)}" + when Symbol + controller_set = true + controller = args.first + controller, @scope[:controller] = @scope[:controller], controller + end + + yield + + self + ensure + @scope[:path] = path if path_set + @scope[:controller] = controller if controller_set + @scope[:options] = options + @scope[:blocks] = blocks + @scope[:constraints] = constraints + end + + def controller(controller) + scope(controller.to_sym) { yield } + end + + def namespace(path) + scope(path.to_s) { yield } + end + + def constraints(constraints = {}) + scope(:constraints => constraints) { yield } + end + end + + class Constraints + def initialize(app, constraints = []) + @app, @constraints = app, constraints + end + + def call(env) + req = Rack::Request.new(env) + + @constraints.each { |constraint| + if constraint.respond_to?(:matches?) && !constraint.matches?(req) + return [417, {}, []] + elsif constraint.respond_to?(:call) && !constraint.call(req) + return [417, {}, []] + end + } + + @app.call(env) + end + end + + def initialize(set) + @set = set + @scope = {} + + extend Scoping + extend Resources + end + + def get(*args, &block) + map_method(:get, *args, &block) + end + + def post(*args, &block) + map_method(:post, *args, &block) + end + + def put(*args, &block) + map_method(:put, *args, &block) + end + + def delete(*args, &block) + map_method(:delete, *args, &block) + end + + def root(options = {}) + match '/', options.merge(:as => :root) + end + + def match(*args) + options = args.last.is_a?(Hash) ? args.pop : {} + + if args.length > 1 + args.each { |path| match(path, options) } + return self + end + + if args.first.is_a?(Symbol) + return match(args.first.to_s, options.merge(:to => args.first.to_sym)) + end + + path = args.first + + options = (@scope[:options] || {}).merge(options) + conditions, defaults = {}, {} + + path = nil if path == "" + path = Rack::Mount::Utils.normalize_path(path) if path + path = "#{@scope[:path]}#{path}" if @scope[:path] + + raise ArgumentError, "path is required" unless path + + constraints = options[:constraints] || {} + unless constraints.is_a?(Hash) + block, constraints = constraints, {} + end + blocks = ((@scope[:blocks] || []) + [block]).compact + constraints = (@scope[:constraints] || {}).merge(constraints) + options.each { |k, v| constraints[k] = v if v.is_a?(Regexp) } + + conditions[:path_info] = path + requirements = constraints.dup + + path_regexp = Rack::Mount::Strexp.compile(path, constraints, SEPARATORS) + segment_keys = Rack::Mount::RegexpWithNamedGroups.new(path_regexp).names + constraints.reject! { |k, v| segment_keys.include?(k.to_s) } + conditions.merge!(constraints) + + if via = options[:via] + via = Array(via).map { |m| m.to_s.upcase } + conditions[:request_method] = Regexp.union(*via) + end + + defaults[:controller] = @scope[:controller].to_s if @scope[:controller] + + if options[:to].respond_to?(:call) + app = options[:to] + defaults.delete(:controller) + defaults.delete(:action) + elsif options[:to].is_a?(String) + defaults[:controller], defaults[:action] = options[:to].split('#') + elsif options[:to].is_a?(Symbol) + defaults[:action] = options[:to].to_s + end + app ||= Routing::RouteSet::Dispatcher.new(:defaults => defaults) + + if app.is_a?(Routing::RouteSet::Dispatcher) + unless defaults.include?(:controller) || segment_keys.include?("controller") + raise ArgumentError, "missing :controller" + end + unless defaults.include?(:action) || segment_keys.include?("action") + raise ArgumentError, "missing :action" + end + end + + app = Constraints.new(app, blocks) if blocks.any? + @set.add_route(app, conditions, requirements, defaults, options[:as]) + + self + end + + def redirect(path, options = {}) + status = options[:status] || 301 + lambda { |env| + req = Rack::Request.new(env) + url = req.scheme + '://' + req.host + path + [status, {'Location' => url, 'Content-Type' => 'text/html'}, ['Moved Permanently']] + } + end + + private + def map_method(method, *args, &block) + options = args.last.is_a?(Hash) ? args.pop : {} + options[:via] = method + args.push(options) + match(*args, &block) + self + end + end + end +end diff --git a/actionpack/lib/action_dispatch/routing/route.rb b/actionpack/lib/action_dispatch/routing/route.rb new file mode 100644 index 0000000000..f1431e7a37 --- /dev/null +++ b/actionpack/lib/action_dispatch/routing/route.rb @@ -0,0 +1,49 @@ +module ActionDispatch + module Routing + class Route #:nodoc: + attr_reader :app, :conditions, :defaults, :name + attr_reader :path, :requirements + + def initialize(app, conditions = {}, requirements = {}, defaults = {}, name = nil) + @app = app + @defaults = defaults + @name = name + + @requirements = requirements.merge(defaults) + @requirements.delete(:controller) if @requirements[:controller].is_a?(Regexp) + @requirements.delete_if { |k, v| + v == Regexp.compile("[^#{SEPARATORS.join}]+") + } + + if path = conditions[:path_info] + @path = path + conditions[:path_info] = ::Rack::Mount::Strexp.compile(path, requirements, SEPARATORS) + end + + @conditions = conditions.inject({}) { |h, (k, v)| + h[k] = Rack::Mount::RegexpWithNamedGroups.new(v) + h + } + end + + def verb + if method = conditions[:request_method] + case method + when Regexp + method.source.upcase + else + method.to_s.upcase + end + end + end + + def segment_keys + @segment_keys ||= conditions[:path_info].names.compact.map { |key| key.to_sym } + end + + def to_a + [@app, @conditions, @defaults, @name] + end + end + end +end diff --git a/actionpack/lib/action_controller/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 25fdbf480e..c28df76f3f 100644 --- a/actionpack/lib/action_controller/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -1,69 +1,63 @@ -module ActionController +require 'rack/mount' +require 'forwardable' + +module ActionDispatch module Routing class RouteSet #:nodoc: - # Mapper instances are used to build routes. The object passed to the draw - # block in config/routes.rb is a Mapper instance. - # - # Mapper instances have relatively few instance methods, in order to avoid - # clashes with named routes. - class Mapper #:doc: - include ActionController::Resources - - def initialize(set) #:nodoc: - @set = set - end + NotFound = lambda { |env| + raise ActionController::RoutingError, "No route matches #{env['PATH_INFO'].inspect} with #{env.inspect}" + } + + PARAMETERS_KEY = 'action_dispatch.request.path_parameters' - # Create an unnamed route with the provided +path+ and +options+. See - # ActionController::Routing for an introduction to routes. - def connect(path, options = {}) - @set.add_route(path, options) + class Dispatcher + def initialize(options = {}) + defaults = options[:defaults] + @glob_param = options.delete(:glob) end - # 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 }) + def call(env) + params = env[PARAMETERS_KEY] + merge_default_action!(params) + split_glob_param!(params) if @glob_param + params.each do |key, value| + if value.is_a?(String) + value = value.dup.force_encoding(Encoding::BINARY) if value.respond_to?(:force_encoding) + params[key] = URI.unescape(value) end end - named_route("root", '', options) - end - - def named_route(name, path, options = {}) #:nodoc: - @set.add_named_route(name, path, options) - end - # Enables the use of resources in a module by setting the name_prefix, path_prefix, and namespace for the model. - # Example: - # - # map.namespace(:admin) do |admin| - # admin.resources :products, - # :has_many => [ :tags, :images, :variants ] - # end - # - # This will create +admin_products_url+ pointing to "admin/products", which will look for an Admin::ProductsController. - # It'll also create +admin_product_tags_url+ pointing to "admin/products/#{product_id}/tags", which will look for - # Admin::TagsController. - 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) + if env['action_controller.recognize'] + [200, {}, params] else - with_options({:path_prefix => name, :name_prefix => "#{name}_", :namespace => "#{name}/" }.merge(options), &block) + controller = controller(params) + controller.action(params[:action]).call(env) end end - def method_missing(route_name, *args, &proc) #:nodoc: - super unless args.length >= 1 && proc.nil? - @set.add_named_route(route_name, *args) - end + private + def controller(params) + if params && params.has_key?(:controller) + controller = "#{params[:controller].camelize}Controller" + ActiveSupport::Inflector.constantize(controller) + end + end + + def merge_default_action!(params) + params[:action] ||= 'index' + end + + def split_glob_param!(params) + params[@glob_param] = params[@glob_param].split('/').map { |v| URI.unescape(v) } + end end + # A NamedRouteCollection instance is a collection of named routes, and also # maintains an anonymous module that can be used to install helpers for the # named routes. class NamedRouteCollection #:nodoc: include Enumerable - include ActionController::Routing::Optimisation attr_reader :routes, :helpers def initialize @@ -175,8 +169,6 @@ module ActionController named_helper_module_eval <<-end_eval # We use module_eval to avoid leaks def #{selector}(*args) # def users_url(*args) # - #{generate_optimisation_block(route, kind)} # #{generate_optimisation_block(route, kind)} - # opts = if args.empty? || Hash === args.first # opts = if args.empty? || Hash === args.first args.first || {} # args.first || {} else # else @@ -216,28 +208,18 @@ module ActionController clear! end - # Subclasses and plugins may override this method to specify a different - # RouteBuilder instance, so that other route DSL's can be created. - def builder - @builder ||= RouteBuilder.new - end - - def draw + def draw(&block) clear! - yield Mapper.new(self) + Mapper.new(self).instance_exec(DeprecatedMapper.new(self), &block) + @set.add_route(NotFound) install_helpers + @set.freeze end def clear! routes.clear named_routes.clear - - @combined_regexp = nil - @routes_by_controller = nil - - # This will force routing/recognition_optimization.rb - # to refresh optimisations. - clear_recognize_optimized! + @set = ::Rack::Mount::RouteSet.new(:parameters_key => PARAMETERS_KEY) end def install_helpers(destinations = [ActionController::Base, ActionView::Base], regenerate_code = false) @@ -257,7 +239,7 @@ module ActionController def configuration_file=(path) add_configuration_file(path) end - + # Deprecated accessor def configuration_file configuration_files @@ -296,31 +278,26 @@ module ActionController def routes_changed_at routes_changed_at = nil - + configuration_files.each do |config| config_changed_at = File.stat(config).mtime if routes_changed_at.nil? || config_changed_at > routes_changed_at - routes_changed_at = config_changed_at + routes_changed_at = config_changed_at end end - + routes_changed_at end - def add_route(path, options = {}) - options.each { |k, v| options[k] = v.to_s if [:controller, :action].include?(k) && v.is_a?(Symbol) } - route = builder.build(path, options) + def add_route(app, conditions = {}, requirements = {}, defaults = {}, name = nil) + route = Route.new(app, conditions, requirements, defaults, name) + @set.add_route(*route) + named_routes[name] = route if name routes << route route end - def add_named_route(name, path, options = {}) - # TODO - is options EVER used? - name = options[:name_prefix] + name.to_s if options[:name_prefix] - named_routes[name.to_sym] = add_route(path, options) - end - def options_as_params(options) # If an explicit :controller was given, always make :action explicit # too, so that action expiry works as expected for things like @@ -356,24 +333,29 @@ module ActionController generate(options, recall, :generate_extras) end - def generate(options, recall = {}, method=:generate) - named_route_name = options.delete(:use_route) - generate_all = options.delete(:generate_all) - if named_route_name - named_route = named_routes[named_route_name] - options = named_route.parameter_shell.merge(options) - end + def generate(options, recall = {}, method = :generate) + options, recall = options.dup, recall.dup + named_route = options.delete(:use_route) options = options_as_params(options) expire_on = build_expiry(options, recall) - if options[:controller] - options[:controller] = options[:controller].to_s + recall[:action] ||= 'index' if options[:controller] || recall[:controller] + + if recall[:controller] && (!options.has_key?(:controller) || options[:controller] == recall[:controller]) + options[:controller] = recall.delete(:controller) + + if recall[:action] && (!options.has_key?(:action) || options[:action] == recall[:action]) + options[:action] = recall.delete(:action) + + if recall[:id] && (!options.has_key?(:id) || options[:id] == recall[:id]) + options[:id] = recall.delete(:id) + end + end end - # if the controller has changed, make sure it changes relative to the - # current controller module, if any. In other words, if we're currently - # on admin/get, and the new controller is 'set', the new controller - # should really be admin/set. + + options[:controller] = options[:controller].to_s if options[:controller] + if !named_route && expire_on[:controller] && options[:controller] && options[:controller][0] != ?/ old_parts = recall[:controller].split('/') new_parts = options[:controller].split('/') @@ -381,63 +363,54 @@ module ActionController options[:controller] = parts.join('/') end - # drop the leading '/' on the controller name options[:controller] = options[:controller][1..-1] if options[:controller] && options[:controller][0] == ?/ - merged = recall.merge(options) - - if named_route - path = named_route.generate(options, merged, expire_on) - if path.nil? - raise_named_route_error(options, named_route, named_route_name) - else - return path - end - else - merged[:action] ||= 'index' - options[:action] ||= 'index' - - controller = merged[:controller] - action = merged[:action] - raise RoutingError, "Need controller and action!" unless controller && action - - if generate_all - # Used by caching to expire all paths for a resource - return routes.collect do |route| - route.__send__(method, options, merged, expire_on) - end.compact - end - - # don't use the recalled keys when determining which routes to check - routes = routes_by_controller[controller][action][options.reject {|k,v| !v}.keys.sort_by { |x| x.object_id }] - - routes.each_with_index do |route, index| - results = route.__send__(method, options, merged, expire_on) - if results && (!results.is_a?(Array) || results.first) - return results - end - end + merged = options.merge(recall) + if options.has_key?(:action) && options[:action].nil? + options.delete(:action) + recall[:action] = 'index' end - - raise RoutingError, "No route matches #{options.inspect}" - end - - # try to give a helpful error message when named route generation fails - def raise_named_route_error(options, named_route, named_route_name) - diff = named_route.requirements.diff(options) - unless diff.empty? - raise RoutingError, "#{named_route_name}_url failed to generate from #{options.inspect}, expected: #{named_route.requirements.inspect}, diff: #{named_route.requirements.diff(options).inspect}" + recall[:action] = options.delete(:action) if options[:action] == 'index' + + path = _uri(named_route, options, recall) + if path && method == :generate_extras + uri = URI(path) + extras = uri.query ? + Rack::Utils.parse_nested_query(uri.query).keys.map { |k| k.to_sym } : + [] + [uri.path, extras] + elsif path + path else - required_segments = named_route.segments.select {|seg| (!seg.optional?) && (!seg.is_a?(DividerSegment)) } - required_keys_or_values = required_segments.map { |seg| seg.key rescue seg.value } # we want either the key or the value from the segment - raise RoutingError, "#{named_route_name}_url failed to generate from #{options.inspect} - you may have ambiguous routes, or you may need to supply additional parameters for this route. content_url has the following required parameters: #{required_keys_or_values.inspect} - are they all satisfied?" + raise ActionController::RoutingError, "No route matches #{options.inspect}" end + rescue Rack::Mount::RoutingError + raise ActionController::RoutingError, "No route matches #{options.inspect}" end def call(env) - request = ActionDispatch::Request.new(env) - app = Routing::Routes.recognize(request) - app.action(request.parameters[:action] || 'index').call(env) + @set.call(env) + rescue ActionController::RoutingError => e + raise e if env['action_controller.rescue_error'] == false + + method, path = env['REQUEST_METHOD'].downcase.to_sym, env['PATH_INFO'] + + # Route was not recognized. Try to find out why (maybe wrong verb). + allows = HTTP_METHODS.select { |verb| + begin + recognize_path(path, {:method => verb}, false) + rescue ActionController::RoutingError + nil + end + } + + if !HTTP_METHODS.include?(method) + raise ActionController::NotImplemented.new(*allows) + elsif !allows.empty? + raise ActionController::MethodNotAllowed.new(*allows) + else + raise e + end end def recognize(request) @@ -446,33 +419,19 @@ module ActionController "#{params[:controller].to_s.camelize}Controller".constantize end - def recognize_path(path, environment={}) - raise "Not optimized! Check that routing/recognition_optimisation overrides RouteSet#recognize_path." - end + def recognize_path(path, environment = {}, rescue_error = true) + method = (environment[:method] || "GET").to_s.upcase - def routes_by_controller - @routes_by_controller ||= Hash.new do |controller_hash, controller| - controller_hash[controller] = Hash.new do |action_hash, action| - action_hash[action] = Hash.new do |key_hash, keys| - key_hash[keys] = routes_for_controller_and_action_and_keys(controller, action, keys) - end - end + begin + env = Rack::MockRequest.env_for(path, {:method => method}) + rescue URI::InvalidURIError => e + raise ActionController::RoutingError, e.message end - end - - def routes_for(options, merged, expire_on) - raise "Need controller and action!" unless controller && action - controller = merged[:controller] - merged = options if expire_on[:controller] - action = merged[:action] || 'index' - routes_by_controller[controller][action][merged.keys][1] - end - - def routes_for_controller_and_action_and_keys(controller, action, keys) - routes.select do |route| - route.matches_controller_and_action? controller, action - end + env['action_controller.recognize'] = true + env['action_controller.rescue_error'] = rescue_error + status, headers, body = call(env) + body end # Subclasses and plugins may override this method to extract further attributes @@ -480,6 +439,59 @@ module ActionController def extract_request_environment(request) { :method => request.method } end + + private + def _uri(named_route, params, recall) + params = URISegment.wrap_values(params) + recall = URISegment.wrap_values(recall) + + unless result = @set.generate(:path_info, named_route, params, recall) + return + end + + uri, params = result + params.each do |k, v| + if v._value + params[k] = v._value + else + params.delete(k) + end + end + + uri << "?#{Rack::Mount::Utils.build_nested_query(params)}" if uri && params.any? + uri + end + + class URISegment < Struct.new(:_value, :_escape) + EXCLUDED = [:controller] + + def self.wrap_values(hash) + hash.inject({}) { |h, (k, v)| + h[k] = new(v, !EXCLUDED.include?(k.to_sym)) + h + } + end + + extend Forwardable + def_delegators :_value, :==, :eql?, :hash + + def to_param + @to_param ||= begin + if _value.is_a?(Array) + _value.map { |v| _escaped(v) }.join('/') + else + _escaped(_value) + end + end + end + alias_method :to_s, :to_param + + private + def _escaped(value) + v = value.respond_to?(:to_param) ? value.to_param : value + _escape ? Rack::Mount::Utils.escape_uri(v) : v.to_s + end + end end end end diff --git a/actionpack/lib/action_dispatch/testing/assertions.rb b/actionpack/lib/action_dispatch/testing/assertions.rb index 96f08f2355..0e4a92048f 100644 --- a/actionpack/lib/action_dispatch/testing/assertions.rb +++ b/actionpack/lib/action_dispatch/testing/assertions.rb @@ -1,8 +1,21 @@ module ActionDispatch module Assertions - %w(response selector tag dom routing model).each do |kind| - require "action_dispatch/testing/assertions/#{kind}" - include const_get("#{kind.camelize}Assertions") + autoload :DomAssertions, 'action_dispatch/testing/assertions/dom' + autoload :ModelAssertions, 'action_dispatch/testing/assertions/model' + autoload :ResponseAssertions, 'action_dispatch/testing/assertions/response' + autoload :RoutingAssertions, 'action_dispatch/testing/assertions/routing' + autoload :SelectorAssertions, 'action_dispatch/testing/assertions/selector' + autoload :TagAssertions, 'action_dispatch/testing/assertions/tag' + + extend ActiveSupport::Concern + + included do + include DomAssertions + include ModelAssertions + include ResponseAssertions + include RoutingAssertions + include SelectorAssertions + include TagAssertions end end end diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index 2c4a3a356d..40d6f97b2a 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -396,8 +396,12 @@ module ActionDispatch # Delegate unhandled messages to the current session instance. def method_missing(sym, *args, &block) reset! unless @integration_session - returning @integration_session.__send__(sym, *args, &block) do - copy_session_variables! + if @integration_session.respond_to?(sym) + returning @integration_session.__send__(sym, *args, &block) do + copy_session_variables! + end + else + super end end end @@ -486,7 +490,7 @@ module ActionDispatch def self.app # DEPRECATE Rails application fallback # This should be set by the initializer - @@app || (defined?(Rails.application) && Rails.application.new) || nil + @@app || (defined?(Rails.application) && Rails.application) || nil end def self.app=(app) diff --git a/actionpack/lib/action_view/base.rb b/actionpack/lib/action_view/base.rb index 31e9c5ef9d..c33695770f 100644 --- a/actionpack/lib/action_view/base.rb +++ b/actionpack/lib/action_view/base.rb @@ -169,6 +169,15 @@ module ActionView #:nodoc: include Helpers, Rendering, Partials, ::ERB::Util + def config + self.config = DEFAULT_CONFIG unless @config + @config + end + + def config=(config) + @config = ActiveSupport::OrderedOptions.new.merge(config) + end + extend ActiveSupport::Memoizable attr_accessor :base_path, :assigns, :template_extension, :formats @@ -178,11 +187,11 @@ module ActionView #:nodoc: def reset_formats(formats) @formats = formats - if defined?(ActionController) + if defined?(AbstractController::HashKey) # This is expensive, but we need to reset this when the format is updated, # which currently only happens Thread.current[:format_locale_key] = - ActionController::HashKey.get(self.class, formats, I18n.locale) + AbstractController::HashKey.get(self.class, formats, I18n.locale) end end @@ -203,6 +212,11 @@ module ActionView #:nodoc: @@cache_template_loading = nil cattr_accessor :cache_template_loading + # :nodoc: + def self.xss_safe? + true + end + def self.cache_template_loading? ActionController::Base.allow_concurrency || (cache_template_loading.nil? ? !ActiveSupport::Dependencies.load? : cache_template_loading) end @@ -236,16 +250,16 @@ module ActionView #:nodoc: # they are in AC. if controller.class.respond_to?(:_helper_serial) klass = @views[controller.class._helper_serial] ||= Class.new(self) do - const_set(:CONTROLLER_CLASS, controller.class) - # Try to make stack traces clearer - def self.name - "ActionView for #{CONTROLLER_CLASS}" - end - - def inspect - "#<#{self.class.name}>" - end + class_eval <<-ruby_eval, __FILE__, __LINE__ + 1 + def self.name + "ActionView for #{controller.class}" + end + + def inspect + "#<#{self.class.name}>" + end + ruby_eval if controller.respond_to?(:_helpers) include controller._helpers diff --git a/actionpack/lib/action_view/erb/util.rb b/actionpack/lib/action_view/erb/util.rb index f767a5e27e..aef859b3ac 100644 --- a/actionpack/lib/action_view/erb/util.rb +++ b/actionpack/lib/action_view/erb/util.rb @@ -23,6 +23,7 @@ class ERB end end + undef :h alias h html_escape module_function :html_escape diff --git a/actionpack/lib/action_view/helpers.rb b/actionpack/lib/action_view/helpers.rb index d63e8602f1..3d088678fc 100644 --- a/actionpack/lib/action_view/helpers.rb +++ b/actionpack/lib/action_view/helpers.rb @@ -1,10 +1,11 @@ +require 'active_support/benchmarkable' + module ActionView #:nodoc: module Helpers #:nodoc: autoload :ActiveModelHelper, 'action_view/helpers/active_model_helper' autoload :AjaxHelper, 'action_view/helpers/ajax_helper' autoload :AssetTagHelper, 'action_view/helpers/asset_tag_helper' autoload :AtomFeedHelper, 'action_view/helpers/atom_feed_helper' - autoload :BenchmarkHelper, 'action_view/helpers/benchmark_helper' autoload :CacheHelper, 'action_view/helpers/cache_helper' autoload :CaptureHelper, 'action_view/helpers/capture_helper' autoload :DateHelper, 'action_view/helpers/date_helper' @@ -33,10 +34,11 @@ module ActionView #:nodoc: include SanitizeHelper::ClassMethods end + include ActiveSupport::Benchmarkable + include ActiveModelHelper include AssetTagHelper include AtomFeedHelper - include BenchmarkHelper include CacheHelper include CaptureHelper include DateHelper diff --git a/actionpack/lib/action_view/helpers/active_model_helper.rb b/actionpack/lib/action_view/helpers/active_model_helper.rb index 7cc1e48572..c70f29f098 100644 --- a/actionpack/lib/action_view/helpers/active_model_helper.rb +++ b/actionpack/lib/action_view/helpers/active_model_helper.rb @@ -191,19 +191,19 @@ module ActionView options = params.extract_options!.symbolize_keys objects = Array.wrap(options.delete(:object) || params).map do |object| - unless object.respond_to?(:to_model) - object = instance_variable_get("@#{object}") - object = convert_to_model(object) - else - object = object.to_model - options[:object_name] ||= object.class.model_name.human + object = instance_variable_get("@#{object}") unless object.respond_to?(:to_model) + object = convert_to_model(object) + + if object.class.respond_to?(:model_name) + options[:object_name] ||= object.class.model_name.human.downcase end + object end objects.compact! - count = objects.inject(0) {|sum, object| sum + object.errors.count } + unless count.zero? html = {} [:id, :class].each do |key| @@ -216,16 +216,20 @@ module ActionView end options[:object_name] ||= params.first - I18n.with_options :locale => options[:locale], :scope => [:activerecord, :errors, :template] do |locale| + I18n.with_options :locale => options[:locale], :scope => [:activemodel, :errors, :template] do |locale| header_message = if options.include?(:header_message) options[:header_message] else - object_name = options[:object_name].to_s.gsub('_', ' ') - object_name = I18n.t(options[:object_name].to_s, :default => object_name, :scope => [:activerecord, :models], :count => 1) - locale.t :header, :count => count, :model => object_name + locale.t :header, :count => count, :model => options[:object_name].to_s.gsub('_', ' ') end + message = options.include?(:message) ? options[:message] : locale.t(:body) - error_messages = objects.sum {|object| object.errors.full_messages.map {|msg| content_tag(:li, ERB::Util.html_escape(msg)) } }.join + + error_messages = objects.sum do |object| + object.errors.full_messages.map do |msg| + content_tag(:li, ERB::Util.html_escape(msg)) + end + end.join contents = '' contents << content_tag(options[:header_tag] || :h2, header_message) unless header_message.blank? diff --git a/actionpack/lib/action_view/helpers/asset_tag_helper.rb b/actionpack/lib/action_view/helpers/asset_tag_helper.rb index faa7f2e2e9..15b70ecff5 100644 --- a/actionpack/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/asset_tag_helper.rb @@ -133,9 +133,13 @@ module ActionView # change. You can use something like Live HTTP Headers for Firefox to verify # that the cache is indeed working. module AssetTagHelper - ASSETS_DIR = defined?(Rails.public_path) ? Rails.public_path : "public" - JAVASCRIPTS_DIR = "#{ASSETS_DIR}/javascripts" - STYLESHEETS_DIR = "#{ASSETS_DIR}/stylesheets" + assets_dir = defined?(Rails.public_path) ? Rails.public_path : "public" + ActionView::DEFAULT_CONFIG = { + :assets_dir => assets_dir, + :javascripts_dir => "#{assets_dir}/javascripts", + :stylesheets_dir => "#{assets_dir}/stylesheets", + } + JAVASCRIPT_DEFAULT_SOURCES = ['prototype', 'effects', 'dragdrop', 'controls'].freeze unless const_defined?(:JAVASCRIPT_DEFAULT_SOURCES) # Returns a link tag that browsers and news readers can use to auto-detect @@ -280,7 +284,7 @@ module ActionView if concat || (ActionController::Base.perform_caching && cache) joined_javascript_name = (cache == true ? "all" : cache) + ".js" - joined_javascript_path = File.join(joined_javascript_name[/^#{File::SEPARATOR}/] ? ASSETS_DIR : JAVASCRIPTS_DIR, joined_javascript_name) + joined_javascript_path = File.join(joined_javascript_name[/^#{File::SEPARATOR}/] ? config.assets_dir : config.javascripts_dir, joined_javascript_name) unless ActionController::Base.perform_caching && File.exists?(joined_javascript_path) write_asset_file_contents(joined_javascript_path, compute_javascript_paths(sources, recursive)) @@ -431,7 +435,7 @@ module ActionView if concat || (ActionController::Base.perform_caching && cache) joined_stylesheet_name = (cache == true ? "all" : cache) + ".css" - joined_stylesheet_path = File.join(joined_stylesheet_name[/^#{File::SEPARATOR}/] ? ASSETS_DIR : STYLESHEETS_DIR, joined_stylesheet_name) + joined_stylesheet_path = File.join(joined_stylesheet_name[/^#{File::SEPARATOR}/] ? config.assets_dir : config.stylesheets_dir, joined_stylesheet_name) unless ActionController::Base.perform_caching && File.exists?(joined_stylesheet_path) write_asset_file_contents(joined_stylesheet_path, compute_stylesheet_paths(sources, recursive)) @@ -630,11 +634,11 @@ module ActionView # 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) + def compute_public_path(source, dir, ext = nil, include_host = true) has_request = @controller.respond_to?(:request) source_ext = File.extname(source)[1..-1] - if ext && !is_uri?(source) && (source_ext.blank? || (ext != source_ext && File.exist?(File.join(ASSETS_DIR, dir, "#{source}.#{ext}")))) + if ext && !is_uri?(source) && (source_ext.blank? || (ext != source_ext && File.exist?(File.join(config.assets_dir, dir, "#{source}.#{ext}")))) source += ".#{ext}" end @@ -700,7 +704,7 @@ module ActionView if @@cache_asset_timestamps && (asset_id = @@asset_timestamps_cache[source]) asset_id else - path = File.join(ASSETS_DIR, source) + path = File.join(config.assets_dir, source) asset_id = File.exist?(path) ? File.mtime(path).to_i.to_s : '' if @@cache_asset_timestamps @@ -743,20 +747,20 @@ module ActionView def expand_javascript_sources(sources, recursive = false) if sources.include?(:all) - all_javascript_files = collect_asset_files(JAVASCRIPTS_DIR, ('**' if recursive), '*.js') + 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(JAVASCRIPTS_DIR, "application.js")) + 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(STYLESHEETS_DIR, ('**' if recursive), '*.css') + collect_asset_files(config.stylesheets_dir, ('**' if recursive), '*.css') else sources.collect do |source| determine_source(source, @@stylesheet_expansions) @@ -803,7 +807,7 @@ module ActionView end def asset_file_path(path) - File.join(ASSETS_DIR, path.split('?').first) + File.join(config.assets_dir, path.split('?').first) end def asset_file_path!(path) diff --git a/actionpack/lib/action_view/helpers/benchmark_helper.rb b/actionpack/lib/action_view/helpers/benchmark_helper.rb deleted file mode 100644 index c13a069a35..0000000000 --- a/actionpack/lib/action_view/helpers/benchmark_helper.rb +++ /dev/null @@ -1,54 +0,0 @@ -require 'active_support/core_ext/benchmark' - -module ActionView - module Helpers - # This helper offers a method to measure the execution time of a block - # in a template. - module BenchmarkHelper - # Allows you to measure the execution time of a block - # in a template and records the result to the log. Wrap this block around - # expensive operations or possible bottlenecks to get a time reading - # for the operation. For example, let's say you thought your file - # processing method was taking too long; you could wrap it in a benchmark block. - # - # <% benchmark "Process data files" do %> - # <%= expensive_files_operation %> - # <% end %> - # - # That would add something like "Process data files (345.2ms)" to the log, - # which you can then use to compare timings when optimizing your code. - # - # You may give an optional logger level as the :level option. - # (:debug, :info, :warn, :error); the default value is :info. - # - # <% benchmark "Low-level files", :level => :debug do %> - # <%= lowlevel_files_operation %> - # <% end %> - # - # Finally, you can pass true as the third argument to silence all log activity - # inside the block. This is great for boiling down a noisy block to just a single statement: - # - # <% benchmark "Process data files", :level => :info, :silence => true do %> - # <%= expensive_and_chatty_files_operation %> - # <% end %> - def benchmark(message = "Benchmarking", options = {}) - if controller.logger - if options.is_a?(Symbol) - ActiveSupport::Deprecation.warn("use benchmark('#{message}', :level => :#{options}) instead", caller) - options = { :level => options, :silence => false } - else - options.assert_valid_keys(:level, :silence) - options[:level] ||= :info - end - - result = nil - ms = Benchmark.ms { result = options[:silence] ? controller.logger.silence { yield } : yield } - controller.logger.send(options[:level], '%s (%.1fms)' % [ message, ms ]) - result - else - yield - end - end - end - end -end diff --git a/actionpack/lib/action_view/helpers/form_helper.rb b/actionpack/lib/action_view/helpers/form_helper.rb index c46b39fc23..d0c66eda60 100644 --- a/actionpack/lib/action_view/helpers/form_helper.rb +++ b/actionpack/lib/action_view/helpers/form_helper.rb @@ -963,7 +963,7 @@ module ActionView end end - (field_helpers - %w(label check_box radio_button fields_for)).each do |selector| + (field_helpers - %w(label check_box radio_button fields_for hidden_field)).each do |selector| src = <<-end_src def #{selector}(method, options = {}) # def text_field(method, options = {}) @template.send( # @template.send( @@ -1022,6 +1022,11 @@ module ActionView def radio_button(method, tag_value, options = {}) @template.radio_button(@object_name, method, tag_value, objectify_options(options)) end + + def hidden_field(method, options = {}) + @emitted_hidden_id = true if method == :id + @template.hidden_field(@object_name, method, objectify_options(options)) + end def error_message_on(method, *args) @template.error_message_on(@object, method, *args) @@ -1035,6 +1040,10 @@ module ActionView @template.submit_tag(value, options.reverse_merge(:id => "#{object_name}_submit")) end + def emitted_hidden_id? + @emitted_hidden_id + end + private def objectify_options(options) @default_options.merge(options.merge(:object => @object)) @@ -1069,8 +1078,8 @@ module ActionView @template.fields_for(name, object, *args, &block) else @template.fields_for(name, object, *args) do |builder| - @template.concat builder.hidden_field(:id) block.call(builder) + @template.concat builder.hidden_field(:id) unless builder.emitted_hidden_id? end end end diff --git a/actionpack/lib/action_view/helpers/translation_helper.rb b/actionpack/lib/action_view/helpers/translation_helper.rb index 4aed10f640..564f12c955 100644 --- a/actionpack/lib/action_view/helpers/translation_helper.rb +++ b/actionpack/lib/action_view/helpers/translation_helper.rb @@ -21,7 +21,7 @@ module ActionView # Delegates to I18n.localize with no additional functionality. def localize(*args) - I18n.localize *args + I18n.localize(*args) end alias :l :localize diff --git a/actionpack/lib/action_view/helpers/url_helper.rb b/actionpack/lib/action_view/helpers/url_helper.rb index e651bc17a9..5b136d4f54 100644 --- a/actionpack/lib/action_view/helpers/url_helper.rb +++ b/actionpack/lib/action_view/helpers/url_helper.rb @@ -83,7 +83,7 @@ module ActionView options when Hash options = { :only_path => options[:host].nil? }.update(options.symbolize_keys) - escape = options.key?(:escape) ? options.delete(:escape) : true + escape = options.key?(:escape) ? options.delete(:escape) : false @controller.send(:url_for, options) when :back escape = false @@ -93,7 +93,7 @@ module ActionView polymorphic_path(options) end - (escape ? escape_once(url) : url).html_safe! + escape ? escape_once(url).html_safe! : url end # Creates a link tag of the given +name+ using a URL created by the set diff --git a/actionpack/lib/action_view/locale/en.yml b/actionpack/lib/action_view/locale/en.yml index 84d94fd700..5e2a92b89a 100644 --- a/actionpack/lib/action_view/locale/en.yml +++ b/actionpack/lib/action_view/locale/en.yml @@ -102,7 +102,7 @@ minute: "Minute" second: "Seconds" - activerecord: + activemodel: errors: template: header: @@ -114,4 +114,4 @@ support: select: # default value for :prompt => true in FormOptionsHelper - prompt: "Please select"
\ No newline at end of file + prompt: "Please select" diff --git a/actionpack/lib/action_view/render/rendering.rb b/actionpack/lib/action_view/render/rendering.rb index b6f5b9b6d1..7006a5b968 100644 --- a/actionpack/lib/action_view/render/rendering.rb +++ b/actionpack/lib/action_view/render/rendering.rb @@ -71,12 +71,10 @@ module ActionView # In this case, the layout would receive the block passed into <tt>render :layout</tt>, # and the Struct specified in the layout would be passed into the block. The result # would be <html>Hello David</html>. - def _layout_for(name = nil) + def _layout_for(name = nil, &block) return @_content_for[name || :layout] if !block_given? || name - with_output_buffer do - return yield - end + capture(&block) end def _render_inline(inline, layout, options) @@ -123,7 +121,6 @@ module ActionView template.render(self, locals) end - @cached_content_for_layout = content @_content_for[:layout] = content if layout @@ -134,4 +131,4 @@ module ActionView content end end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_view/safe_buffer.rb b/actionpack/lib/action_view/safe_buffer.rb index 8ba9cd80d6..09f44ab26f 100644 --- a/actionpack/lib/action_view/safe_buffer.rb +++ b/actionpack/lib/action_view/safe_buffer.rb @@ -5,7 +5,7 @@ module ActionView #:nodoc: if value.html_safe? super(value) else - super(CGI.escapeHTML(value)) + super(ERB::Util.h(value)) end end diff --git a/actionpack/lib/action_view/template/handlers/erb.rb b/actionpack/lib/action_view/template/handlers/erb.rb index a780ab8d85..88aeb4b053 100644 --- a/actionpack/lib/action_view/template/handlers/erb.rb +++ b/actionpack/lib/action_view/template/handlers/erb.rb @@ -37,11 +37,16 @@ module ActionView self.erb_trim_mode = '-' self.default_format = Mime::HTML + + cattr_accessor :erubis_implementation + self.erubis_implementation = Erubis def compile(template) - magic = $1 if template.source =~ /\A(<%#.*coding[:=]\s*(\S+)\s*-?%>)/ - erb = "#{magic}<% __in_erb_template=true %>#{template.source}" - Erubis.new(erb, :trim=>(self.class.erb_trim_mode == "-")).src + source = template.source.gsub(/\A(<%(#.*coding[:=]\s*(\S+)\s*)-?%>)\s*\n?/, '') + erb = "<% __in_erb_template=true %>#{source}" + result = self.class.erubis_implementation.new(erb, :trim=>(self.class.erb_trim_mode == "-")).src + result = "#{$2}\n#{result}" if $2 + result end end end diff --git a/actionpack/lib/action_view/template/resolver.rb b/actionpack/lib/action_view/template/resolver.rb index f5591ead09..7336114e1b 100644 --- a/actionpack/lib/action_view/template/resolver.rb +++ b/actionpack/lib/action_view/template/resolver.rb @@ -121,7 +121,7 @@ module ActionView # # :api: plugin def path_to_details(path) # [:erb, :format => :html, :locale => :en, :partial => true/false] - if m = path.match(%r'(?:^|/)(_)?[\w-]+(\.[\w-]+)*\.(\w+)$') + if m = path.match(%r'(?:^|/)(_)?[\w-]+((?:\.[\w-]+)*)\.(\w+)$') partial = m[1] == '_' details = (m[2]||"").split('.').reject { |e| e.empty? } handler = Template.handler_class_for_extension(m[3]) diff --git a/actionpack/lib/action_view/template/template.rb b/actionpack/lib/action_view/template/template.rb index 0f64c23649..d1970ca3c7 100644 --- a/actionpack/lib/action_view/template/template.rb +++ b/actionpack/lib/action_view/template/template.rb @@ -27,10 +27,10 @@ module ActionView end def render(view, locals, &block) - ActiveSupport::Orchestra.instrument(:render_template, :identifier => identifier) do + ActiveSupport::Notifications.instrument(:render_template, :identifier => identifier) do method_name = compile(locals, view) view.send(method_name, locals, &block) - end.result + end rescue Exception => e if e.is_a?(TemplateError) e.sub_template_of(self) diff --git a/actionpack/lib/action_view/template/text.rb b/actionpack/lib/action_view/template/text.rb index f7d0df5ba0..f6e011a5ab 100644 --- a/actionpack/lib/action_view/template/text.rb +++ b/actionpack/lib/action_view/template/text.rb @@ -1,6 +1,8 @@ module ActionView #:nodoc: class TextTemplate < String #:nodoc: - def initialize(string, content_type = Mime[:html]) + HTML = Mime[:html] + + def initialize(string, content_type = HTML) super(string.to_s) @content_type = Mime[content_type] || content_type end @@ -14,11 +16,11 @@ module ActionView #:nodoc: end def inspect - 'inline template' + 'text template' end def render(*args) - self + to_s end def mime_type diff --git a/actionpack/lib/action_view/test_case.rb b/actionpack/lib/action_view/test_case.rb index 8beda24aba..86bbad822d 100644 --- a/actionpack/lib/action_view/test_case.rb +++ b/actionpack/lib/action_view/test_case.rb @@ -120,6 +120,7 @@ module ActionView def _view view = ActionView::Base.new(ActionController::Base.view_paths, _assigns, @controller) view.class.send :include, _helpers + view.output_buffer = self.output_buffer view end |