diff options
Diffstat (limited to 'actionpack/lib')
114 files changed, 2158 insertions, 1536 deletions
diff --git a/actionpack/lib/abstract_controller/asset_paths.rb b/actionpack/lib/abstract_controller/asset_paths.rb index ad14cd6d87..c2a6809f58 100644 --- a/actionpack/lib/abstract_controller/asset_paths.rb +++ b/actionpack/lib/abstract_controller/asset_paths.rb @@ -3,7 +3,8 @@ module AbstractController extend ActiveSupport::Concern included do - config_accessor :asset_host, :asset_path, :assets_dir, :javascripts_dir, :stylesheets_dir, :use_sprockets + config_accessor :asset_host, :asset_path, :assets_dir, :javascripts_dir, + :stylesheets_dir, :default_asset_host_protocol end end end diff --git a/actionpack/lib/abstract_controller/base.rb b/actionpack/lib/abstract_controller/base.rb index f67d0e558e..fd6a46fbec 100644 --- a/actionpack/lib/abstract_controller/base.rb +++ b/actionpack/lib/abstract_controller/base.rb @@ -10,7 +10,7 @@ module AbstractController # <tt>AbstractController::Base</tt> is a low-level API. Nobody should be # using it directly, and subclasses (like ActionController::Base) are # expected to provide their own +render+ method, since rendering means - # different things depending on the context. + # different things depending on the context. class Base attr_internal :response_body attr_internal :action_name diff --git a/actionpack/lib/abstract_controller/callbacks.rb b/actionpack/lib/abstract_controller/callbacks.rb index e8426bc52b..7004e607a1 100644 --- a/actionpack/lib/abstract_controller/callbacks.rb +++ b/actionpack/lib/abstract_controller/callbacks.rb @@ -75,38 +75,122 @@ module AbstractController end end + ## + # :method: before_filter + # + # :call-seq: before_filter(names, block) + # + # Append a before filter. See _insert_callbacks for parameter details. + + ## + # :method: prepend_before_filter + # + # :call-seq: prepend_before_filter(names, block) + # + # Prepend a before filter. See _insert_callbacks for parameter details. + + ## + # :method: skip_before_filter + # + # :call-seq: skip_before_filter(names, block) + # + # Skip a before filter. See _insert_callbacks for parameter details. + + ## + # :method: append_before_filter + # + # :call-seq: append_before_filter(names, block) + # + # Append a before filter. See _insert_callbacks for parameter details. + + ## + # :method: after_filter + # + # :call-seq: after_filter(names, block) + # + # Append an after filter. See _insert_callbacks for parameter details. + + ## + # :method: prepend_after_filter + # + # :call-seq: prepend_after_filter(names, block) + # + # Prepend an after filter. See _insert_callbacks for parameter details. + + ## + # :method: skip_after_filter + # + # :call-seq: skip_after_filter(names, block) + # + # Skip an after filter. See _insert_callbacks for parameter details. + + ## + # :method: append_after_filter + # + # :call-seq: append_after_filter(names, block) + # + # Append an after filter. See _insert_callbacks for parameter details. + + ## + # :method: around_filter + # + # :call-seq: around_filter(names, block) + # + # Append an around filter. See _insert_callbacks for parameter details. + + ## + # :method: prepend_around_filter + # + # :call-seq: prepend_around_filter(names, block) + # + # Prepend an around filter. See _insert_callbacks for parameter details. + + ## + # :method: skip_around_filter + # + # :call-seq: skip_around_filter(names, block) + # + # Skip an around filter. See _insert_callbacks for parameter details. + + ## + # :method: append_around_filter + # + # :call-seq: append_around_filter(names, block) + # + # Append an around filter. See _insert_callbacks for parameter details. + # set up before_filter, prepend_before_filter, skip_before_filter, etc. # for each of before, after, and around. [:before, :after, :around].each do |filter| class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 # Append a before, after or around filter. See _insert_callbacks # for details on the allowed parameters. - def #{filter}_filter(*names, &blk) - _insert_callbacks(names, blk) do |name, options| - options[:if] = (Array.wrap(options[:if]) << "!halted") if #{filter == :after} - set_callback(:process_action, :#{filter}, name, options) - end - end + def #{filter}_filter(*names, &blk) # def before_filter(*names, &blk) + _insert_callbacks(names, blk) do |name, options| # _insert_callbacks(names, blk) do |name, options| + options[:if] = (Array.wrap(options[:if]) << "!halted") if #{filter == :after} # options[:if] = (Array.wrap(options[:if]) << "!halted") if false + set_callback(:process_action, :#{filter}, name, options) # set_callback(:process_action, :before, name, options) + end # end + end # end # Prepend a before, after or around filter. See _insert_callbacks # for details on the allowed parameters. - def prepend_#{filter}_filter(*names, &blk) - _insert_callbacks(names, blk) do |name, options| - options[:if] = (Array.wrap(options[:if]) << "!halted") if #{filter == :after} - set_callback(:process_action, :#{filter}, name, options.merge(:prepend => true)) - end - end + def prepend_#{filter}_filter(*names, &blk) # def prepend_before_filter(*names, &blk) + _insert_callbacks(names, blk) do |name, options| # _insert_callbacks(names, blk) do |name, options| + options[:if] = (Array.wrap(options[:if]) << "!halted") if #{filter == :after} # options[:if] = (Array.wrap(options[:if]) << "!halted") if false + set_callback(:process_action, :#{filter}, name, options.merge(:prepend => true)) # set_callback(:process_action, :before, name, options.merge(:prepend => true)) + end # end + end # end # Skip a before, after or around filter. See _insert_callbacks # for details on the allowed parameters. - def skip_#{filter}_filter(*names, &blk) - _insert_callbacks(names, blk) do |name, options| - skip_callback(:process_action, :#{filter}, name, options) - end - end + def skip_#{filter}_filter(*names, &blk) # def skip_before_filter(*names, &blk) + _insert_callbacks(names, blk) do |name, options| # _insert_callbacks(names, blk) do |name, options| + skip_callback(:process_action, :#{filter}, name, options) # skip_callback(:process_action, :before, name, options) + end # end + end # end # *_filter is the same as append_*_filter - alias_method :append_#{filter}_filter, :#{filter}_filter + alias_method :append_#{filter}_filter, :#{filter}_filter # alias_method :append_before_filter, :before_filter RUBY_EVAL end end diff --git a/actionpack/lib/abstract_controller/helpers.rb b/actionpack/lib/abstract_controller/helpers.rb index dc9778a416..77cc4c07d9 100644 --- a/actionpack/lib/abstract_controller/helpers.rb +++ b/actionpack/lib/abstract_controller/helpers.rb @@ -67,7 +67,7 @@ module AbstractController # 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 + # 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 diff --git a/actionpack/lib/abstract_controller/layouts.rb b/actionpack/lib/abstract_controller/layouts.rb index 8f73e244d7..10aa34c76b 100644 --- a/actionpack/lib/abstract_controller/layouts.rb +++ b/actionpack/lib/abstract_controller/layouts.rb @@ -81,11 +81,12 @@ module AbstractController # 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. - # - # The TellerController uses +teller.html.erb+, and TillController inherits that layout and - # uses it as well. + # In these examples: + # * The InformationController uses the "bank_standard" layout, inherited from BankController. + # * The TellerController follows convention and uses +app/views/layouts/teller.html.erb+. + # * The TillController inherits the layout from TellerController and uses +teller.html.erb+ as well. + # * The VaultController chooses a layout dynamically by calling the <tt>access_level_layout</tt> method. + # * The EmployeeController does not use a layout at all. # # == Types of layouts # @@ -138,8 +139,8 @@ module AbstractController # # 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. + # This will assign "weblog_standard" as the WeblogController's layout for all actions except for the +rss+ action, which will + # be rendered directly, without wrapping 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>. @@ -158,7 +159,7 @@ module AbstractController # end # end # - # This will render the help action with the "help" layout instead of the controller-wide "weblog_standard" layout. + # This will override the controller-wide "weblog_standard" layout, and will render the help action with the "help" layout instead. module Layouts extend ActiveSupport::Concern @@ -166,6 +167,7 @@ module AbstractController included do class_attribute :_layout_conditions + remove_possible_method :_layout_conditions delegate :_layout_conditions, :to => :'self.class' self._layout_conditions = {} _write_layout_method diff --git a/actionpack/lib/abstract_controller/rendering.rb b/actionpack/lib/abstract_controller/rendering.rb index ab2c532859..41fdc11196 100644 --- a/actionpack/lib/abstract_controller/rendering.rb +++ b/actionpack/lib/abstract_controller/rendering.rb @@ -120,8 +120,6 @@ module AbstractController view_renderer.render(view_context, options) end - private - DEFAULT_PROTECTED_INSTANCE_VARIABLES = %w( @_action_name @_response_body @_formats @_prefixes @_config @_view_context_class @_view_renderer @_lookup_context @@ -139,6 +137,8 @@ module AbstractController hash end + private + # Normalize args and options. # :api: private def _normalize_render(*args, &block) diff --git a/actionpack/lib/abstract_controller/url_for.rb b/actionpack/lib/abstract_controller/url_for.rb index e5d5bef6b4..e4261068d8 100644 --- a/actionpack/lib/abstract_controller/url_for.rb +++ b/actionpack/lib/abstract_controller/url_for.rb @@ -1,3 +1,9 @@ +# Includes +url_for+ into the host class (e.g. an abstract controller or mailer). The class +# has to provide a +RouteSet+ by implementing the <tt>_routes</tt> methods. Otherwise, an +# exception will be raised. +# +# Note that this module is completely decoupled from HTTP - the only requirement is a valid +# <tt>_routes</tt> implementation. module AbstractController module UrlFor extend ActiveSupport::Concern diff --git a/actionpack/lib/abstract_controller/view_paths.rb b/actionpack/lib/abstract_controller/view_paths.rb index 6b7aae8c74..e8394447a7 100644 --- a/actionpack/lib/abstract_controller/view_paths.rb +++ b/actionpack/lib/abstract_controller/view_paths.rb @@ -1,3 +1,5 @@ +require 'action_view/base' + module AbstractController module ViewPaths extend ActiveSupport::Concern @@ -63,7 +65,7 @@ module AbstractController # the default view path. You may also provide a custom view path # (see ActionView::PathSet for more information) def append_view_path(path) - self.view_paths = view_paths.dup + Array(path) + self._view_paths = view_paths + Array(path) end # Prepend a path to the list of view paths for this controller. @@ -73,7 +75,7 @@ module AbstractController # the default view path. You may also provide a custom view path # (see ActionView::PathSet for more information) def prepend_view_path(path) - self.view_paths = Array(path) + view_paths.dup + self._view_paths = ActionView::PathSet.new(Array(path) + view_paths) end # A list of all of the default view paths for this controller. @@ -87,8 +89,7 @@ module AbstractController # * <tt>paths</tt> - If a PathSet is provided, use that; # otherwise, process the parameter into a PathSet. def view_paths=(paths) - self._view_paths = ActionView::Base.process_view_paths(paths) - self._view_paths.freeze + self._view_paths = ActionView::PathSet.new(Array.wrap(paths)) end end end diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb index eba5e9377b..f4eaa2fd1b 100644 --- a/actionpack/lib/action_controller.rb +++ b/actionpack/lib/action_controller.rb @@ -37,30 +37,16 @@ module ActionController autoload :UrlFor end - autoload :Integration, 'action_controller/deprecated/integration_test' - autoload :IntegrationTest, 'action_controller/deprecated/integration_test' - autoload :PerformanceTest, 'action_controller/deprecated/performance_test' - autoload :UrlWriter, 'action_controller/deprecated' - autoload :Routing, 'action_controller/deprecated' - autoload :TestCase, 'action_controller/test_case' + autoload :Integration, 'action_controller/deprecated/integration_test' + autoload :IntegrationTest, 'action_controller/deprecated/integration_test' + autoload :PerformanceTest, 'action_controller/deprecated/performance_test' + autoload :UrlWriter, 'action_controller/deprecated' + autoload :Routing, 'action_controller/deprecated' + autoload :TestCase, 'action_controller/test_case' + autoload :TemplateAssertions, 'action_controller/test_case' eager_autoload do autoload :RecordIdentifier - - # TODO: Don't autoload exceptions, setup explicit - # requires for files that need them - autoload_at "action_controller/metal/exceptions" do - autoload :ActionControllerError - autoload :RenderError - autoload :RoutingError - autoload :MethodNotAllowed - autoload :NotImplemented - autoload :UnknownController - autoload :MissingFile - autoload :RenderError - autoload :SessionOverflowError - autoload :UnknownHttpMethod - end end end diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index c03c77cb4a..da93c988c4 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -31,7 +31,7 @@ module ActionController # "302 Moved" HTTP response that takes the user to the index action. # # These two methods represent the two basic action archetypes used in Action Controllers. Get-and-show and do-and-redirect. - # Most actions are variations of these themes. + # Most actions are variations on these themes. # # == Requests # @@ -63,7 +63,7 @@ module ActionController # # == Sessions # - # Sessions allows you to store objects in between requests. This is useful for objects that are not yet ready to be persisted, + # Sessions allow you to store objects in between requests. This is useful for objects that are not yet ready to be persisted, # such as a Signup object constructed in a multi-paged process, or objects that don't change much and are needed all the time, such # as a User object for a system that requires login. The session should not be used, however, as a cache for objects where it's likely # they could be changed unknowingly. It's usually too much work to keep it all synchronized -- something databases already excel at. @@ -116,8 +116,8 @@ module ActionController # # Title: <%= @post.title %> # - # You don't have to rely on the automated rendering. Especially actions that could result in the rendering of different templates will use - # the manual rendering methods: + # You don't have to rely on the automated rendering. For example, actions that could result in the rendering of different templates + # will use the manual rendering methods: # # def search # @results = Search.find(params[:query]) @@ -132,9 +132,9 @@ module ActionController # # == Redirects # - # Redirects are used to move from one action to another. For example, after a <tt>create</tt> action, which stores a blog entry to a database, - # we might like to show the user the new entry. Because we're following good DRY principles (Don't Repeat Yourself), we're going to reuse (and redirect to) - # a <tt>show</tt> action that we'll assume has already been created. The code might look like this: + # Redirects are used to move from one action to another. For example, after a <tt>create</tt> action, which stores a blog entry to the + # database, we might like to show the user the new entry. Because we're following good DRY principles (Don't Repeat Yourself), we're + # going to reuse (and redirect to) a <tt>show</tt> action that we'll assume has already been created. The code might look like this: # # def create # @entry = Entry.new(params[:entry]) @@ -146,7 +146,9 @@ module ActionController # end # end # - # In this case, after saving our new entry to the database, the user is redirected to the <tt>show</tt> method which is then executed. + # In this case, after saving our new entry to the database, the user is redirected to the <tt>show</tt> method, which is then executed. + # Note that this is an external HTTP-level redirection which will cause the browser to make a second request (a GET to the show action), + # and not some internal re-routing which calls both "create" and then "show" within one request. # # Learn more about <tt>redirect_to</tt> and what options you have in ActionController::Redirecting. # @@ -210,16 +212,16 @@ module ActionController # also include them at the bottom. AbstractController::Callbacks, + # Append rescue at the bottom to wrap as much as possible. + Rescue, + # Add instrumentations hooks at the bottom, to ensure they instrument # all the methods properly. Instrumentation, # Params wrapper should come before instrumentation so they are # properly showed in logs - ParamsWrapper, - - # The same with rescue, append it at the end to wrap as much as possible. - Rescue + ParamsWrapper ] MODULES.each do |mod| diff --git a/actionpack/lib/action_controller/caching/actions.rb b/actionpack/lib/action_controller/caching/actions.rb index 5fc6956266..0031d2701f 100644 --- a/actionpack/lib/action_controller/caching/actions.rb +++ b/actionpack/lib/action_controller/caching/actions.rb @@ -38,9 +38,9 @@ module ActionController #:nodoc: # <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 - # <tt>:cache_path</tt> option. This will be passed directly to - # <tt>ActionCachePath.path_for</tt>. This is handy for actions with + # You can modify the default action cache path by passing a + # <tt>:cache_path</tt> option. This will be passed directly to + # <tt>ActionCachePath.path_for</tt>. This is handy for actions with # multiple possible routes that should be cached differently. If a # block is given, it is called with the current controller instance. # diff --git a/actionpack/lib/action_controller/caching/fragments.rb b/actionpack/lib/action_controller/caching/fragments.rb index 0be04b70a1..abeb49d16f 100644 --- a/actionpack/lib/action_controller/caching/fragments.rb +++ b/actionpack/lib/action_controller/caching/fragments.rb @@ -12,13 +12,13 @@ module ActionController #:nodoc: # # <% cache do %> # All the topics in the system: - # <%= render :partial => "topic", :collection => Topic.find(:all) %> + # <%= render :partial => "topic", :collection => Topic.all %> # <% end %> # # This cache will bind the name of the action that called it, so if # this code was part of the view for the topics/list action, you # would be able to invalidate it using: - # + # # expire_fragment(:controller => "topics", :action => "list") # # This default behavior is limited if you need to cache multiple @@ -109,7 +109,6 @@ module ActionController #:nodoc: def expire_fragment(key, options = nil) return unless cache_configured? key = fragment_cache_key(key) unless key.is_a?(Regexp) - message = nil instrument_fragment_cache :expire_fragment, key do if key.is_a?(Regexp) diff --git a/actionpack/lib/action_controller/caching/pages.rb b/actionpack/lib/action_controller/caching/pages.rb index 8c583c7ce0..957bb7de6b 100644 --- a/actionpack/lib/action_controller/caching/pages.rb +++ b/actionpack/lib/action_controller/caching/pages.rb @@ -16,9 +16,10 @@ module ActionController #:nodoc: # caches_page :show, :new # end # - # This will generate cache files such as <tt>weblog/show/5.html</tt> and <tt>weblog/new.html</tt>, - # which match the URLs used to trigger the dynamic generation. This is how the web server is able - # pick up a cache file when it exists and otherwise let the request pass on to Action Pack to generate it. + # This will generate cache files such as <tt>weblog/show/5.html</tt> and <tt>weblog/new.html</tt>, which match the URLs used + # that would normally trigger dynamic page generation. Page caching works by configuring a web server to first check for the + # existence of files on disk, and to serve them directly when found, without passing the request through to Action Pack. + # This is much faster than handling the full dynamic request in the usual way. # # Expiration of the cache is handled by deleting the cached file, which results in a lazy regeneration approach where the cache # is not restored before another hit is made against it. The API for doing so mimics the options from +url_for+ and friends: @@ -121,7 +122,7 @@ module ActionController #:nodoc: if options.is_a?(Hash) if options[:action].is_a?(Array) - options[:action].dup.each do |action| + options[:action].each do |action| self.class.expire_page(url_for(options.merge(:only_path => true, :action => action))) end else @@ -132,8 +133,8 @@ module ActionController #:nodoc: end end - # Manually cache the +content+ in the key determined by +options+. If no content is provided, the contents of response.body is used - # If no options are provided, the requested url is used. Example: + # Manually cache the +content+ in the key determined by +options+. If no content is provided, the contents of response.body is used. + # If no options are provided, the url of the current request being handled is used. Example: # cache_page "I'm the cached content", :controller => "lists", :action => "show" def cache_page(content = nil, options = nil) return unless self.class.perform_caching && caching_allowed? diff --git a/actionpack/lib/action_controller/caching/sweeping.rb b/actionpack/lib/action_controller/caching/sweeping.rb index e9db0d97b6..49cf70ec21 100644 --- a/actionpack/lib/action_controller/caching/sweeping.rb +++ b/actionpack/lib/action_controller/caching/sweeping.rb @@ -61,6 +61,7 @@ module ActionController #:nodoc: end def after(controller) + self.controller = controller callback(:after) if controller.perform_caching # Clean up, so that the controller can be collected after this request self.controller = nil @@ -87,7 +88,7 @@ module ActionController #:nodoc: end def method_missing(method, *arguments, &block) - return if @controller.nil? + return unless @controller @controller.__send__(method, *arguments, &block) end end diff --git a/actionpack/lib/action_controller/log_subscriber.rb b/actionpack/lib/action_controller/log_subscriber.rb index 8d813a8e38..35e29398e6 100644 --- a/actionpack/lib/action_controller/log_subscriber.rb +++ b/actionpack/lib/action_controller/log_subscriber.rb @@ -10,7 +10,7 @@ module ActionController format = payload[:format] format = format.to_s.upcase if format.is_a?(Symbol) - info " Processing by #{payload[:controller]}##{payload[:action]} as #{format}" + info "Processing by #{payload[:controller]}##{payload[:action]} as #{format}" info " Parameters: #{params.inspect}" unless params.empty? end @@ -20,10 +20,11 @@ module ActionController status = payload[:status] if status.nil? && payload[:exception].present? - status = Rack::Utils.status_code(ActionDispatch::ShowExceptions.rescue_responses[payload[:exception].first]) rescue nil - end + status = Rack::Utils.status_code(ActionDispatch::ShowExceptions.rescue_responses[payload[:exception].first]) rescue nil + end message = "Completed #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]} in %.0fms" % event.duration message << " (#{additions.join(" | ")})" unless additions.blank? + message << "\n" info(message) end @@ -59,4 +60,4 @@ module ActionController end end -ActionController::LogSubscriber.attach_to :action_controller
\ No newline at end of file +ActionController::LogSubscriber.attach_to :action_controller diff --git a/actionpack/lib/action_controller/metal/data_streaming.rb b/actionpack/lib/action_controller/metal/data_streaming.rb index 997bc6e958..5e077dd7bd 100644 --- a/actionpack/lib/action_controller/metal/data_streaming.rb +++ b/actionpack/lib/action_controller/metal/data_streaming.rb @@ -1,4 +1,5 @@ require 'active_support/core_ext/file/path' +require 'action_controller/metal/exceptions' module ActionController #:nodoc: # Methods for sending arbitrary data and for streaming files to the browser, @@ -16,7 +17,7 @@ module ActionController #:nodoc: protected # Sends the file. This uses a server-appropriate method (such as X-Sendfile) # via the Rack::Sendfile middleware. The header to use is set via - # config.action_dispatch.x_sendfile_header, and defaults to "X-Sendfile". + # config.action_dispatch.x_sendfile_header. # Your server can also configure this for you by setting the X-Sendfile-Type header. # # Be careful to sanitize the path parameter if it is coming from a web @@ -26,8 +27,11 @@ module ActionController #:nodoc: # Options: # * <tt>:filename</tt> - suggests a filename for the browser to use. # Defaults to <tt>File.basename(path)</tt>. - # * <tt>:type</tt> - specifies an HTTP content type. Defaults to 'application/octet-stream'. You can specify - # either a string or a symbol for a registered type register with <tt>Mime::Type.register</tt>, for example :json + # * <tt>:type</tt> - specifies an HTTP content type. + # You can specify either a string or a symbol for a registered type register with + # <tt>Mime::Type.register</tt>, for example :json + # If omitted, type will be guessed from the file extension specified in <tt>:filename</tt>. + # If no content type is registered for the extension, default type 'application/octet-stream' will be used. # * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded. # Valid values are 'inline' and 'attachment' (default). # * <tt>:status</tt> - specifies the status code to send with the response. Defaults to '200 OK'. @@ -37,7 +41,7 @@ module ActionController #:nodoc: # # The default Content-Type and Content-Disposition headers are # set to download arbitrary binary files in as many browsers as - # possible. IE versions 4, 5, 5.5, and 6 are all known to have + # possible. IE versions 4, 5, 5.5, and 6 are all known to have # a variety of quirks (especially when downloading over SSL). # # Simple download: @@ -58,8 +62,8 @@ module ActionController #:nodoc: # # Also be aware that the document may be cached by proxies and browsers. # The Pragma and Cache-Control headers declare how the file may be cached - # by intermediaries. They default to require clients to validate with - # the server before releasing cached responses. See + # by intermediaries. They default to require clients to validate with + # the server before releasing cached responses. See # http://www.mnot.net/cache_docs/ for an overview of web caching and # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9 # for the Cache-Control header spec. @@ -84,6 +88,8 @@ module ActionController #:nodoc: # * <tt>:filename</tt> - suggests a filename for the browser to use. # * <tt>:type</tt> - specifies an HTTP content type. Defaults to 'application/octet-stream'. You can specify # either a string or a symbol for a registered type register with <tt>Mime::Type.register</tt>, for example :json + # If omitted, type will be guessed from the file extension specified in <tt>:filename</tt>. + # If no content type is registered for the extension, default type 'application/octet-stream' will be used. # * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded. # Valid values are 'inline' and 'attachment' (default). # * <tt>:status</tt> - specifies the status code to send with the response. Defaults to '200 OK'. @@ -108,6 +114,8 @@ module ActionController #:nodoc: private def send_file_headers!(options) + type_provided = options.has_key?(:type) + options.update(DEFAULT_SEND_FILE_OPTIONS.merge(options)) [:type, :disposition].each do |arg| raise ArgumentError, ":#{arg} option required" if options[arg].nil? @@ -123,6 +131,10 @@ module ActionController #:nodoc: raise ArgumentError, "Unknown MIME type #{options[:type]}" unless extension self.content_type = extension else + if !type_provided && options[:filename] + # If type wasn't provided, try guessing from file extension. + content_type = Mime::Type.lookup_by_extension(File.extname(options[:filename]).downcase.tr('.','')) || content_type + end self.content_type = content_type end diff --git a/actionpack/lib/action_controller/metal/force_ssl.rb b/actionpack/lib/action_controller/metal/force_ssl.rb index eb8ed7dfbd..ed693c5967 100644 --- a/actionpack/lib/action_controller/metal/force_ssl.rb +++ b/actionpack/lib/action_controller/metal/force_ssl.rb @@ -1,12 +1,12 @@ module ActionController - # This module provides a method which will redirects browser to use HTTPS + # This module provides a method which will redirect browser to use HTTPS # protocol. This will ensure that user's sensitive information will be # transferred safely over the internet. You _should_ always force browser # to use HTTPS when you're transferring sensitive information such as # user authentication, account information, or credit card information. # - # Note that if you really concern about your application safety, you might - # consider using +config.force_ssl+ in your configuration config file instead. + # Note that if you are really concerned about your application security, + # you might consider using +config.force_ssl+ in your config file instead. # That will ensure all the data transferred via HTTPS protocol and prevent # user from getting session hijacked when accessing the site under unsecured # HTTP protocol. diff --git a/actionpack/lib/action_controller/metal/helpers.rb b/actionpack/lib/action_controller/metal/helpers.rb index 75757db564..bd515bba82 100644 --- a/actionpack/lib/action_controller/metal/helpers.rb +++ b/actionpack/lib/action_controller/metal/helpers.rb @@ -7,9 +7,12 @@ module ActionController # by default. # # In addition to using the standard template helpers provided, creating custom helpers to - # extract complicated logic or reusable functionality is strongly encouraged. By default, the controller will - # include a helper whose name matches that of the controller, e.g., <tt>MyController</tt> will automatically - # include <tt>MyHelper</tt>. + # extract complicated logic or reusable functionality is strongly encouraged. By default, each controller + # will include all helpers. + # + # In previous versions of \Rails the controller will include a helper whose + # name matches that of the controller, e.g., <tt>MyController</tt> will automatically + # include <tt>MyHelper</tt>. To return old behavior set +config.action_controller.include_all_helpers+ to +false+. # # Additional helpers can be specified using the +helper+ class method in ActionController::Base or any # controller which inherits from it. @@ -29,7 +32,7 @@ module ActionController # class EventsController < ActionController::Base # helper FormattedTimeHelper # def index - # @events = Event.find(:all) + # @events = Event.all # end # end # diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb index 1d6df89007..264806cd36 100644 --- a/actionpack/lib/action_controller/metal/http_authentication.rb +++ b/actionpack/lib/action_controller/metal/http_authentication.rb @@ -106,7 +106,7 @@ module ActionController module ControllerMethods extend ActiveSupport::Concern - + module ClassMethods def http_basic_authenticate_with(options = {}) before_filter(options.except(:name, :password, :realm)) do @@ -116,7 +116,7 @@ module ActionController end end end - + def authenticate_or_request_with_http_basic(realm = "Application", &login_procedure) authenticate_with_http_basic(&login_procedure) || request_http_basic_authentication(realm) end @@ -145,7 +145,7 @@ module ActionController end def encode_credentials(user_name, password) - "Basic #{ActiveSupport::Base64.encode64("#{user_name}:#{password}")}" + "Basic #{ActiveSupport::Base64.encode64s("#{user_name}:#{password}")}" end def authentication_request(controller, realm) @@ -400,7 +400,7 @@ module ActionController # the present token and options. # # controller - ActionController::Base instance for the current request. - # login_procedure - Proc to call if a token is present. The Proc should + # login_procedure - Proc to call if a token is present. The Proc should # take 2 arguments: # authenticate(controller) { |token, options| ... } # @@ -413,7 +413,7 @@ module ActionController end end - # Parses the token and options out of the token authorization header. If + # Parses the token and options out of the token authorization header. If # the header looks like this: # Authorization: Token token="abc", nonce="def" # Then the returned token is "abc", and the options is {:nonce => "def"} @@ -423,7 +423,7 @@ module ActionController # Returns an Array of [String, Hash] if a token is present. # Returns nil if no token is found. def token_and_options(request) - if header = request.authorization.to_s[/^Token (.*)/] + if request.authorization.to_s[/^Token (.*)/] values = Hash[$1.split(',').map do |value| value.strip! # remove any spaces between commas and values key, value = value.split(/\=\"?/) # split key=value pairs diff --git a/actionpack/lib/action_controller/metal/instrumentation.rb b/actionpack/lib/action_controller/metal/instrumentation.rb index 16cbbce2fb..777a0ab343 100644 --- a/actionpack/lib/action_controller/metal/instrumentation.rb +++ b/actionpack/lib/action_controller/metal/instrumentation.rb @@ -19,7 +19,7 @@ module ActionController :controller => self.class.name, :action => self.action_name, :params => request.filtered_parameters, - :format => request.format.ref, + :format => request.format.try(:ref), :method => request.method, :path => (request.fullpath rescue "unknown") } @@ -58,8 +58,8 @@ module ActionController def redirect_to(*args) ActiveSupport::Notifications.instrument("redirect_to.action_controller") do |payload| result = super - payload[:status] = self.status - payload[:location] = self.location + payload[:status] = response.status + payload[:location] = response.location result end end @@ -97,4 +97,4 @@ module ActionController end end end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_controller/metal/params_wrapper.rb b/actionpack/lib/action_controller/metal/params_wrapper.rb index 9b27bb8b91..e0d8e1c992 100644 --- a/actionpack/lib/action_controller/metal/params_wrapper.rb +++ b/actionpack/lib/action_controller/metal/params_wrapper.rb @@ -2,45 +2,45 @@ require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/hash/slice' require 'active_support/core_ext/hash/except' require 'active_support/core_ext/array/wrap' +require 'active_support/core_ext/module/anonymous' require 'action_dispatch/http/mime_types' module ActionController - # Wraps parameters hash into nested hash. This will allow client to submit - # POST request without having to specify a root element in it. + # Wraps the parameters hash into a nested hash. This will allow clients to submit + # POST requests without having to specify any root elements. # - # By default this functionality won't be enabled. You can enable - # it globally by setting +ActionController::Base.wrap_parameters+: - # - # ActionController::Base.wrap_parameters = [:json] + # This functionality is enabled in +config/initializers/wrap_parameters.rb+ + # and can be customized. If you are upgrading to \Rails 3.1, this file will + # need to be created for the functionality to be enabled. # # You could also turn it on per controller by setting the format array to - # non-empty array: + # a non-empty array: # # class UsersController < ApplicationController # wrap_parameters :format => [:json, :xml] # end # - # If you enable +ParamsWrapper+ for +:json+ format. Instead of having to + # If you enable +ParamsWrapper+ for +:json+ format, instead of having to # send JSON parameters like this: # # {"user": {"name": "Konata"}} # - # You can now just send a parameters like this: + # You can send parameters like this: # # {"name": "Konata"} # - # And it will be wrapped into a nested hash with the key name matching + # And it will be wrapped into a nested hash with the key name matching the # controller's name. For example, if you're posting to +UsersController+, # your new +params+ hash will look like this: # # {"name" => "Konata", "user" => {"name" => "Konata"}} # # You can also specify the key in which the parameters should be wrapped to, - # and also the list of attributes it should wrap by using either +:only+ or - # +:except+ options like this: + # and also the list of attributes it should wrap by using either +:include+ or + # +:exclude+ options like this: # # class UsersController < ApplicationController - # wrap_parameters :person, :only => [:username, :password] + # wrap_parameters :person, :include => [:username, :password] # end # # If you're going to pass the parameters to an +ActiveModel+ object (such as @@ -52,7 +52,7 @@ module ActionController # wrap_parameters Person # end # - # You still could pass +:only+ and +:except+ to set the list of attributes + # You still could pass +:include+ and +:exclude+ to set the list of attributes # you want to wrap. # # By default, if you don't specify the key in which the parameters would be @@ -63,7 +63,7 @@ module ActionController # end # # will try to check if +Admin::User+ or +User+ model exists, and use it to - # determine the wrapper key respectively. If both of the model doesn't exists, + # determine the wrapper key respectively. If both models don't exist, # it will then fallback to use +user+ as the key. module ParamsWrapper extend ActiveSupport::Concern @@ -72,7 +72,7 @@ module ActionController included do class_attribute :_wrapper_options - self._wrapper_options = {:format => []} + self._wrapper_options = { :format => [] } end module ClassMethods @@ -81,27 +81,27 @@ module ActionController # # ==== Examples # wrap_parameters :format => :xml - # # enables the parmeter wrapper for XML format + # # enables the parameter wrapper for XML format # # wrap_parameters :person # # wraps parameters into +params[:person]+ hash # # wrap_parameters Person - # # wraps parameters by determine the wrapper key from Person class + # # wraps parameters by determining the wrapper key from Person class # (+person+, in this case) and the list of attribute names # - # wrap_parameters :only => [:username, :title] + # wrap_parameters :include => [:username, :title] # # wraps only +:username+ and +:title+ attributes from parameters. # # wrap_parameters false - # # disable parameters wrapping for this controller altogether. + # # disables parameters wrapping for this controller altogether. # # ==== Options # * <tt>:format</tt> - The list of formats in which the parameters wrapper # will be enabled. - # * <tt>:only</tt> - The list of attribute names which parameters wrapper + # * <tt>:include</tt> - The list of attribute names which parameters wrapper # will wrap into a nested hash. - # * <tt>:except</tt> - The list of attribute names which parameters wrapper + # * <tt>:exclude</tt> - The list of attribute names which parameters wrapper # will exclude from a nested hash. def wrap_parameters(name_or_model_or_options, options = {}) model = nil @@ -140,18 +140,17 @@ module ActionController # This method also does namespace lookup. Foo::Bar::UsersController will # try to find Foo::Bar::User, Foo::User and finally User. def _default_wrap_model #:nodoc: + return nil if self.anonymous? model_name = self.name.sub(/Controller$/, '').singularize begin - model_klass = model_name.constantize - rescue NameError, ArgumentError => e - if e.message =~ /is not missing constant|uninitialized constant #{model_name}/ + if model_klass = model_name.safe_constantize + model_klass + else namespaces = model_name.split("::") namespaces.delete_at(-2) break if namespaces.last == model_name model_name = namespaces.join("::") - else - raise end end until model_klass @@ -161,22 +160,22 @@ module ActionController def _set_wrapper_defaults(options, model=nil) options = options.dup - unless options[:only] || options[:except] + unless options[:include] || options[:exclude] model ||= _default_wrap_model - if model.respond_to?(:column_names) - options[:only] = model.column_names + if model.respond_to?(:attribute_names) && model.attribute_names.present? + options[:include] = model.attribute_names end end - unless options[:name] + unless options[:name] || self.anonymous? model ||= _default_wrap_model options[:name] = model ? model.to_s.demodulize.underscore : controller_name.singularize end - options[:only] = Array.wrap(options[:only]).collect(&:to_s) if options[:only] - options[:except] = Array.wrap(options[:except]).collect(&:to_s) if options[:except] - options[:format] = Array.wrap(options[:format]) + options[:include] = Array.wrap(options[:include]).collect(&:to_s) if options[:include] + options[:exclude] = Array.wrap(options[:exclude]).collect(&:to_s) if options[:exclude] + options[:format] = Array.wrap(options[:format]) self._wrapper_options = options end @@ -213,11 +212,11 @@ module ActionController # Returns the list of parameters which will be selected for wrapped. def _wrap_parameters(parameters) - value = if only = _wrapper_options[:only] - parameters.slice(*only) + value = if include_only = _wrapper_options[:include] + parameters.slice(*include_only) else - except = _wrapper_options[:except] || [] - parameters.except(*(except + EXCLUDE_PARAMETERS)) + exclude = _wrapper_options[:exclude] || [] + parameters.except(*(exclude + EXCLUDE_PARAMETERS)) end { _wrapper_key => value } @@ -226,7 +225,7 @@ module ActionController # Checks if we should perform parameters wrapping. def _wrapper_enabled? ref = request.content_mime_type.try(:ref) - _wrapper_formats.include?(ref) && !request.request_parameters[_wrapper_key] + _wrapper_formats.include?(ref) && _wrapper_key && !request.request_parameters[_wrapper_key] end end end diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb index 55c650df6c..f2dfb3833b 100644 --- a/actionpack/lib/action_controller/metal/redirecting.rb +++ b/actionpack/lib/action_controller/metal/redirecting.rb @@ -43,8 +43,9 @@ module ActionController # # The status code can either be a standard {HTTP Status code}[http://www.iana.org/assignments/http-status-codes] as an # integer, or a symbol representing the downcased, underscored and symbolized description. + # Note that the status code must be a 3xx HTTP code, or redirection will not occur. # - # It is also possible to assign a flash message as part of the redirection. There are two special accessors for commonly used the flash names + # It is also possible to assign a flash message as part of the redirection. There are two special accessors for the commonly used flash names # +alert+ and +notice+ as well as a general purpose +flash+ bucket. # # Examples: @@ -56,7 +57,7 @@ module ActionController # When using <tt>redirect_to :back</tt>, if there is no referrer, RedirectBackError will be raised. You may specify some fallback # behavior for this case by rescuing RedirectBackError. def redirect_to(options = {}, response_status = {}) #:doc: - raise ActionControllerError.new("Cannot redirect to nil!") if options.nil? + raise ActionControllerError.new("Cannot redirect to nil!") unless options raise AbstractController::DoubleRenderError if response_body self.status = _extract_redirect_to_status(options, response_status) diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index 13044a7450..bc22e39efb 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -1,4 +1,5 @@ require 'active_support/core_ext/class/attribute' +require 'action_controller/metal/exceptions' module ActionController #:nodoc: class InvalidAuthenticityToken < ActionControllerError #:nodoc: @@ -7,17 +8,16 @@ module ActionController #:nodoc: # Controller actions are protected from Cross-Site Request Forgery (CSRF) attacks # by including a token in the rendered html for your application. This token is # stored as a random string in the session, to which an attacker does not have - # access. When a request reaches your application, \Rails then verifies the received - # token with the token in the session. Only HTML and javascript requests are checked, + # access. When a request reaches your application, \Rails verifies the received + # token with the token in the session. Only HTML and JavaScript requests are checked, # so this will not protect your XML API (presumably you'll have a different # authentication scheme there anyway). Also, GET requests are not protected as these # should be idempotent. # # CSRF protection is turned on with the <tt>protect_from_forgery</tt> method, - # which will check the token and raise an ActionController::InvalidAuthenticityToken - # if it doesn't match what was expected. A call to this method is generated for new - # \Rails applications by default. You can customize the error message by editing - # public/422.html. + # which checks the token and resets the session if it doesn't match what was expected. + # A call to this method is generated for new \Rails applications by default. + # You can customize the error message by editing public/422.html. # # The token parameter is named <tt>authenticity_token</tt> by default. The name and # value of this token must be added to every layout that renders forms by including @@ -63,7 +63,7 @@ module ActionController #:nodoc: # # Valid Options: # - # * <tt>:only/:except</tt> - Passed to the <tt>before_filter</tt> call. Set which actions are verified. + # * <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 prepend_before_filter :verify_authenticity_token, options @@ -71,19 +71,21 @@ module ActionController #:nodoc: end protected - # The actual before_filter that is used. Modify this to change how you handle unverified requests. + # The actual before_filter that is used. Modify this to change how you handle unverified requests. def verify_authenticity_token unless verified_request? - logger.debug "WARNING: Can't verify CSRF token authenticity" if logger + logger.warn "WARNING: Can't verify CSRF token authenticity" if logger handle_unverified_request end end + # This is the method that defines the application behavior when a request is found to be unverified. + # By default, \Rails resets the session when it finds an unverified request. def handle_unverified_request reset_session end - # Returns true or false if a request is verified. Checks: + # Returns true or false if a request is verified. Checks: # # * is it a GET request? Gets should be safe and idempotent # * Does the form_authenticity_token match the given token value from the params? @@ -96,7 +98,7 @@ module ActionController #:nodoc: # Sets the token value for the current session. def form_authenticity_token - session[:_csrf_token] ||= ActiveSupport::SecureRandom.base64(32) + session[:_csrf_token] ||= SecureRandom.base64(32) end # The form's authenticity parameter. Override to provide your own. diff --git a/actionpack/lib/action_controller/metal/responder.rb b/actionpack/lib/action_controller/metal/responder.rb index ebadb29ea7..3794e277f6 100644 --- a/actionpack/lib/action_controller/metal/responder.rb +++ b/actionpack/lib/action_controller/metal/responder.rb @@ -9,7 +9,7 @@ module ActionController #:nodoc: # respond_to :html, :xml, :json # # def index - # @people = Person.find(:all) + # @people = Person.all # respond_with(@people) # end # end @@ -162,6 +162,11 @@ module ActionController #:nodoc: navigation_behavior(e) end + # to_js simply tries to render a template. If no template is found, raises the error. + def to_js + default_render + end + # All other formats follow the procedure below. First we try to render a # template, if the template is not available, we verify if the resource # responds to :to_format and display it. diff --git a/actionpack/lib/action_controller/metal/streaming.rb b/actionpack/lib/action_controller/metal/streaming.rb index 3892a12407..5fe5334458 100644 --- a/actionpack/lib/action_controller/metal/streaming.rb +++ b/actionpack/lib/action_controller/metal/streaming.rb @@ -24,20 +24,8 @@ module ActionController #:nodoc: # # == Examples # - # Streaming can be added to a controller easily, all you need to do is - # call +stream+ in the controller class: - # - # class PostsController - # stream - # end - # - # The +stream+ method accepts the same options as +before_filter+ and friends: - # - # class PostsController - # stream :only => :index - # end - # - # You can also selectively turn on streaming for specific actions: + # Streaming can be added to a given template easily, all you need to do is + # to pass the :stream option. # # class PostsController # def index @@ -72,6 +60,9 @@ module ActionController #:nodoc: # render :stream => true # end # + # Notice that :stream only works with templates. Rendering :json + # or :xml with :stream won't work. + # # == Communication between layout and template # # When streaming, rendering happens top-down instead of inside-out. @@ -209,33 +200,9 @@ module ActionController #:nodoc: extend ActiveSupport::Concern include AbstractController::Rendering - attr_internal :stream - - module ClassMethods - # Render streaming templates. It accepts :only, :except, :if and :unless as options - # to specify when to stream, as in ActionController filters. - def stream(options={}) - if defined?(Fiber) - before_filter :_stream_filter, options - else - raise "You cannot use streaming if Fiber is not available." - end - end - end protected - # Mark following render calls as streaming. - def _stream_filter #:nodoc: - self.stream = true - end - - # Consider the stream option when normalazing options. - def _normalize_options(options) #:nodoc: - super - options[:stream] = self.stream unless options.key?(:stream) - end - # Set proper cache control and transfer encoding when streaming def _process_options(options) #:nodoc: super @@ -260,4 +227,3 @@ module ActionController #:nodoc: end end end -
\ No newline at end of file diff --git a/actionpack/lib/action_controller/metal/testing.rb b/actionpack/lib/action_controller/metal/testing.rb index f4efeb33ba..d1813ee745 100644 --- a/actionpack/lib/action_controller/metal/testing.rb +++ b/actionpack/lib/action_controller/metal/testing.rb @@ -4,6 +4,11 @@ module ActionController include RackDelegation + def recycle! + @_url_options = nil + end + + # TODO: Clean this up def process_with_new_base_test(request, response) @_request = request diff --git a/actionpack/lib/action_controller/metal/url_for.rb b/actionpack/lib/action_controller/metal/url_for.rb index 6fc0cf1fb8..0b40b1fc4c 100644 --- a/actionpack/lib/action_controller/metal/url_for.rb +++ b/actionpack/lib/action_controller/metal/url_for.rb @@ -1,3 +1,24 @@ +# Includes +url_for+ into the host class. The class has to provide a +RouteSet+ by implementing +# the <tt>_routes</tt> method. Otherwise, an exception will be raised. +# +# In addition to <tt>AbstractController::UrlFor</tt>, this module accesses the HTTP layer to define +# url options like the +host+. In order to do so, this module requires the host class +# to implement +env+ and +request+, which need to be a Rack-compatible. +# +# Example: +# +# class RootUrl +# include ActionController::UrlFor +# include Rails.application.routes.url_helpers +# +# delegate :env, :request, :to => :controller +# +# def initialize(controller) +# @controller = controller +# @url = root_path # named route from the application. +# end +# end +# module ActionController module UrlFor extend ActiveSupport::Concern diff --git a/actionpack/lib/action_controller/railtie.rb b/actionpack/lib/action_controller/railtie.rb index d2ba052c8d..f0c29825ba 100644 --- a/actionpack/lib/action_controller/railtie.rb +++ b/actionpack/lib/action_controller/railtie.rb @@ -4,7 +4,6 @@ require "action_dispatch/railtie" require "action_view/railtie" require "abstract_controller/railties/routes_helpers" require "action_controller/railties/paths" -require "sprockets/railtie" module ActionController class Railtie < Rails::Railtie diff --git a/actionpack/lib/action_controller/record_identifier.rb b/actionpack/lib/action_controller/record_identifier.rb index 2def78b51a..2036442cfe 100644 --- a/actionpack/lib/action_controller/record_identifier.rb +++ b/actionpack/lib/action_controller/record_identifier.rb @@ -40,7 +40,7 @@ module ActionController # dom_class(post, :edit) # => "edit_post" # dom_class(Person, :edit) # => "edit_person" def dom_class(record_or_class, prefix = nil) - singular = ActiveModel::Naming.singular(record_or_class) + singular = ActiveModel::Naming.param_key(record_or_class) prefix ? "#{prefix}#{JOIN}#{singular}" : singular end @@ -67,7 +67,7 @@ module ActionController # This can be overwritten to customize the default generated string representation if desired. # If you need to read back a key from a dom_id in order to query for the underlying database record, # you should write a helper like 'person_record_from_dom_id' that will extract the key either based - # on the default implementation (which just joins all key attributes with '-') or on your own + # on the default implementation (which just joins all key attributes with '_') or on your own # overwritten version of the method. By default, this implementation passes the key string through a # method that replaces all characters that are invalid inside DOM ids, with valid ones. You need to # make sure yourself that your dom ids are valid, in case you overwrite this method. diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index 0085f542aa..6913c1ef4a 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -2,6 +2,7 @@ require 'rack/session/abstract/id' require 'active_support/core_ext/object/blank' require 'active_support/core_ext/object/to_query' require 'active_support/core_ext/class/attribute' +require 'active_support/core_ext/module/anonymous' module ActionController module TemplateAssertions @@ -78,10 +79,10 @@ module ActionController "expecting <?> but rendering with <?>", options, rendered.keys.join(', ')) assert_block(msg) do - if options.nil? - @templates.blank? - else + if options rendered.any? { |t,num| t.match(options) } + else + @templates.blank? end end when Hash @@ -129,7 +130,7 @@ module ActionController super self.session = TestSession.new - self.session_options = TestSession::DEFAULT_OPTIONS.merge(:id => ActiveSupport::SecureRandom.hex(16)) + self.session_options = TestSession::DEFAULT_OPTIONS.merge(:id => SecureRandom.hex(16)) end class Result < ::Array #:nodoc: @@ -174,17 +175,21 @@ module ActionController end def recycle! - write_cookies! - @env.delete('HTTP_COOKIE') if @cookies.blank? - @env.delete('action_dispatch.cookies') - @cookies = nil @formats = nil @env.delete_if { |k, v| k =~ /^(action_dispatch|rack)\.request/ } @env.delete_if { |k, v| k =~ /^action_dispatch\.rescue/ } @symbolized_path_params = nil @method = @request_method = nil - @fullpath = @ip = @remote_ip = nil + @fullpath = @ip = @remote_ip = @protocol = nil @env['action_dispatch.request.query_parameters'] = {} + @set_cookies ||= {} + @set_cookies.update(Hash[cookie_jar.instance_variable_get("@set_cookies").map{ |k,o| [k,o[:value]] }]) + deleted_cookies = cookie_jar.instance_variable_get("@delete_cookies") + @set_cookies.reject!{ |k,v| deleted_cookies.include?(k) } + cookie_jar.update(rack_cookies) + cookie_jar.update(cookies) + cookie_jar.update(@set_cookies) + cookie_jar.recycle! end end @@ -205,7 +210,7 @@ module ActionController DEFAULT_OPTIONS = Rack::Session::Abstract::ID::DEFAULT_OPTIONS def initialize(session = {}) - @env, @by = nil, nil + super(nil, nil) replace(session.stringify_keys) @loaded = true end @@ -300,18 +305,17 @@ module ActionController # For redirects within the same controller, you can even call follow_redirect and the redirect will be followed, triggering another # action call which can then be asserted against. # - # == Manipulating the request collections + # == Manipulating session and cookie variables # - # The collections described above link to the response, so you can test if what the actions were expected to do happened. But - # sometimes you also want to manipulate these collections in the incoming request. This is really only relevant for sessions - # and cookies, though. For sessions, you just do: + # Sometimes you need to set up the session and cookie variables for a test. + # To do this just assign a value to the session or cookie collection: # - # @request.session[:key] = "value" - # @request.cookies[:key] = "value" + # session[:key] = "value" + # cookies[:key] = "value" # - # To clear the cookies for a test just clear the request's cookies hash: + # To clear the cookies for a test just clear the cookie collection: # - # @request.cookies.clear + # cookies.clear # # == \Testing named routes # @@ -329,9 +333,21 @@ module ActionController module ClassMethods # Sets the controller class name. Useful if the name can't be inferred from test class. - # Expects +controller_class+ as a constant. Example: <tt>tests WidgetController</tt>. + # Normalizes +controller_class+ before using. Examples: + # + # tests WidgetController + # tests :widget + # tests 'widget' + # def tests(controller_class) - self.controller_class = controller_class + case controller_class + when String, Symbol + self.controller_class = "#{controller_class.to_s.underscore}_controller".camelize.constantize + when Class + self.controller_class = controller_class + else + raise ArgumentError, "controller class must be a String, Symbol, or Class" + end end def controller_class=(new_class) @@ -348,9 +364,7 @@ module ActionController end def determine_default_controller_class(name) - name.sub(/Test$/, '').constantize - rescue NameError - nil + name.sub(/Test$/, '').safe_constantize end def prepare_controller_class(new_class) @@ -394,7 +408,24 @@ module ActionController end alias xhr :xml_http_request + def paramify_values(hash_or_array_or_value) + case hash_or_array_or_value + when Hash + Hash[hash_or_array_or_value.map{|key, value| [key, paramify_values(value)] }] + when Array + hash_or_array_or_value.map {|i| paramify_values(i)} + when Rack::Test::UploadedFile + hash_or_array_or_value + else + hash_or_array_or_value.to_param + end + end + def process(action, parameters = nil, session = nil, flash = nil, http_method = 'GET') + # Ensure that numbers and symbols passed as params are converted to + # proper params, as is the case when engaging rack. + parameters = paramify_values(parameters) + # Sanity check for required instance variables so we can give an # understandable error message. %w(@routes @controller @request @response).each do |iv_name| @@ -413,7 +444,11 @@ module ActionController @request.env['REQUEST_METHOD'] = http_method parameters ||= {} - @request.assign_parameters(@routes, @controller.class.name.underscore.sub(/_controller$/, ''), action.to_s, parameters) + controller_class_name = @controller.class.anonymous? ? + "anonymous_controller" : + @controller.class.name.underscore.sub(/_controller$/, '') + + @request.assign_parameters(@routes, controller_class_name, action.to_s, parameters) @request.session = ActionController::TestSession.new(session) if session @request.session["flash"] = @request.flash.update(flash || {}) @@ -423,10 +458,10 @@ module ActionController @controller.params.merge!(parameters) build_request_uri(action, parameters) @controller.class.class_eval { include Testing } + @controller.recycle! @controller.process_with_new_base_test(@request, @response) @assigns = @controller.respond_to?(:view_assigns) ? @controller.view_assigns : {} @request.session.delete('flash') if @request.session['flash'].blank? - @request.cookies.merge!(@response.cookies) @response end diff --git a/actionpack/lib/action_controller/vendor/html-scanner/html/node.rb b/actionpack/lib/action_controller/vendor/html-scanner/html/node.rb index 22b3243104..4e1f016431 100644 --- a/actionpack/lib/action_controller/vendor/html-scanner/html/node.rb +++ b/actionpack/lib/action_controller/vendor/html-scanner/html/node.rb @@ -156,7 +156,7 @@ module HTML #:nodoc: end closing = ( scanner.scan(/\//) ? :close : nil ) - return Text.new(parent, line, pos, content) unless name = scanner.scan(/[\w:-]+/) + return Text.new(parent, line, pos, content) unless name = scanner.scan(/[^\s!>\/]+/) name.downcase! unless closing diff --git a/actionpack/lib/action_controller/vendor/html-scanner/html/sanitizer.rb b/actionpack/lib/action_controller/vendor/html-scanner/html/sanitizer.rb index 91a97c02ff..af06bffa16 100644 --- a/actionpack/lib/action_controller/vendor/html-scanner/html/sanitizer.rb +++ b/actionpack/lib/action_controller/vendor/html-scanner/html/sanitizer.rb @@ -1,4 +1,5 @@ require 'set' +require 'cgi' require 'active_support/core_ext/class/attribute' module HTML @@ -103,7 +104,7 @@ module HTML # Specifies the default Set of allowed shorthand css properties for the #sanitize and #sanitize_css helpers. self.shorthand_css_properties = Set.new(%w(background border margin padding)) - # Sanitizes a block of css code. Used by #sanitize when it comes across a style attribute + # Sanitizes a block of css code. Used by #sanitize when it comes across a style attribute def sanitize_css(style) # disallow urls style = style.to_s.gsub(/url\s*\(\s*[^\s)]+?\s*\)\s*/, ' ') diff --git a/actionpack/lib/action_dispatch/http/headers.rb b/actionpack/lib/action_dispatch/http/headers.rb index 1e43104f0a..505d5560b1 100644 --- a/actionpack/lib/action_dispatch/http/headers.rb +++ b/actionpack/lib/action_dispatch/http/headers.rb @@ -3,9 +3,10 @@ require 'active_support/memoizable' module ActionDispatch module Http class Headers < ::Hash - extend ActiveSupport::Memoizable + @@env_cache = Hash.new { |h,k| h[k] = "HTTP_#{k.upcase.gsub(/-/, '_')}" } def initialize(*args) + if args.size == 1 && args[0].is_a?(Hash) super() update(args[0]) @@ -25,9 +26,8 @@ module ActionDispatch private # Converts a HTTP header name to an environment variable name. def env_name(header_name) - "HTTP_#{header_name.upcase.gsub(/-/, '_')}" + @@env_cache[header_name] end - memoize :env_name end end end diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb index 980c658ab7..5c48a60469 100644 --- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb +++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb @@ -98,7 +98,8 @@ module ActionDispatch BROWSER_LIKE_ACCEPTS = /,\s*\*\/\*|\*\/\*\s*,/ def valid_accept_header - xhr? || (accept && accept !~ BROWSER_LIKE_ACCEPTS) + (xhr? && (accept || content_mime_type)) || + (accept && accept !~ BROWSER_LIKE_ACCEPTS) end def use_accept_header diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb index 7c9ebe7c7b..8a9f9c4315 100644 --- a/actionpack/lib/action_dispatch/http/mime_type.rb +++ b/actionpack/lib/action_dispatch/http/mime_type.rb @@ -47,7 +47,7 @@ module Mime cattr_reader :html_types # These are the content types which browsers can generate without using ajax, flash, etc - # i.e. following a link, getting an image or posting a form. CSRF protection + # i.e. following a link, getting an image or posting a form. CSRF protection # only needs to protect against these types. @@browser_generated_types = Set.new [:html, :url_encoded_form, :multipart_form, :text] cattr_reader :browser_generated_types @@ -246,7 +246,7 @@ module Mime end end - # Returns true if Action Pack should check requests using this Mime Type for possible request forgery. See + # Returns true if Action Pack should check requests using this Mime Type for possible request forgery. See # ActionController::RequestForgeryProtection. def verify_request? @@browser_generated_types.include?(to_sym) @@ -256,6 +256,10 @@ module Mime @@html_types.include?(to_sym) || @string =~ /html/ end + def respond_to?(method, include_private = false) #:nodoc: + super || method.to_s =~ /(\w+)\?$/ + end + private def method_missing(method, *args) if method.to_s =~ /(\w+)\?$/ diff --git a/actionpack/lib/action_dispatch/http/mime_types.rb b/actionpack/lib/action_dispatch/http/mime_types.rb index 68f37d2f65..3da4f91051 100644 --- a/actionpack/lib/action_dispatch/http/mime_types.rb +++ b/actionpack/lib/action_dispatch/http/mime_types.rb @@ -7,6 +7,15 @@ Mime::Type.register "text/javascript", :js, %w( application/javascript applicati Mime::Type.register "text/css", :css Mime::Type.register "text/calendar", :ics Mime::Type.register "text/csv", :csv + +Mime::Type.register "image/png", :png, [], %w(png) +Mime::Type.register "image/jpeg", :jpeg, [], %w(jpg jpeg jpe) +Mime::Type.register "image/gif", :gif, [], %w(gif) +Mime::Type.register "image/bmp", :bmp, [], %w(bmp) +Mime::Type.register "image/tiff", :tiff, [], %w(tif tiff) + +Mime::Type.register "video/mpeg", :mpeg, [], %w(mpg mpeg mpe) + Mime::Type.register "application/xml", :xml, %w( text/xml application/x-xml ) Mime::Type.register "application/rss+xml", :rss Mime::Type.register "application/atom+xml", :atom @@ -19,5 +28,8 @@ Mime::Type.register "application/x-www-form-urlencoded", :url_encoded_form # http://www.json.org/JSONRequest.html Mime::Type.register "application/json", :json, %w( text/x-json application/jsonrequest ) +Mime::Type.register "application/pdf", :pdf, [], %w(pdf) +Mime::Type.register "application/zip", :zip, [], %w(zip) + # Create Mime::ALL but do not add it to the SET. Mime::ALL = Mime::Type.new("*/*", :all, []) diff --git a/actionpack/lib/action_dispatch/http/rack_cache.rb b/actionpack/lib/action_dispatch/http/rack_cache.rb index b5c1435903..cc8edee300 100644 --- a/actionpack/lib/action_dispatch/http/rack_cache.rb +++ b/actionpack/lib/action_dispatch/http/rack_cache.rb @@ -14,11 +14,15 @@ module ActionDispatch end def read(key) - @store.read(key) || [] + if data = @store.read(key) + Marshal.load(data) + else + [] + end end def write(key, value) - @store.write(key, value) + @store.write(key, Marshal.dump(value)) end ::Rack::Cache::MetaStore::RAILS = self diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index ccb866f4f7..37d0a3e0b8 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -2,11 +2,11 @@ require 'tempfile' require 'stringio' require 'strscan' -require 'active_support/core_ext/module/deprecation' require 'active_support/core_ext/hash/indifferent_access' require 'active_support/core_ext/string/access' require 'active_support/inflector' require 'action_dispatch/http/headers' +require 'action_controller/metal/exceptions' module ActionDispatch class Request < Rack::Request @@ -26,12 +26,12 @@ module ActionDispatch HTTP_ACCEPT HTTP_ACCEPT_CHARSET HTTP_ACCEPT_ENCODING HTTP_ACCEPT_LANGUAGE HTTP_CACHE_CONTROL HTTP_FROM HTTP_NEGOTIATE HTTP_PRAGMA ].freeze - + ENV_METHODS.each do |env| class_eval <<-METHOD, __FILE__, __LINE__ + 1 - def #{env.sub(/^HTTP_/n, '').downcase} - @env["#{env}"] - end + def #{env.sub(/^HTTP_/n, '').downcase} # def accept_charset + @env["#{env}"] # @env["HTTP_ACCEPT_CHARSET"] + end # end METHOD end @@ -134,11 +134,6 @@ module ActionDispatch @fullpath ||= super end - def forgery_whitelisted? - get? - end - deprecate :forgery_whitelisted? => "it is just an alias for 'get?' now, update your code" - def media_type content_mime_type.to_s end @@ -172,10 +167,10 @@ module ActionDispatch )\. }x - # Determines originating IP address. REMOTE_ADDR is the standard - # but will fail if the user is behind a proxy. HTTP_CLIENT_IP and/or + # Determines originating IP address. REMOTE_ADDR is the standard + # but will fail if the user is behind a proxy. HTTP_CLIENT_IP and/or # HTTP_X_FORWARDED_FOR are set by proxies so check for these if - # REMOTE_ADDR is a proxy. HTTP_X_FORWARDED_FOR may be a comma- + # REMOTE_ADDR is a proxy. HTTP_X_FORWARDED_FOR may be a comma- # delimited list in the case of multiple chained proxies; the last # address which is not trusted is the originating IP. def remote_ip diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index 3a6b1da4fd..f1e85559a3 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -33,7 +33,8 @@ module ActionDispatch # :nodoc: # end # end class Response - attr_accessor :request, :header, :status + attr_accessor :request, :header + attr_reader :status attr_writer :sending_file alias_method :headers=, :header= @@ -115,32 +116,9 @@ module ActionDispatch # :nodoc: EMPTY = " " - class BodyBuster #:nodoc: - def initialize(response) - @response = response - @body = "" - end - - def bust(body) - body.call(@response, self) - body.close if body.respond_to?(:close) - @body - end - - def write(string) - @body << string.to_s - end - end - def body=(body) @blank = true if body == EMPTY - if body.respond_to?(:call) - ActiveSupport::Deprecation.warn "Setting a Proc or an object that responds to call " \ - "in response_body is no longer supported", caller - body = BodyBuster.new(self).bust(body) - end - # Explicitly check for strings. This is *wrong* theoretically # but if we don't check this, the performance on string bodies # is bad on Ruby 1.8 (because strings responds to each then). diff --git a/actionpack/lib/action_dispatch/http/upload.rb b/actionpack/lib/action_dispatch/http/upload.rb index 37effade4f..94fa747a79 100644 --- a/actionpack/lib/action_dispatch/http/upload.rb +++ b/actionpack/lib/action_dispatch/http/upload.rb @@ -4,31 +4,30 @@ module ActionDispatch attr_accessor :original_filename, :content_type, :tempfile, :headers def initialize(hash) - @original_filename = hash[:filename] + @original_filename = encode_filename(hash[:filename]) @content_type = hash[:type] @headers = hash[:head] @tempfile = hash[:tempfile] raise(ArgumentError, ':tempfile is required') unless @tempfile end - def open - @tempfile.open - end - - def path - @tempfile.path - end - def read(*args) @tempfile.read(*args) end - def rewind - @tempfile.rewind + # Delegate these methods to the tempfile. + [:open, :path, :rewind, :size].each do |method| + class_eval "def #{method}; @tempfile.#{method}; end" end - - def size - @tempfile.size + + private + def encode_filename(filename) + # Encode the filename in the utf8 encoding, unless it is nil or we're in 1.8 + if "ruby".encoding_aware? && filename + filename.force_encoding("UTF-8").encode! + else + filename + end end end diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb index d9c07d6ca3..170c68f3e0 100644 --- a/actionpack/lib/action_dispatch/http/url.rb +++ b/actionpack/lib/action_dispatch/http/url.rb @@ -45,7 +45,7 @@ module ActionDispatch rewritten_url << (options[:trailing_slash] ? path.sub(/\?|\z/) { "/" + $& } : path) rewritten_url << "?#{params.to_query}" unless params.empty? - rewritten_url << "##{Rack::Mount::Utils.escape_uri(options[:anchor].to_param.to_s)}" if options[:anchor] + rewritten_url << "##{Journey::Router::Utils.escape_uri(options[:anchor].to_param.to_s)}" if options[:anchor] rewritten_url end @@ -64,14 +64,16 @@ module ActionDispatch end def host_or_subdomain_and_domain(options) - return options[:host] unless options[:subdomain] || options[:domain] + return options[:host] if options[:subdomain].nil? && options[:domain].nil? tld_length = options[:tld_length] || @@tld_length host = "" - host << (options[:subdomain] || extract_subdomain(options[:host], tld_length)) - host << "." - host << (options[:domain] || extract_domain(options[:host], tld_length)) + unless options[:subdomain] == false + host << (options[:subdomain] || extract_subdomain(options[:host], tld_length)) + host << "." + end + host << (options[:domain] || extract_domain(options[:host], tld_length)) host end end @@ -162,7 +164,7 @@ module ActionDispatch # Returns all the \subdomains as a string, so <tt>"dev.www"</tt> would be # returned for "dev.www.rubyonrails.org". You can specify a different <tt>tld_length</tt>, - # such as 2 to catch <tt>["www"]</tt> instead of <tt>"www.rubyonrails"</tt> + # such as 2 to catch <tt>"www"</tt> instead of <tt>"www.rubyonrails"</tt> # in "www.rubyonrails.co.uk". def subdomain(tld_length = @@tld_length) subdomains(tld_length).join(".") diff --git a/actionpack/lib/action_dispatch/middleware/callbacks.rb b/actionpack/lib/action_dispatch/middleware/callbacks.rb index 1bb2ad7f67..8c0f4052ec 100644 --- a/actionpack/lib/action_dispatch/middleware/callbacks.rb +++ b/actionpack/lib/action_dispatch/middleware/callbacks.rb @@ -19,8 +19,7 @@ module ActionDispatch set_callback(:call, :after, *args, &block) end - def initialize(app, unused = nil) - ActiveSupport::Deprecation.warn "Passing a second argument to ActionDispatch::Callbacks.new is deprecated." unless unused.nil? + def initialize(app) @app = app end diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index 24ebb8fed7..8c4615c0c1 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -1,4 +1,5 @@ -require "active_support/core_ext/object/blank" +require 'active_support/core_ext/object/blank' +require 'active_support/core_ext/hash/keys' module ActionDispatch class Request @@ -59,7 +60,7 @@ module ActionDispatch # The option symbols for setting cookies are: # # * <tt>:value</tt> - The cookie's value or list of values (as an array). - # * <tt>:path</tt> - The path for which this cookie applies. Defaults to the root + # * <tt>:path</tt> - The path for which this cookie applies. Defaults to the root # of the application. # * <tt>:domain</tt> - The domain for which this cookie applies so you can # restrict to the domain level. If you use a schema like www.example.com @@ -84,6 +85,7 @@ module ActionDispatch class CookieOverflow < StandardError; end class CookieJar #:nodoc: + include Enumerable # This regular expression is used to split the levels of a domain. # The top level domain can be any string without a period or @@ -123,13 +125,22 @@ module ActionDispatch alias :closed? :closed def close!; @closed = true end + def each(&block) + @cookies.each(&block) + end + # Returns the value of the cookie by +name+, or +nil+ if no such cookie exists. def [](name) @cookies[name.to_s] end + def key?(name) + @cookies.key?(name.to_s) + end + alias :has_key? :key? + def update(other_hash) - @cookies.update other_hash + @cookies.update other_hash.stringify_keys self end @@ -167,8 +178,8 @@ module ActionDispatch handle_options(options) - @set_cookies[key] = options - @delete_cookies.delete(key) + @set_cookies[key.to_s] = options + @delete_cookies.delete(key.to_s) value end @@ -181,10 +192,15 @@ module ActionDispatch handle_options(options) value = @cookies.delete(key.to_s) - @delete_cookies[key] = options + @delete_cookies[key.to_s] = options value end + # Removes all cookies on the client machine by calling <tt>delete</tt> for each cookie + def clear(options = {}) + @cookies.each_key{ |k| delete(k, options) } + end + # Returns a jar that'll automatically set the assigned cookies to have an expiration date 20 years from now. Example: # # cookies.permanent[:prefers_open_id] = true @@ -222,6 +238,11 @@ module ActionDispatch @delete_cookies.each { |k, v| ::Rack::Utils.delete_cookie_header!(headers, k, v) } end + def recycle! #:nodoc: + @set_cookies.clear + @delete_cookies.clear + end + private def write_cookie?(cookie) @@ -305,7 +326,7 @@ module ActionDispatch if secret.length < SECRET_MIN_LENGTH raise ArgumentError, "Secret should be something secure, " + - "like \"#{ActiveSupport::SecureRandom.hex(16)}\". The value you " + + "like \"#{SecureRandom.hex(16)}\". The value you " + "provided, \"#{secret}\", is shorter than the minimum length " + "of #{SECRET_MIN_LENGTH} characters" end diff --git a/actionpack/lib/action_dispatch/middleware/head.rb b/actionpack/lib/action_dispatch/middleware/head.rb index 56e2d2f2a8..f1906a3ab3 100644 --- a/actionpack/lib/action_dispatch/middleware/head.rb +++ b/actionpack/lib/action_dispatch/middleware/head.rb @@ -8,7 +8,7 @@ module ActionDispatch if env["REQUEST_METHOD"] == "HEAD" env["REQUEST_METHOD"] = "GET" env["rack.methodoverride.original_method"] = "HEAD" - status, headers, body = @app.call(env) + status, headers, _ = @app.call(env) [status, headers, []] else @app.call(env) diff --git a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb index 1a811ce1b1..6bcf099d2c 100644 --- a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb @@ -29,7 +29,7 @@ module ActionDispatch end def generate_sid - sid = ActiveSupport::SecureRandom.hex(16) + sid = SecureRandom.hex(16) sid.encode!('UTF-8') if sid.respond_to?(:encode!) sid end @@ -59,7 +59,10 @@ module ActionDispatch # Note that the regexp does not allow $1 to end with a ':' $1.constantize rescue LoadError, NameError => const_error - raise ActionDispatch::Session::SessionRestoreError, "Session contains objects whose class definition isn't available.\nRemember to require the classes for all objects kept in the session.\n(Original exception: #{const_error.message} [#{const_error.class}])\n" + raise ActionDispatch::Session::SessionRestoreError, + "Session contains objects whose class definition isn't available.\n" + + "Remember to require the classes for all objects kept in the session.\n" + + "(Original exception: #{const_error.message} [#{const_error.class}])\n" end retry else @@ -73,13 +76,7 @@ module ActionDispatch include StaleSessionCheck def destroy_session(env, sid, options) - ActiveSupport::Deprecation.warn "Implementing #destroy in session stores is deprecated. " << - "Please implement destroy_session(env, session_id, options) instead." - destroy(env) - end - - def destroy(env) - raise '#destroy needs to be implemented.' + raise '#destroy_session needs to be implemented.' end end end diff --git a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb index c17c746096..2fa68c64c5 100644 --- a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb @@ -1,4 +1,5 @@ require 'active_support/core_ext/exception' +require 'action_controller/metal/exceptions' require 'active_support/notifications' require 'action_dispatch/http/request' @@ -85,8 +86,8 @@ module ActionDispatch :framework_trace => framework_trace(exception), :full_trace => full_trace(exception) ) - file = "rescues/#{@@rescue_templates[exception.class.name]}.erb" - body = template.render(:file => file, :layout => 'rescues/layout.erb') + file = "rescues/#{@@rescue_templates[exception.class.name]}" + body = template.render(:template => file, :layout => 'rescues/layout') render(status_code(exception), body) end diff --git a/actionpack/lib/action_dispatch/railtie.rb b/actionpack/lib/action_dispatch/railtie.rb index f51cc3711b..1af89858d1 100644 --- a/actionpack/lib/action_dispatch/railtie.rb +++ b/actionpack/lib/action_dispatch/railtie.rb @@ -1,10 +1,9 @@ require "action_dispatch" -require "rails" module ActionDispatch class Railtie < Rails::Railtie config.action_dispatch = ActiveSupport::OrderedOptions.new - config.action_dispatch.x_sendfile_header = "" + config.action_dispatch.x_sendfile_header = nil config.action_dispatch.ip_spoofing_check = true config.action_dispatch.show_exceptions = true config.action_dispatch.best_standards_support = true diff --git a/actionpack/lib/action_dispatch/routing.rb b/actionpack/lib/action_dispatch/routing.rb index 43fd93adf6..1dcd83ceb5 100644 --- a/actionpack/lib/action_dispatch/routing.rb +++ b/actionpack/lib/action_dispatch/routing.rb @@ -49,8 +49,8 @@ module ActionDispatch # You may wish to organize groups of controllers under a namespace. Most # commonly, you might group a number of administrative controllers under # an +admin+ namespace. You would place these controllers under the - # app/controllers/admin directory, and you can group them together in your - # router: + # <tt>app/controllers/admin</tt> directory, and you can group them together + # in your router: # # namespace "admin" do # resources :posts, :comments @@ -152,7 +152,7 @@ module ActionDispatch # } # end # - # Using the multiline match modifier will raise an ArgumentError. + # Using the multiline match modifier will raise an +ArgumentError+. # Encoding regular expression modifiers are silently ignored. The # match will always use the default encoding or ASCII. # @@ -161,7 +161,7 @@ module ActionDispatch # Consider the following route, which you will find commented out at the # bottom of your generated <tt>config/routes.rb</tt>: # - # match ':controller(/:action(/:id(.:format)))' + # match ':controller(/:action(/:id))(.:format)' # # This route states that it expects requests to consist of a # <tt>:controller</tt> followed optionally by an <tt>:action</tt> that in diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index a65f6e1fce..cd59b13c42 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -1,4 +1,3 @@ -require 'erb' require 'active_support/core_ext/hash/except' require 'active_support/core_ext/object/blank' require 'active_support/core_ext/object/inclusion' @@ -49,6 +48,9 @@ module ActionDispatch class Mapping #:nodoc: IGNORE_OPTIONS = [:to, :as, :via, :on, :constraints, :defaults, :only, :except, :anchor, :shallow, :shallow_path, :shallow_prefix] + ANCHOR_CHARACTERS_REGEX = %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z} + SHORTHAND_REGEX = %r{^/[\w/]+$} + WILDCARD_PATH = %r{\*([^/\)]+)\)?$} def initialize(set, scope, path, options) @set, @scope = set, scope @@ -77,7 +79,7 @@ module ActionDispatch # segment_keys.include?(k.to_s) || k == :controller next unless Regexp === requirement && !constraints[name] - if requirement.source =~ %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z} + if requirement.source =~ ANCHOR_CHARACTERS_REGEX raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}" end if requirement.multiline? @@ -88,7 +90,7 @@ module ActionDispatch # match "account/overview" def using_match_shorthand?(path, options) - path && options.except(:via, :anchor, :to, :as).empty? && path =~ %r{^/[\w\/]+$} + path && options.except(:via, :anchor, :to, :as).empty? && path =~ SHORTHAND_REGEX end def normalize_path(path) @@ -102,13 +104,13 @@ module ActionDispatch # controllers with default routes like :controller/:action/:id(.:format), e.g: # GET /admin/products/show/1 # => { :controller => 'admin/products', :action => 'show', :id => '1' } - @options.reverse_merge!(:controller => /.+?/) + @options[:controller] ||= /.+?/ end # Add a constraint for wildcard route to make it non-greedy and match the # optional format part of the route by default - if path.match(/\*([^\/]+)$/) && @options[:format] != false - @options.reverse_merge!(:"#{$1}" => /.+?/) + if path.match(WILDCARD_PATH) && @options[:format] != false + @options[$1.to_sym] ||= /.+?/ end if @options[:format] == false @@ -116,6 +118,8 @@ module ActionDispatch path elsif path.include?(":format") || path.end_with?('/') path + elsif @options[:format] == true + "#{path}.:format" else "#{path}(.:format)" end @@ -187,13 +191,12 @@ module ActionDispatch end def blocks - block = @scope[:blocks] || [] - - if @options[:constraints].present? && !@options[:constraints].is_a?(Hash) - block << @options[:constraints] + constraints = @options[:constraints] + if constraints.present? && !constraints.is_a?(Hash) + [constraints] + else + @scope[:blocks] || [] end - - block end def constraints @@ -210,8 +213,8 @@ module ActionDispatch end def segment_keys - @segment_keys ||= Rack::Mount::RegexpWithNamedGroups.new( - Rack::Mount::Strexp.compile(@path, requirements, SEPARATORS) + @segment_keys ||= Journey::Path::Pattern.new( + Journey::Router::Strexp.compile(@path, requirements, SEPARATORS) ).names end @@ -220,19 +223,11 @@ module ActionDispatch end def default_controller - if @options[:controller] - @options[:controller] - elsif @scope[:controller] - @scope[:controller] - end + @options[:controller] || @scope[:controller] end def default_action - if @options[:action] - @options[:action] - elsif @scope[:action] - @scope[:action] - end + @options[:action] || @scope[:action] end end @@ -240,7 +235,7 @@ module ActionDispatch # (:locale) becomes (/:locale) instead of /(:locale). Except # for root cases, where the latter is the correct one. def self.normalize_path(path) - path = Rack::Mount::Utils.normalize_path(path) + path = Journey::Router::Utils.normalize_path(path) path.gsub!(%r{/(\(+)/?}, '\1/') unless path =~ %r{^/\(+[^/]+\)$} path end @@ -260,7 +255,7 @@ module ActionDispatch # because this means it will be matched first. As this is the most popular route # of most Rails applications, this is beneficial. def root(options = {}) - match '/', options.reverse_merge(:as => :root) + match '/', { :as => :root }.merge(options) end # Matches a url pattern to one or more routes. Any symbols in a pattern @@ -335,7 +330,7 @@ module ActionDispatch # # [:on] # Shorthand for wrapping routes in a specific RESTful context. Valid - # values are :member, :collection, and :new. Only use within + # values are +:member+, +:collection+, and +:new+. Only use within # <tt>resource(s)</tt> block. For example: # # resource :bar do @@ -352,7 +347,7 @@ module ActionDispatch # # [:constraints] # Constrains parameters with a hash of regular expressions or an - # object that responds to #matches? + # object that responds to <tt>matches?</tt> # # match 'path/:id', :constraints => { :id => /[A-Z]\d{5}/ } # @@ -373,7 +368,7 @@ module ActionDispatch # See <tt>Scoping#defaults</tt> for its scope equivalent. # # [:anchor] - # Boolean to anchor a #match pattern. Default is true. When set to + # Boolean to anchor a <tt>match</tt> pattern. Default is true. When set to # false, the pattern matches any request prefixed with the given path. # # # Matches any request starting with 'path' @@ -457,7 +452,9 @@ module ActionDispatch prefix_options = options.slice(*_route.segment_keys) # we must actually delete prefix segment keys to avoid passing them to next url_for _route.segment_keys.each { |k| options.delete(k) } - _routes.url_helpers.send("#{name}_path", prefix_options) + prefix = _routes.url_helpers.send("#{name}_path", prefix_options) + prefix = '' if prefix == '/' + prefix end end end @@ -517,15 +514,15 @@ module ActionDispatch # You may wish to organize groups of controllers under a namespace. # Most commonly, you might group a number of administrative controllers # under an +admin+ namespace. You would place these controllers under - # the app/controllers/admin directory, and you can group them together - # in your router: + # the <tt>app/controllers/admin</tt> directory, and you can group them + # together in your router: # # namespace "admin" do # resources :posts, :comments # end # # This will create a number of routes for each of the posts and comments - # controller. For Admin::PostsController, Rails will create: + # controller. For <tt>Admin::PostsController</tt>, Rails will create: # # GET /admin/posts # GET /admin/posts/new @@ -536,7 +533,7 @@ module ActionDispatch # DELETE /admin/posts/1 # # If you want to route /posts (without the prefix /admin) to - # Admin::PostsController, you could use + # <tt>Admin::PostsController</tt>, you could use # # scope :module => "admin" do # resources :posts @@ -546,7 +543,7 @@ module ActionDispatch # # resources :posts, :module => "admin" # - # If you want to route /admin/posts to PostsController + # If you want to route /admin/posts to +PostsController+ # (without the Admin:: module prefix), you could use # # scope "/admin" do @@ -555,11 +552,11 @@ module ActionDispatch # # or, for a single case # - # resources :posts, :path => "/admin" + # resources :posts, :path => "/admin/posts" # # In each of these cases, the named routes remain the same as if you did # not use scope. In the last case, the following paths map to - # PostsController: + # +PostsController+: # # GET /admin/posts # GET /admin/posts/new @@ -578,8 +575,8 @@ module ActionDispatch # end # # This generates helpers such as +account_projects_path+, just like +resources+ does. - # The difference here being that the routes generated are like /rails/projects/2, - # rather than /accounts/rails/projects/2. + # The difference here being that the routes generated are like /:account_id/projects, + # rather than /accounts/:account_id/projects. # # === Options # @@ -587,7 +584,7 @@ module ActionDispatch # # === Examples # - # # route /posts (without the prefix /admin) to Admin::PostsController + # # route /posts (without the prefix /admin) to <tt>Admin::PostsController</tt> # scope :module => "admin" do # resources :posts # end @@ -597,7 +594,7 @@ module ActionDispatch # resources :posts # end # - # # prefix the routing helper name: sekret_posts_path instead of posts_path + # # prefix the routing helper name: +sekret_posts_path+ instead of +posts_path+ # scope :as => "sekret" do # resources :posts # end @@ -656,13 +653,13 @@ module ActionDispatch # # This generates the following routes: # - # admin_posts GET /admin/posts(.:format) {:action=>"index", :controller=>"admin/posts"} - # admin_posts POST /admin/posts(.:format) {:action=>"create", :controller=>"admin/posts"} - # new_admin_post GET /admin/posts/new(.:format) {:action=>"new", :controller=>"admin/posts"} - # edit_admin_post GET /admin/posts/:id/edit(.:format) {:action=>"edit", :controller=>"admin/posts"} - # admin_post GET /admin/posts/:id(.:format) {:action=>"show", :controller=>"admin/posts"} - # admin_post PUT /admin/posts/:id(.:format) {:action=>"update", :controller=>"admin/posts"} - # admin_post DELETE /admin/posts/:id(.:format) {:action=>"destroy", :controller=>"admin/posts"} + # admin_posts GET /admin/posts(.:format) admin/posts#index + # admin_posts POST /admin/posts(.:format) admin/posts#create + # new_admin_post GET /admin/posts/new(.:format) admin/posts#new + # edit_admin_post GET /admin/posts/:id/edit(.:format) admin/posts#edit + # admin_post GET /admin/posts/:id(.:format) admin/posts#show + # admin_post PUT /admin/posts/:id(.:format) admin/posts#update + # admin_post DELETE /admin/posts/:id(.:format) admin/posts#destroy # # === Options # @@ -679,12 +676,12 @@ module ActionDispatch # resources :posts # end # - # # maps to Sekret::PostsController rather than Admin::PostsController + # # maps to <tt>Sekret::PostsController</tt> rather than <tt>Admin::PostsController</tt> # namespace :admin, :module => "sekret" do # resources :posts # end # - # # generates sekret_posts_path rather than admin_posts_path + # # generates +sekret_posts_path+ rather than +admin_posts_path+ # namespace :admin, :as => "sekret" do # resources :posts # end @@ -712,6 +709,7 @@ module ActionDispatch # constraints(:post_id => /\d+\.\d+) do # resources :comments # end + # end # # === Restricting based on IP # @@ -846,20 +844,20 @@ module ActionDispatch # You may wish to organize groups of controllers under a namespace. Most # commonly, you might group a number of administrative controllers under # an +admin+ namespace. You would place these controllers under the - # app/controllers/admin directory, and you can group them together in your - # router: + # <tt>app/controllers/admin</tt> directory, and you can group them together + # in your router: # # namespace "admin" do # resources :posts, :comments # end # - # By default the :id parameter doesn't accept dots. If you need to - # use dots as part of the :id parameter add a constraint which + # By default the +:id+ parameter doesn't accept dots. If you need to + # use dots as part of the +:id+ parameter add a constraint which # overrides this restriction, e.g: # # resources :articles, :id => /[^\/]+/ # - # This allows any character other than a slash as part of your :id. + # This allows any character other than a slash as part of your +:id+. # module Resources # CANONICAL_ACTIONS holds all actions that does not need a prefix or @@ -875,9 +873,9 @@ module ActionDispatch def initialize(entities, options = {}) @name = entities.to_s - @path = (options.delete(:path) || @name).to_s - @controller = (options.delete(:controller) || @name).to_s - @as = options.delete(:as) + @path = (options[:path] || @name).to_s + @controller = (options[:controller] || @name).to_s + @as = options[:as] @options = options end @@ -909,7 +907,7 @@ module ActionDispatch alias :member_name :singular - # Checks for uncountable plurals, and appends "_index" if the plural + # Checks for uncountable plurals, and appends "_index" if the plural # and singular form are the same. def collection_name singular == plural ? "#{plural}_index" : plural @@ -939,12 +937,11 @@ module ActionDispatch DEFAULT_ACTIONS = [:show, :create, :update, :destroy, :new, :edit] def initialize(entities, options) + super + @as = nil - @name = entities.to_s - @path = (options.delete(:path) || @name).to_s - @controller = (options.delete(:controller) || plural).to_s - @as = options.delete(:as) - @options = options + @controller = (options[:controller] || plural).to_s + @as = options[:as] end def plural @@ -975,7 +972,7 @@ module ActionDispatch # resource :geocoder # # creates six different routes in your application, all mapping to - # the GeoCoders controller (note that the controller is named after + # the +GeoCoders+ controller (note that the controller is named after # the plural): # # GET /geocoder/new @@ -1024,7 +1021,7 @@ module ActionDispatch # resources :photos # # creates seven different routes in your application, all mapping to - # the Photos controller: + # the +Photos+ controller: # # GET /photos/new # POST /photos @@ -1041,12 +1038,12 @@ module ActionDispatch # # This generates the following comments routes: # - # GET /photos/:id/comments/new - # POST /photos/:id/comments - # GET /photos/:id/comments/:id - # GET /photos/:id/comments/:id/edit - # PUT /photos/:id/comments/:id - # DELETE /photos/:id/comments/:id + # GET /photos/:photo_id/comments/new + # POST /photos/:photo_id/comments + # GET /photos/:photo_id/comments/:id + # GET /photos/:photo_id/comments/:id/edit + # PUT /photos/:photo_id/comments/:id + # DELETE /photos/:photo_id/comments/:id # # === Options # Takes same options as <tt>Base#match</tt> as well as: @@ -1082,24 +1079,28 @@ module ActionDispatch # Is the same as: # # resources :posts do - # resources :comments + # resources :comments, :except => [:show, :edit, :update, :destroy] # end - # resources :comments + # resources :comments, :only => [:show, :edit, :update, :destroy] + # + # This allows URLs for resources that otherwise would be deeply nested such + # as a comment on a blog post like <tt>/posts/a-long-permalink/comments/1234</tt> + # to be shortened to just <tt>/comments/1234</tt>. # # [:shallow_path] # Prefixes nested shallow routes with the specified path. # - # scope :shallow_path => "sekret" do - # resources :posts do - # resources :comments, :shallow => true + # scope :shallow_path => "sekret" do + # resources :posts do + # resources :comments, :shallow => true + # end # end - # end # # The +comments+ resource here will have the following routes generated for it: # - # post_comments GET /sekret/posts/:post_id/comments(.:format) - # post_comments POST /sekret/posts/:post_id/comments(.:format) - # new_post_comment GET /sekret/posts/:post_id/comments/new(.:format) + # post_comments GET /posts/:post_id/comments(.:format) + # post_comments POST /posts/:post_id/comments(.:format) + # new_post_comment GET /posts/:post_id/comments/new(.:format) # edit_comment GET /sekret/comments/:id/edit(.:format) # comment GET /sekret/comments/:id(.:format) # comment PUT /sekret/comments/:id(.:format) @@ -1107,11 +1108,11 @@ module ActionDispatch # # === Examples # - # # routes call Admin::PostsController + # # routes call <tt>Admin::PostsController</tt> # resources :posts, :module => "admin" # # # resource actions are at /admin/posts. - # resources :posts, :path => "admin" + # resources :posts, :path => "admin/posts" def resources(*resources, &block) options = resources.extract_options! @@ -1151,7 +1152,7 @@ module ActionDispatch # end # # This will enable Rails to recognize paths such as <tt>/photos/search</tt> - # with GET, and route to the search action of PhotosController. It will also + # with GET, and route to the search action of +PhotosController+. It will also # create the <tt>search_photos_url</tt> and <tt>search_photos_path</tt> # route helpers. def collection @@ -1175,7 +1176,7 @@ module ActionDispatch # end # # This will recognize <tt>/photos/1/preview</tt> with GET, and route to the - # preview action of PhotosController. It will also create the + # preview action of +PhotosController+. It will also create the # <tt>preview_photo_url</tt> and <tt>preview_photo_path</tt> helpers. def member unless resource_scope? @@ -1418,7 +1419,9 @@ module ActionDispatch end def action_path(name, path = nil) #:nodoc: - path || @scope[:path_names][name.to_sym] || name.to_s + # Ruby 1.8 can't transform empty strings to symbols + name = name.to_sym if name.is_a?(String) && !name.empty? + path || @scope[:path_names][name] || name.to_s end def prefix_name_for_action(as, action) #:nodoc: @@ -1435,7 +1438,7 @@ module ActionDispatch name_prefix = @scope[:as] if parent_resource - return nil if as.nil? && action.nil? + return nil unless as || action collection_name = parent_resource.collection_name member_name = parent_resource.member_name @@ -1462,9 +1465,9 @@ module ActionDispatch end module Shorthand #:nodoc: - def match(*args) - if args.size == 1 && args.last.is_a?(Hash) - options = args.pop + def match(path, *rest) + if rest.empty? && Hash === path + options = path path, to = options.find { |name, value| name.is_a?(String) } options.merge!(:to => to).delete(path) super(path, options) diff --git a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb index 82c4fadb50..e989a38d8b 100644 --- a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb +++ b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb @@ -124,14 +124,7 @@ module ActionDispatch args.last.kind_of?(Hash) ? args.last.merge!(url_options) : args << url_options end - if proxy - proxy.send(named_route, *args) - else - # we need to use url_for, because polymorphic_url can be used in context of other than - # current routes (e.g. engine's routes). As named routes from engine are not included - # calling engine's named route directly would fail. - url_for _routes.url_helpers.__send__("hash_for_#{named_route}", *args) - end + (proxy || self).send(named_route, *args) end # Returns the path component of a URL for the given record. It uses @@ -182,10 +175,12 @@ module ActionDispatch if record.is_a?(Symbol) || record.is_a?(String) route << record - else + elsif record route << ActiveModel::Naming.route_key(record) route = [route.join("_").singularize] if inflection == :singular route << "index" if ActiveModel::Naming.uncountable?(record) && inflection == :plural + else + raise ArgumentError, "Nil location provided. Can't build URI." end route << routing_type(options) diff --git a/actionpack/lib/action_dispatch/routing/route.rb b/actionpack/lib/action_dispatch/routing/route.rb deleted file mode 100644 index a049510182..0000000000 --- a/actionpack/lib/action_dispatch/routing/route.rb +++ /dev/null @@ -1,67 +0,0 @@ -require 'active_support/core_ext/module/deprecation' - -module ActionDispatch - module Routing - class Route #:nodoc: - attr_reader :app, :conditions, :defaults, :name - attr_reader :path, :requirements, :set - - def initialize(set, app, conditions, requirements, defaults, name, anchor) - @set = set - @app = app - @defaults = defaults - @name = name - - # FIXME: we should not be doing this much work in a constructor. - - @requirements = requirements.merge(defaults) - @requirements.delete(:controller) if @requirements[:controller].is_a?(Regexp) - @requirements.delete_if { |k, v| - v == Regexp.compile("[^#{SEPARATORS.join}]+") - } - - if path = conditions[:path_info] - @path = path - conditions[:path_info] = ::Rack::Mount::Strexp.compile(path, requirements, SEPARATORS, anchor) - end - - @verbs = conditions[:request_method] || [] - - @conditions = conditions.dup - - # Rack-Mount requires that :request_method be a regular expression. - # :request_method represents the HTTP verb that matches this route. - # - # Here we munge values before they get sent on to rack-mount. - @conditions[:request_method] = %r[^#{verb}$] unless @verbs.empty? - @conditions[:path_info] = Rack::Mount::RegexpWithNamedGroups.new(@conditions[:path_info]) if @conditions[:path_info] - @conditions.delete_if{ |k,v| k != :path_info && !valid_condition?(k) } - @requirements.delete_if{ |k,v| !valid_condition?(k) } - end - - def verb - @verbs.join '|' - 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 - deprecate :to_a - - def to_s - @to_s ||= begin - "%-6s %-40s %s" % [(verb || :any).to_s.upcase, path, requirements.inspect] - end - end - - private - def valid_condition?(method) - segment_keys.include?(method) || set.valid_conditions.include?(method) - end - end - end -end diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 5097f6732d..bc956ef216 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -1,9 +1,10 @@ -require 'rack/mount' +require 'journey' require 'forwardable' require 'active_support/core_ext/object/blank' require 'active_support/core_ext/object/to_query' require 'active_support/core_ext/hash/slice' require 'active_support/core_ext/module/remove_method' +require 'action_controller/metal/exceptions' module ActionDispatch module Routing @@ -164,13 +165,14 @@ module ActionDispatch remove_possible_method :#{selector} def #{selector}(*args) options = args.extract_options! + result = #{options.inspect} if args.any? - options[:_positional_args] = args - options[:_positional_keys] = #{route.segment_keys.inspect} + result[:_positional_args] = args + result[:_positional_keys] = #{route.segment_keys.inspect} end - options ? #{options.inspect}.merge(options) : #{options.inspect} + result.merge(options) end protected :#{selector} END_EVAL @@ -204,29 +206,42 @@ module ActionDispatch end end - attr_accessor :set, :routes, :named_routes, :default_scope + attr_accessor :formatter, :set, :named_routes, :default_scope, :router attr_accessor :disable_clear_and_finalize, :resources_path_names attr_accessor :default_url_options, :request_class, :valid_conditions + alias :routes :set + def self.default_resources_path_names { :new => 'new', :edit => 'edit' } end def initialize(request_class = ActionDispatch::Request) - self.routes = [] self.named_routes = NamedRouteCollection.new self.resources_path_names = self.class.default_resources_path_names.dup self.default_url_options = {} self.request_class = request_class - self.valid_conditions = request_class.public_instance_methods.map { |m| m.to_sym } + @valid_conditions = {} + + request_class.public_instance_methods.each { |m| + @valid_conditions[m.to_sym] = true + } + @valid_conditions[:controller] = true + @valid_conditions[:action] = true + self.valid_conditions.delete(:id) - self.valid_conditions.push(:controller, :action) - @append = [] - @prepend = [] + @append = [] + @prepend = [] @disable_clear_and_finalize = false - clear! + @finalized = false + + @set = Journey::Routes.new + @router = Journey::Router.new(@set, { + :parameters_key => PARAMETERS_KEY, + :request_class => request_class}) + @formatter = Journey::Formatter.new @set end def draw(&block) @@ -262,17 +277,13 @@ module ActionDispatch return if @finalized @append.each { |blk| eval_block(blk) } @finalized = true - @set.freeze end def clear! @finalized = false - routes.clear named_routes.clear - @set = ::Rack::Mount::RouteSet.new( - :parameters_key => PARAMETERS_KEY, - :request_class => request_class - ) + set.clear + formatter.clear @prepend.each { |blk| eval_block(blk) } end @@ -340,26 +351,55 @@ module ActionDispatch def add_route(app, conditions = {}, requirements = {}, defaults = {}, name = nil, anchor = true) raise ArgumentError, "Invalid route name: '#{name}'" unless name.blank? || name.to_s.match(/^[_a-z]\w*$/i) - route = Route.new(self, app, conditions, requirements, defaults, name, anchor) - @set.add_route(route.app, route.conditions, route.defaults, route.name) + + path = build_path(conditions.delete(:path_info), requirements, SEPARATORS, anchor) + conditions = build_conditions(conditions, valid_conditions, path.names.map { |x| x.to_sym }) + + route = @set.add_route(app, path, conditions, defaults, name) named_routes[name] = route if name - routes << route route end + def build_path(path, requirements, separators, anchor) + strexp = Journey::Router::Strexp.new( + path, + requirements, + SEPARATORS, + anchor) + + Journey::Path::Pattern.new(strexp) + end + private :build_path + + def build_conditions(current_conditions, req_predicates, path_values) + conditions = current_conditions.dup + + verbs = conditions[:request_method] || [] + + # Rack-Mount requires that :request_method be a regular expression. + # :request_method represents the HTTP verb that matches this route. + # + # Here we munge values before they get sent on to rack-mount. + unless verbs.empty? + conditions[:request_method] = %r[^#{verbs.join('|')}$] + end + conditions.delete_if { |k,v| !(req_predicates.include?(k) || path_values.include?(k)) } + + conditions + end + private :build_conditions + class Generator #:nodoc: - PARAMETERIZE = { - :parameterize => lambda do |name, value| - if name == :controller - value - elsif value.is_a?(Array) - value.map { |v| Rack::Mount::Utils.escape_uri(v.to_param) }.join('/') - else - return nil unless param = value.to_param - param.split('/').map { |v| Rack::Mount::Utils.escape_uri(v) }.join("/") - end + PARAMETERIZE = lambda do |name, value| + if name == :controller + value + elsif value.is_a?(Array) + value.map { |v| Journey::Router::Utils.escape_uri(v.to_param) }.join('/') + else + return nil unless param = value.to_param + param.split('/').map { |v| Journey::Router::Utils.escape_uri(v) }.join("/") end - } + end attr_reader :options, :recall, :set, :named_route @@ -449,14 +489,14 @@ module ActionDispatch end def generate - path, params = @set.set.generate(:path_info, named_route, options, recall, PARAMETERIZE) + path, params = @set.formatter.generate(:path_info, named_route, options, recall, PARAMETERIZE) raise_routing_error unless path return [path, params.keys] if @extras [path, params] - rescue Rack::Mount::RoutingError + rescue Journey::Router::RoutingError raise_routing_error end @@ -528,12 +568,12 @@ module ActionDispatch def call(env) finalize! - @set.call(env) + @router.call(env) end def recognize_path(path, environment = {}) method = (environment[:method] || "GET").to_s.upcase - path = Rack::Mount::Utils.normalize_path(path) unless path =~ %r{://} + path = Journey::Router::Utils.normalize_path(path) unless path =~ %r{://} begin env = Rack::MockRequest.env_for(path, {:method => method}) @@ -542,7 +582,7 @@ module ActionDispatch end req = @request_class.new(env) - @set.recognize(req) do |route, matches, params| + @router.recognize(req) do |route, matches, params| params.each do |key, value| if value.is_a?(String) value = value.dup.force_encoding(Encoding::BINARY) if value.encoding_aware? diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb index d4db78a25a..8fc8dc191b 100644 --- a/actionpack/lib/action_dispatch/routing/url_for.rb +++ b/actionpack/lib/action_dispatch/routing/url_for.rb @@ -108,7 +108,7 @@ module ActionDispatch end # Generate a url based on the options provided, default_url_options and the - # routes defined in routes.rb. The following options are supported: + # routes defined in routes.rb. The following options are supported: # # * <tt>:only_path</tt> - If true, the relative url is returned. Defaults to +false+. # * <tt>:protocol</tt> - The protocol to connect to. Defaults to 'http'. @@ -116,9 +116,10 @@ module ActionDispatch # If <tt>:only_path</tt> is false, this option must be # provided either explicitly, or via +default_url_options+. # * <tt>:subdomain</tt> - Specifies the subdomain of the link, using the +tld_length+ - # to split the domain from the host. - # * <tt>:domain</tt> - Specifies the domain of the link, using the +tld_length+ # to split the subdomain from the host. + # If false, removes all subdomains from the host part of the link. + # * <tt>:domain</tt> - Specifies the domain of the link, using the +tld_length+ + # to split the domain from the host. # * <tt>:tld_length</tt> - Number of labels the TLD id composed of, only used if # <tt>:subdomain</tt> or <tt>:domain</tt> are supplied. Defaults to # <tt>ActionDispatch::Http::URL.tld_length</tt>, which in turn defaults to 1. @@ -131,16 +132,20 @@ module ActionDispatch # # Examples: # - # url_for :controller => 'tasks', :action => 'testing', :host=>'somehost.org', :port=>'8080' # => 'http://somehost.org:8080/tasks/testing' - # url_for :controller => 'tasks', :action => 'testing', :host=>'somehost.org', :anchor => 'ok', :only_path => true # => '/tasks/testing#ok' - # url_for :controller => 'tasks', :action => 'testing', :trailing_slash=>true # => 'http://somehost.org/tasks/testing/' - # url_for :controller => 'tasks', :action => 'testing', :host=>'somehost.org', :number => '33' # => 'http://somehost.org/tasks/testing?number=33' + # url_for :controller => 'tasks', :action => 'testing', :host => 'somehost.org', :port => '8080' + # # => 'http://somehost.org:8080/tasks/testing' + # url_for :controller => 'tasks', :action => 'testing', :host => 'somehost.org', :anchor => 'ok', :only_path => true + # # => '/tasks/testing#ok' + # url_for :controller => 'tasks', :action => 'testing', :trailing_slash => true + # # => 'http://somehost.org/tasks/testing/' + # url_for :controller => 'tasks', :action => 'testing', :host => 'somehost.org', :number => '33' + # # => 'http://somehost.org/tasks/testing?number=33' def url_for(options = nil) case options when String options when nil, Hash - _routes.url_for((options || {}).reverse_merge!(url_options).symbolize_keys) + _routes.url_for((options || {}).reverse_merge(url_options).symbolize_keys) else polymorphic_url(options) end diff --git a/actionpack/lib/action_dispatch/testing/assertions.rb b/actionpack/lib/action_dispatch/testing/assertions.rb index 822150b768..226baf9ad0 100644 --- a/actionpack/lib/action_dispatch/testing/assertions.rb +++ b/actionpack/lib/action_dispatch/testing/assertions.rb @@ -8,12 +8,11 @@ module ActionDispatch extend ActiveSupport::Concern - included do - include DomAssertions - include ResponseAssertions - include RoutingAssertions - include SelectorAssertions - include TagAssertions - end + include DomAssertions + include ResponseAssertions + include RoutingAssertions + include SelectorAssertions + include TagAssertions end end + diff --git a/actionpack/lib/action_dispatch/testing/assertions/response.rb b/actionpack/lib/action_dispatch/testing/assertions/response.rb index 3335742d47..7381617dd7 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/response.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/response.rb @@ -6,13 +6,6 @@ module ActionDispatch module ResponseAssertions extend ActiveSupport::Concern - included do - # TODO: Need to pull in AV::Template monkey patches that track which - # templates are rendered. assert_template should probably be part - # of AV instead of AD. - require 'action_view/test_case' - end - # Asserts that the response is one of the following types: # # * <tt>:success</tt> - Status code was 200 @@ -62,16 +55,14 @@ module ActionDispatch # assert_redirected_to @customer # def assert_redirected_to(options = {}, message=nil) - validate_request! - assert_response(:redirect, message) return true if options == @response.location - redirected_to_after_normalisation = normalize_argument_to_redirection(@response.location) - options_after_normalisation = normalize_argument_to_redirection(options) + redirect_is = normalize_argument_to_redirection(@response.location) + redirect_expected = normalize_argument_to_redirection(options) - if redirected_to_after_normalisation != options_after_normalisation - flunk "Expected response to be a redirect to <#{options_after_normalisation}> but was a redirect to <#{redirected_to_after_normalisation}>" + if redirect_is != redirect_expected + flunk "Expected response to be a redirect to <#{redirect_expected}> but was a redirect to <#{redirect_is}>" end end diff --git a/actionpack/lib/action_dispatch/testing/assertions/routing.rb b/actionpack/lib/action_dispatch/testing/assertions/routing.rb index b760db42e2..b10aab9029 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/routing.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/routing.rb @@ -1,24 +1,25 @@ require 'uri' require 'active_support/core_ext/hash/diff' require 'active_support/core_ext/hash/indifferent_access' +require 'action_controller/metal/exceptions' module ActionDispatch module Assertions # Suite of assertions to test routes generated by \Rails and the handling of requests made to them. module RoutingAssertions # Asserts that the routing of the given +path+ was handled correctly and that the parsed options (given in the +expected_options+ hash) - # match +path+. Basically, it asserts that \Rails recognizes the route given by +expected_options+. + # match +path+. Basically, it asserts that \Rails recognizes the route given by +expected_options+. # - # Pass a hash in the second argument (+path+) to specify the request method. This is useful for routes - # requiring a specific HTTP method. The hash should contain a :path with the incoming request path + # Pass a hash in the second argument (+path+) to specify the request method. This is useful for routes + # requiring a specific HTTP method. The hash should contain a :path with the incoming request path # and a :method containing the required HTTP verb. # # # assert that POSTing to /items will call the create action on ItemsController # assert_recognizes({:controller => 'items', :action => 'create'}, {:path => 'items', :method => :post}) # - # You can also pass in +extras+ with a hash containing URL parameters that would normally be in the query string. This can be used - # to assert that values in the query string string will end up in the params hash correctly. To test query strings you must use the - # extras argument, appending the query string on the path directly will not work. For example: + # You can also pass in +extras+ with a hash containing URL parameters that would normally be in the query string. This can be used + # to assert that values in the query string string will end up in the params hash correctly. To test query strings you must use the + # extras argument, appending the query string on the path directly will not work. For example: # # # assert that a path of '/items/list/1?view=print' returns the correct options # assert_recognizes({:controller => 'items', :action => 'list', :id => '1', :view => 'print'}, 'items/list/1', { :view => "print" }) @@ -49,7 +50,7 @@ module ActionDispatch assert_equal(expected_options, request.path_parameters, msg) end - # Asserts that the provided options can be used to generate the provided path. This is the inverse of +assert_recognizes+. + # Asserts that the provided options can be used to generate the provided path. This is the inverse of +assert_recognizes+. # The +extras+ parameter is used to tell the request the names and values of additional request parameters that would be in # a query string. The +message+ parameter allows you to specify a custom error message for assertion failures. # @@ -92,10 +93,10 @@ module ActionDispatch end # Asserts that path and options match both ways; in other words, it verifies that <tt>path</tt> generates - # <tt>options</tt> and then that <tt>options</tt> generates <tt>path</tt>. This essentially combines +assert_recognizes+ + # <tt>options</tt> and then that <tt>options</tt> generates <tt>path</tt>. This essentially combines +assert_recognizes+ # and +assert_generates+ into one step. # - # The +extras+ hash allows you to specify options that would normally be provided as a query string to the action. The + # The +extras+ hash allows you to specify options that would normally be provided as a query string to the action. The # +message+ parameter allows you to specify a custom error message to display upon failure. # # ==== Examples diff --git a/actionpack/lib/action_dispatch/testing/assertions/selector.rb b/actionpack/lib/action_dispatch/testing/assertions/selector.rb index c67a0664dc..b4555f4f59 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/selector.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/selector.rb @@ -169,7 +169,7 @@ module ActionDispatch # assert_select "title", "Welcome" # # # Page title is "Welcome" and there is only one title element - # assert_select "title", {:count=>1, :text=>"Welcome"}, + # assert_select "title", {:count => 1, :text => "Welcome"}, # "Wrong title or more than one title element" # # # Page contains no forms @@ -415,9 +415,9 @@ module ActionDispatch assert !deliveries.empty?, "No e-mail in delivery list" for delivery in deliveries - for part in delivery.parts + for part in (delivery.parts.empty? ? [delivery] : delivery.parts) if part["Content-Type"].to_s =~ /^text\/html\W/ - root = HTML::Document.new(part.body).root + root = HTML::Document.new(part.body.to_s).root assert_select root, ":root", &block end end diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index 7d707d03a9..0f1bb9f260 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -62,7 +62,7 @@ module ActionDispatch # # The request_method is +:get+, +:post+, +:put+, +:delete+ or +:head+; the # parameters are +nil+, a hash, or a url-encoded or multipart string; - # the headers are a hash. Keys are automatically upcased and prefixed + # the headers are a hash. Keys are automatically upcased and prefixed # with 'HTTP_' if not already. def xml_http_request(request_method, path, parameters = nil, headers = nil) headers ||= {} @@ -207,9 +207,6 @@ module ActionDispatch "*/*;q=0.5" unless defined? @named_routes_configured - # install the named routes in this session instance. - klass = singleton_class - # the helpers are made protected by default--we make them public for # easier access during testing and troubleshooting. @named_routes_configured = true @@ -244,8 +241,8 @@ module ActionDispatch end # Performs the actual request. - def process(method, path, parameters = nil, env = nil) - env ||= {} + def process(method, path, parameters = nil, rack_env = nil) + rack_env ||= {} if path =~ %r{://} location = URI.parse(path) https! URI::HTTPS === location if location.scheme @@ -261,7 +258,7 @@ module ActionDispatch hostname, port = host.split(':') - default_env = { + env = { :method => method, :params => parameters, @@ -279,7 +276,7 @@ module ActionDispatch session = Rack::Test::Session.new(_mock_session) - env.reverse_merge!(default_env) + env.merge!(rack_env) # NOTE: rack-test v0.5 doesn't build a default uri correctly # Make sure requested path is always a full uri diff --git a/actionpack/lib/action_dispatch/testing/performance_test.rb b/actionpack/lib/action_dispatch/testing/performance_test.rb index e7aeb45fb3..13fe693c32 100644 --- a/actionpack/lib/action_dispatch/testing/performance_test.rb +++ b/actionpack/lib/action_dispatch/testing/performance_test.rb @@ -1,17 +1,10 @@ require 'active_support/testing/performance' -begin - module ActionDispatch - # An integration test that runs a code profiler on your test methods. - # Profiling output for combinations of each test method, measurement, and - # output format are written to your tmp/performance directory. - # - # By default, process_time is measured and both flat and graph_html output - # formats are written, so you'll have two output files per test method. - class PerformanceTest < ActionDispatch::IntegrationTest - include ActiveSupport::Testing::Performance - end +module ActionDispatch + # An integration test that runs a code profiler on your test methods. + # Profiling output for combinations of each test method, measurement, and + # output format are written to your tmp/performance directory. + class PerformanceTest < ActionDispatch::IntegrationTest + include ActiveSupport::Testing::Performance end -rescue NameError - $stderr.puts "Specify ruby-prof as application's dependency in Gemfile to run benchmarks." end diff --git a/actionpack/lib/action_dispatch/testing/test_process.rb b/actionpack/lib/action_dispatch/testing/test_process.rb index d430691429..b08ff41950 100644 --- a/actionpack/lib/action_dispatch/testing/test_process.rb +++ b/actionpack/lib/action_dispatch/testing/test_process.rb @@ -1,15 +1,11 @@ +require 'action_dispatch/middleware/cookies' require 'action_dispatch/middleware/flash' require 'active_support/core_ext/hash/indifferent_access' module ActionDispatch module TestProcess def assigns(key = nil) - assigns = {}.with_indifferent_access - @controller.instance_variable_names.each do |ivar| - next if ActionController::Base.protected_instance_variables.include?(ivar) - assigns[ivar[1..-1]] = @controller.instance_variable_get(ivar) - end - + assigns = @controller.view_assigns.with_indifferent_access key.nil? ? assigns : assigns[key] end @@ -22,14 +18,14 @@ module ActionDispatch end def cookies - @request.cookies.merge(@response.cookies).with_indifferent_access + @request.cookie_jar end def redirect_to_url @response.redirect_url end - # Shortcut for <tt>ARack::Test::UploadedFile.new(ActionController::TestCase.fixture_path + path, type)</tt>: + # Shortcut for <tt>Rack::Test::UploadedFile.new(ActionController::TestCase.fixture_path + path, type)</tt>: # # post :change_avatar, :avatar => fixture_file_upload('/files/spongebob.png', 'image/png') # @@ -38,7 +34,7 @@ module ActionDispatch # # post :change_avatar, :avatar => fixture_file_upload('/files/spongebob.png', 'image/png', :binary) def fixture_file_upload(path, mime_type = nil, binary = false) - fixture_path = ActionController::TestCase.send(:fixture_path) if ActionController::TestCase.respond_to?(:fixture_path) + fixture_path = self.class.fixture_path if self.class.respond_to?(:fixture_path) Rack::Test::UploadedFile.new("#{fixture_path}#{path}", mime_type, binary) end end diff --git a/actionpack/lib/action_dispatch/testing/test_request.rb b/actionpack/lib/action_dispatch/testing/test_request.rb index 822adb6a47..7280e9a93b 100644 --- a/actionpack/lib/action_dispatch/testing/test_request.rb +++ b/actionpack/lib/action_dispatch/testing/test_request.rb @@ -1,4 +1,5 @@ require 'active_support/core_ext/object/blank' +require 'active_support/core_ext/hash/indifferent_access' require 'active_support/core_ext/hash/reverse_merge' require 'rack/utils' @@ -14,18 +15,11 @@ module ActionDispatch env = Rails.application.env_config.merge(env) if defined?(Rails.application) super(DEFAULT_ENV.merge(env)) - @cookies = nil self.host = 'test.host' self.remote_addr = '0.0.0.0' self.user_agent = 'Rails Testing' end - def env - write_cookies! - delete_nil_values! - super - end - def request_method=(method) @env['REQUEST_METHOD'] = method.to_s.upcase end @@ -71,23 +65,10 @@ module ActionDispatch @env['HTTP_ACCEPT'] = Array(mime_types).collect { |mime_type| mime_type.to_s }.join(",") end + alias :rack_cookies :cookies + def cookies - @cookies ||= super + @cookies ||= {}.with_indifferent_access end - - private - def write_cookies! - unless @cookies.blank? - @env['HTTP_COOKIE'] = @cookies.map { |name, value| escape_cookie(name, value) }.join('; ') - end - end - - def escape_cookie(name, value) - "#{Rack::Utils.escape(name)}=#{Rack::Utils.escape(value)}" - end - - def delete_nil_values! - @env.delete_if { |k, v| v.nil? } - end end end diff --git a/actionpack/lib/action_pack/version.rb b/actionpack/lib/action_pack/version.rb index 584e5c3791..add6b56425 100644 --- a/actionpack/lib/action_pack/version.rb +++ b/actionpack/lib/action_pack/version.rb @@ -1,9 +1,9 @@ module ActionPack module VERSION #:nodoc: MAJOR = 3 - MINOR = 1 + MINOR = 2 TINY = 0 - PRE = "beta1" + PRE = "beta" STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') end diff --git a/actionpack/lib/action_view.rb b/actionpack/lib/action_view.rb index a67b61c1ef..d7229419a9 100644 --- a/actionpack/lib/action_view.rb +++ b/actionpack/lib/action_view.rb @@ -30,6 +30,7 @@ module ActionView extend ActiveSupport::Autoload eager_autoload do + autoload :AssetPaths autoload :Base autoload :Context autoload :Helpers @@ -71,11 +72,6 @@ module ActionView autoload :TemplateError autoload :WrongEncodingError end - - autoload_at "action_view/template" do - autoload :TemplateHandler - autoload :TemplateHandlers - end end ENCODING_FLAG = '#.*coding[:=]\s*(\S+)[ \t]*' diff --git a/actionpack/lib/action_view/asset_paths.rb b/actionpack/lib/action_view/asset_paths.rb new file mode 100644 index 0000000000..1d16e34df6 --- /dev/null +++ b/actionpack/lib/action_view/asset_paths.rb @@ -0,0 +1,136 @@ +require 'zlib' +require 'active_support/core_ext/file' +require 'action_controller/metal/exceptions' + +module ActionView + class AssetPaths #:nodoc: + attr_reader :config, :controller + + def initialize(config, controller = nil) + @config = config + @controller = controller + end + + # Add the extension +ext+ if not present. Return full or scheme-relative URLs otherwise untouched. + # Prefix with <tt>/dir/</tt> if lacking a leading +/+. Account for relative URL + # roots. Rewrite the asset path for cache-busting asset ids. Include + # asset host, if configured, with the correct request protocol. + # + # When :relative (default), the protocol will be determined by the client using current protocol + # When :request, the protocol will be the request protocol + # Otherwise, the protocol is used (E.g. :http, :https, etc) + def compute_public_path(source, dir, options = {}) + source = source.to_s + return source if is_uri?(source) + + source = rewrite_extension(source, dir, options[:ext]) if options[:ext] + source = rewrite_asset_path(source, dir, options) + source = rewrite_relative_url_root(source, relative_url_root) + source = rewrite_host_and_protocol(source, options[:protocol]) + source + end + + # Return the filesystem path for the source + def compute_source_path(source, dir, ext) + source = rewrite_extension(source, dir, ext) if ext + File.join(config.assets_dir, dir, source) + end + + def is_uri?(path) + path =~ %r{^[-a-z]+://|^cid:|^//} + end + + private + + def rewrite_extension(source, dir, ext) + raise NotImplementedError + end + + def rewrite_asset_path(source, path = nil) + raise NotImplementedError + end + + def rewrite_relative_url_root(source, relative_url_root) + relative_url_root && !source.starts_with?("#{relative_url_root}/") ? "#{relative_url_root}#{source}" : source + end + + def has_request? + controller.respond_to?(:request) + end + + def rewrite_host_and_protocol(source, protocol = nil) + host = compute_asset_host(source) + if host && !is_uri?(host) + if (protocol || default_protocol) == :request && !has_request? + host = nil + else + host = "#{compute_protocol(protocol)}#{host}" + end + end + host ? "#{host}#{source}" : source + end + + def compute_protocol(protocol) + protocol ||= default_protocol + case protocol + when :relative + "//" + when :request + unless @controller + invalid_asset_host!("The protocol requested was :request. Consider using :relative instead.") + end + @controller.request.protocol + else + "#{protocol}://" + end + end + + def default_protocol + @config.default_asset_host_protocol || (has_request? ? :request : :relative) + end + + def invalid_asset_host!(help_message) + raise ActionController::RoutingError, "This asset host cannot be computed without a request in scope. #{help_message}" + end + + # Pick an asset host for this source. Returns +nil+ if no host is set, + # the host if no wildcard is set, the host interpolated with the + # numbers 0-3 if it contains <tt>%d</tt> (the number is the source hash mod 4), + # or the value returned from invoking call on an object responding to call + # (proc or otherwise). + def compute_asset_host(source) + if host = asset_host_config + if host.respond_to?(:call) + args = [source] + arity = arity_of(host) + if arity > 1 && !has_request? + invalid_asset_host!("Remove the second argument to your asset_host Proc if you do not need the request.") + end + args << current_request if (arity > 1 || arity < 0) && has_request? + host.call(*args) + else + (host =~ /%d/) ? host % (Zlib.crc32(source) % 4) : host + end + end + end + + def relative_url_root + config.relative_url_root + end + + def asset_host_config + config.asset_host + end + + # Returns the current request if one exists. + def current_request + controller.request if has_request? + end + + # Returns the arity of a callable + def arity_of(callable) + callable.respond_to?(:arity) ? callable.arity : callable.method(:call).arity + end + + end +end diff --git a/actionpack/lib/action_view/base.rb b/actionpack/lib/action_view/base.rb index c98110353f..36c49d9c91 100644 --- a/actionpack/lib/action_view/base.rb +++ b/actionpack/lib/action_view/base.rb @@ -4,6 +4,7 @@ require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/array/wrap' require 'active_support/ordered_options' require 'action_view/log_subscriber' +require 'active_support/core_ext/module/deprecation' module ActionView #:nodoc: # = Action View Base @@ -85,11 +86,11 @@ module ActionView #:nodoc: # # Here are some basic examples: # - # xml.em("emphasized") # => <em>emphasized</em> - # xml.em { xml.b("emph & bold") } # => <em><b>emph & bold</b></em> - # xml.a("A Link", "href"=>"http://onestepback.org") # => <a href="http://onestepback.org">A Link</a> - # xml.target("name"=>"compile", "option"=>"fast") # => <target option="fast" name="compile"\> - # # NOTE: order of attributes is not specified. + # xml.em("emphasized") # => <em>emphasized</em> + # xml.em { xml.b("emph & bold") } # => <em><b>emph & bold</b></em> + # xml.a("A Link", "href" => "http://onestepback.org") # => <a href="http://onestepback.org">A Link</a> + # xml.target("name" => "compile", "option" => "fast") # => <target option="fast" name="compile"\> + # # NOTE: order of attributes is not specified. # # Any method with a block will be treated as an XML markup tag with nested markup in the block. For example, the following: # @@ -115,7 +116,7 @@ module ActionView #:nodoc: # xml.language "en-us" # xml.ttl "40" # - # for item in @recent_items + # @recent_items.each do |item| # xml.item do # xml.title(item_title(item)) # xml.description(item_description(item)) if item_description(item) @@ -161,6 +162,7 @@ module ActionView #:nodoc: value.is_a?(PathSet) ? value.dup : ActionView::PathSet.new(Array.wrap(value)) end + deprecate :process_view_paths def xss_safe? #:nodoc: true @@ -194,7 +196,7 @@ module ActionView #:nodoc: end def initialize(context = nil, assigns = {}, controller = nil, formats = nil) #:nodoc: - @_config = {} + @_config = ActiveSupport::InheritableOptions.new # Handle all these for backwards compatibility. # TODO Provide a new API for AV::Base and deprecate this one. diff --git a/actionpack/lib/action_view/buffers.rb b/actionpack/lib/action_view/buffers.rb index 089fc68706..be7f65c2ce 100644 --- a/actionpack/lib/action_view/buffers.rb +++ b/actionpack/lib/action_view/buffers.rb @@ -35,9 +35,9 @@ module ActionView def html_safe? true end - + def html_safe self end end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_view/helpers.rb b/actionpack/lib/action_view/helpers.rb index 78a68db282..262e0f1010 100644 --- a/actionpack/lib/action_view/helpers.rb +++ b/actionpack/lib/action_view/helpers.rb @@ -22,7 +22,6 @@ module ActionView #:nodoc: autoload :RecordTagHelper autoload :RenderingHelper autoload :SanitizeHelper - autoload :SprocketsHelper autoload :TagHelper autoload :TextHelper autoload :TranslationHelper @@ -53,7 +52,6 @@ module ActionView #:nodoc: include RecordTagHelper include RenderingHelper include SanitizeHelper - include SprocketsHelper include TagHelper include TextHelper include TranslationHelper diff --git a/actionpack/lib/action_view/helpers/asset_paths.rb b/actionpack/lib/action_view/helpers/asset_paths.rb index 958f0e0a10..fae2e4fc1c 100644 --- a/actionpack/lib/action_view/helpers/asset_paths.rb +++ b/actionpack/lib/action_view/helpers/asset_paths.rb @@ -1,79 +1,7 @@ -require 'active_support/core_ext/file' -require 'action_view/helpers/asset_paths' +ActiveSupport::Deprecation.warn "ActionView::Helpers::AssetPaths is deprecated. Please use ActionView::AssetPaths instead." module ActionView module Helpers - - class AssetPaths #:nodoc: - attr_reader :config, :controller - - def initialize(config, controller) - @config = config - @controller = controller - end - - # Add the extension +ext+ if not present. Return full or scheme-relative URLs otherwise untouched. - # Prefix with <tt>/dir/</tt> if lacking a leading +/+. Account for relative URL - # roots. Rewrite the asset path for cache-busting asset ids. Include - # asset host, if configured, with the correct request protocol. - def compute_public_path(source, dir, ext = nil, include_host = true) - source = source.to_s - return source if is_uri?(source) - - source = rewrite_extension(source, dir, ext) if ext - source = rewrite_asset_path(source, dir) - - if controller && include_host - has_request = controller.respond_to?(:request) - source = rewrite_host_and_protocol(source, has_request) - end - - source - end - - def is_uri?(path) - path =~ %r{^[-a-z]+://|^cid:|^//} - end - - private - - def rewrite_extension(source, dir, ext) - raise NotImplementedError - end - - def rewrite_asset_path(source, path = nil) - raise NotImplementedError - end - - def rewrite_host_and_protocol(source, has_request) - host = compute_asset_host(source) - if has_request && host && !is_uri?(host) - host = "#{controller.request.protocol}#{host}" - end - "#{host}#{source}" - end - - # Pick an asset host for this source. Returns +nil+ if no host is set, - # the host if no wildcard is set, the host interpolated with the - # numbers 0-3 if it contains <tt>%d</tt> (the number is the source hash mod 4), - # or the value returned from invoking the proc if it's a proc or the value from - # invoking call if it's an object responding to call. - def compute_asset_host(source) - if host = config.asset_host - if host.is_a?(Proc) || host.respond_to?(:call) - case host.is_a?(Proc) ? host.arity : host.method(:call).arity - when 2 - request = controller.respond_to?(:request) && controller.request - host.call(source, request) - else - host.call(source) - end - else - (host =~ /%d/) ? host % (source.hash % 4) : host - end - end - end - end - + AssetPaths = ::ActionView::AssetPaths end end
\ No newline at end of file diff --git a/actionpack/lib/action_view/helpers/asset_tag_helper.rb b/actionpack/lib/action_view/helpers/asset_tag_helper.rb index 9bc847a1ab..7d01e5ddb8 100644 --- a/actionpack/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/asset_tag_helper.rb @@ -1,6 +1,7 @@ require 'action_view/helpers/asset_tag_helpers/javascript_tag_helpers' require 'action_view/helpers/asset_tag_helpers/stylesheet_tag_helpers' require 'action_view/helpers/asset_tag_helpers/asset_paths' +require 'action_view/helpers/tag_helper' module ActionView # = Action View Asset Tag Helpers @@ -153,7 +154,7 @@ module ActionView # "/release-#{RELEASE_NUMBER}#{asset_path}" # } # - # This example would cause the following behaviour on all servers no + # This example would cause the following behavior on all servers no # matter when they were deployed: # # image_tag("rails.png") @@ -191,6 +192,7 @@ module ActionView # RewriteEngine On # RewriteRule ^/release-\d+/(images|javascripts|stylesheets)/(.*)$ /$1/$2 [L] module AssetTagHelper + include TagHelper include JavascriptTagHelpers include StylesheetTagHelpers # Returns a link tag that browsers and news readers can use to auto-detect @@ -274,11 +276,7 @@ module ActionView # The alias +path_to_image+ is provided to avoid that. Rails uses the alias internally, and # plugin authors are encouraged to do so. def image_path(source) - if config.use_sprockets - asset_path(source) - else - asset_paths.compute_public_path(source, 'images') - end + asset_paths.compute_public_path(source, 'images') end alias_method :path_to_image, :image_path # aliased to avoid conflicts with an image_path named route @@ -293,11 +291,7 @@ module ActionView # video_path("/trailers/hd.avi") # => /trailers/hd.avi # video_path("http://www.example.com/vid/hd.avi") # => http://www.example.com/vid/hd.avi def video_path(source) - if config.use_sprockets - asset_path(source) - else - asset_paths.compute_public_path(source, 'videos') - end + asset_paths.compute_public_path(source, 'videos') end alias_method :path_to_video, :video_path # aliased to avoid conflicts with a video_path named route @@ -312,11 +306,7 @@ module ActionView # audio_path("/sounds/horse.wav") # => /sounds/horse.wav # audio_path("http://www.example.com/sounds/horse.wav") # => http://www.example.com/sounds/horse.wav def audio_path(source) - if config.use_sprockets - asset_path(source) - else - asset_paths.compute_public_path(source, 'audios') - end + asset_paths.compute_public_path(source, 'audios') end alias_method :path_to_audio, :audio_path # aliased to avoid conflicts with an audio_path named route @@ -359,7 +349,7 @@ module ActionView src = options[:src] = path_to_image(source) unless src =~ /^cid:/ - options[:alt] = options.fetch(:alt){ File.basename(src, '.*').capitalize } + options[:alt] = options.fetch(:alt){ image_alt(src) } end if size = options.delete(:size) @@ -374,6 +364,10 @@ module ActionView tag("img", options) end + def image_alt(src) + File.basename(src, '.*').sub(/-[[:xdigit:]]{32}\z/, '').capitalize + end + # Returns an html video tag for the +sources+. If +sources+ is a string, # a single video tag will be returned. If +sources+ is an array, a video # tag with nested source tags for each source will be returned. The diff --git a/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_include_tag.rb b/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_include_tag.rb index e4662a2919..05d5f1870a 100644 --- a/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_include_tag.rb +++ b/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_include_tag.rb @@ -60,12 +60,16 @@ module ActionView private - def path_to_asset(source, include_host = true) - asset_paths.compute_public_path(source, asset_name.to_s.pluralize, extension, include_host) + def path_to_asset(source, options = {}) + asset_paths.compute_public_path(source, asset_name.to_s.pluralize, options.merge(:ext => extension)) + end + + def path_to_asset_source(source) + asset_paths.compute_source_path(source, asset_name.to_s.pluralize, extension) end def compute_paths(*args) - expand_sources(*args).collect { |source| asset_paths.compute_public_path(source, asset_name.pluralize, extension, false) } + expand_sources(*args).collect { |source| path_to_asset_source(source) } end def expand_sources(sources, recursive) @@ -92,7 +96,7 @@ module ActionView def ensure_sources!(sources) sources.each do |source| - asset_file_path!(path_to_asset(source, false)) + asset_file_path!(path_to_asset_source(source)) end end @@ -123,19 +127,14 @@ module ActionView # Set mtime to the latest of the combined files to allow for # consistent ETag without a shared filesystem. - mt = asset_paths.map { |p| File.mtime(asset_file_path(p)) }.max + mt = asset_paths.map { |p| File.mtime(asset_file_path!(p)) }.max File.utime(mt, mt, joined_asset_path) end - def asset_file_path(path) - File.join(config.assets_dir, path.split('?').first) - end - - def asset_file_path!(path, error_if_file_is_uri = false) - if asset_paths.is_uri?(path) + def asset_file_path!(absolute_path, error_if_file_is_uri = false) + if asset_paths.is_uri?(absolute_path) raise(Errno::ENOENT, "Asset file #{path} is uri and cannot be merged into single file") if error_if_file_is_uri else - absolute_path = asset_file_path(path) raise(Errno::ENOENT, "Asset file not found at '#{absolute_path}'" ) unless File.exist?(absolute_path) return absolute_path end diff --git a/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_paths.rb b/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_paths.rb index cd0f8c8878..dd4e9ae4cc 100644 --- a/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_paths.rb +++ b/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_paths.rb @@ -1,11 +1,11 @@ +require 'thread' require 'active_support/core_ext/file' -require 'action_view/helpers/asset_paths' module ActionView module Helpers module AssetTagHelper - class AssetPaths < ActionView::Helpers::AssetPaths #:nodoc: + class AssetPaths < ::ActionView::AssetPaths #:nodoc: # You can enable or disable the asset tag ids cache. # With the cache enabled, the asset tag helper methods will make fewer # expensive file system calls (the default implementation checks the file @@ -41,7 +41,7 @@ module ActionView # Break out the asset path rewrite in case plugins wish to put the asset id # someplace other than the query string. - def rewrite_asset_path(source, dir) + def rewrite_asset_path(source, dir, options = nil) source = "/#{dir}/#{source}" unless source[0] == ?/ path = config.asset_path @@ -85,17 +85,8 @@ module ActionView end end end - - def rewrite_relative_url_root(source, relative_url_root) - relative_url_root && !source.starts_with?("#{relative_url_root}/") ? "#{relative_url_root}#{source}" : source - end - - def rewrite_host_and_protocol(source, has_request) - source = rewrite_relative_url_root(source, controller.config.relative_url_root) if has_request - super(source, has_request) - end end end end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_view/helpers/asset_tag_helpers/javascript_tag_helpers.rb b/actionpack/lib/action_view/helpers/asset_tag_helpers/javascript_tag_helpers.rb index e1ee0d0e1a..09700bd0c5 100644 --- a/actionpack/lib/action_view/helpers/asset_tag_helpers/javascript_tag_helpers.rb +++ b/actionpack/lib/action_view/helpers/asset_tag_helpers/javascript_tag_helpers.rb @@ -83,11 +83,7 @@ module ActionView # javascript_path "http://www.example.com/js/xmlhr" # => http://www.example.com/js/xmlhr # javascript_path "http://www.example.com/js/xmlhr.js" # => http://www.example.com/js/xmlhr.js def javascript_path(source) - if config.use_sprockets - asset_path(source, 'js') - else - asset_paths.compute_public_path(source, 'javascripts', 'js') - end + asset_paths.compute_public_path(source, 'javascripts', :ext => 'js') end alias_method :path_to_javascript, :javascript_path # aliased to avoid conflicts with a javascript_path named route @@ -187,12 +183,8 @@ module ActionView # # javascript_include_tag :all, :cache => true, :recursive => true def javascript_include_tag(*sources) - if config.use_sprockets - sprockets_javascript_include_tag(*sources) - else - @javascript_include ||= JavascriptIncludeTag.new(config, asset_paths) - @javascript_include.include_tag(*sources) - end + @javascript_include ||= JavascriptIncludeTag.new(config, asset_paths) + @javascript_include.include_tag(*sources) end end end diff --git a/actionpack/lib/action_view/helpers/asset_tag_helpers/stylesheet_tag_helpers.rb b/actionpack/lib/action_view/helpers/asset_tag_helpers/stylesheet_tag_helpers.rb index a95eb221be..343153c8c5 100644 --- a/actionpack/lib/action_view/helpers/asset_tag_helpers/stylesheet_tag_helpers.rb +++ b/actionpack/lib/action_view/helpers/asset_tag_helpers/stylesheet_tag_helpers.rb @@ -16,7 +16,8 @@ module ActionView end def asset_tag(source, options) - tag("link", { "rel" => "stylesheet", "type" => Mime::CSS, "media" => "screen", "href" => ERB::Util.html_escape(path_to_asset(source)) }.merge(options), false, false) + # We force the :request protocol here to avoid a double-download bug in IE7 and IE8 + tag("link", { "rel" => "stylesheet", "type" => Mime::CSS, "media" => "screen", "href" => path_to_asset(source, :protocol => :request) }.merge(options)) end def custom_dir @@ -52,7 +53,7 @@ module ActionView # If the +source+ filename has no extension, <tt>.css</tt> will be appended (except for explicit URIs). # Full paths from the document root will be passed through. # Used internally by +stylesheet_link_tag+ to build the stylesheet path. - # + # # ==== Examples # stylesheet_path "style" # => /stylesheets/style.css # stylesheet_path "dir/style.css" # => /stylesheets/dir/style.css @@ -60,11 +61,7 @@ module ActionView # stylesheet_path "http://www.example.com/css/style" # => http://www.example.com/css/style # stylesheet_path "http://www.example.com/css/style.css" # => http://www.example.com/css/style.css def stylesheet_path(source) - if config.use_sprockets - asset_path(source, 'css') - else - asset_paths.compute_public_path(source, 'stylesheets', 'css') - end + asset_paths.compute_public_path(source, 'stylesheets', :ext => 'css', :protocol => :request) end alias_method :path_to_stylesheet, :stylesheet_path # aliased to avoid conflicts with a stylesheet_path named route @@ -137,12 +134,8 @@ module ActionView # stylesheet_link_tag :all, :concat => true # def stylesheet_link_tag(*sources) - if config.use_sprockets - sprockets_stylesheet_link_tag(*sources) - else - @stylesheet_include ||= StylesheetIncludeTag.new(config, asset_paths) - @stylesheet_include.include_tag(*sources) - end + @stylesheet_include ||= StylesheetIncludeTag.new(config, asset_paths) + @stylesheet_include.include_tag(*sources) end end diff --git a/actionpack/lib/action_view/helpers/atom_feed_helper.rb b/actionpack/lib/action_view/helpers/atom_feed_helper.rb index 96e5722252..39c37b25dc 100644 --- a/actionpack/lib/action_view/helpers/atom_feed_helper.rb +++ b/actionpack/lib/action_view/helpers/atom_feed_helper.rb @@ -20,7 +20,7 @@ module ActionView # # GET /posts.html # # GET /posts.atom # def index - # @posts = Post.find(:all) + # @posts = Post.all # # respond_to do |format| # format.html @@ -34,7 +34,7 @@ module ActionView # feed.title("My great blog!") # feed.updated(@posts.first.created_at) # - # for post in @posts + # @posts.each do |post| # feed.entry(post) do |entry| # entry.title(post.title) # entry.content(post.body, :type => 'html') @@ -66,7 +66,7 @@ module ActionView # feed.updated((@posts.first.created_at)) # feed.tag!(openSearch:totalResults, 10) # - # for post in @posts + # @posts.each do |post| # feed.entry(post) do |entry| # entry.title(post.title) # entry.content(post.body, :type => 'html') @@ -81,8 +81,8 @@ module ActionView # # The Atom spec defines five elements (content rights title subtitle # summary) which may directly contain xhtml content if :type => 'xhtml' - # is specified as an attribute. If so, this helper will take care of - # the enclosing div and xhtml namespace declaration. Example usage: + # is specified as an attribute. If so, this helper will take care of + # the enclosing div and xhtml namespace declaration. Example usage: # # entry.summary :type => 'xhtml' do |xhtml| # xhtml.p pluralize(order.line_items.count, "line item") @@ -91,8 +91,8 @@ module ActionView # end # # - # atom_feed yields an AtomFeedBuilder instance. Nested elements yield - # an AtomBuilder instance. + # <tt>atom_feed</tt> yields an +AtomFeedBuilder+ instance. Nested elements yield + # an +AtomBuilder+ instance. def atom_feed(options = {}, &block) if options[:schema_date] options[:schema_date] = options[:schema_date].strftime("%Y-%m-%d") if options[:schema_date].respond_to?(:strftime) diff --git a/actionpack/lib/action_view/helpers/cache_helper.rb b/actionpack/lib/action_view/helpers/cache_helper.rb index 385378ea29..f81ce3e31c 100644 --- a/actionpack/lib/action_view/helpers/cache_helper.rb +++ b/actionpack/lib/action_view/helpers/cache_helper.rb @@ -3,7 +3,7 @@ module ActionView module Helpers module CacheHelper # This helper exposes a method for caching fragments of a view - # rather than an entire action or page. This technique is useful + # rather than an entire action or page. This technique is useful # caching pieces like menus, lists of newstopics, static HTML # fragments, and so on. This method takes a block that contains # the content you wish to cache. @@ -51,7 +51,11 @@ module ActionView # This dance is needed because Builder can't use capture pos = output_buffer.length yield + output_safe = output_buffer.html_safe? fragment = output_buffer.slice!(pos..-1) + if output_safe + self.output_buffer = output_buffer.html_safe + end controller.write_fragment(name, fragment, options) end end diff --git a/actionpack/lib/action_view/helpers/capture_helper.rb b/actionpack/lib/action_view/helpers/capture_helper.rb index 3b5f4e694f..8abd85c3a3 100644 --- a/actionpack/lib/action_view/helpers/capture_helper.rb +++ b/actionpack/lib/action_view/helpers/capture_helper.rb @@ -27,7 +27,7 @@ module ActionView # "The current timestamp is #{Time.now}." # end # - # You can then use that variable anywhere else. For example: + # You can then use that variable anywhere else. For example: # # <html> # <head><title><%= @greeting %></title></head> @@ -76,7 +76,7 @@ module ActionView # # <%= stored_content %> # - # You can use the <tt>yield</tt> syntax alongside an existing call to <tt>yield</tt> in a layout. For example: + # You can use the <tt>yield</tt> syntax alongside an existing call to <tt>yield</tt> in a layout. For example: # # <%# This is the layout %> # <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> @@ -134,9 +134,9 @@ module ActionView # WARNING: content_for is ignored in caches. So you shouldn't use it # for elements that will be fragment cached. def content_for(name, content = nil, &block) - content = capture(&block) if block_given? - if content - @view_flow.append(name, content) + if content || block_given? + content = capture(&block) if block_given? + @view_flow.append(name, content) if content nil else @view_flow.get(name) diff --git a/actionpack/lib/action_view/helpers/controller_helper.rb b/actionpack/lib/action_view/helpers/controller_helper.rb index e22331cb3c..1a583e62ae 100644 --- a/actionpack/lib/action_view/helpers/controller_helper.rb +++ b/actionpack/lib/action_view/helpers/controller_helper.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/module/attr_internal' + module ActionView module Helpers # This module keeps all methods and behavior in ActionView @@ -18,4 +20,4 @@ module ActionView end end end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_view/helpers/date_helper.rb b/actionpack/lib/action_view/helpers/date_helper.rb index d2ddaafcb3..4deb87180c 100644 --- a/actionpack/lib/action_view/helpers/date_helper.rb +++ b/actionpack/lib/action_view/helpers/date_helper.rb @@ -8,18 +8,18 @@ module ActionView module Helpers # = Action View Date Helpers # - # The Date Helper primarily creates select/option tags for different kinds of dates and date elements. All of the - # select-type methods share a number of common options that are as follows: + # The Date Helper primarily creates select/option tags for different kinds of dates and times or date and time + # elements. All of the select-type methods share a number of common options that are as follows: # # * <tt>:prefix</tt> - overwrites the default prefix of "date" used for the select names. So specifying "birthday" - # would give birthday[month] instead of date[month] if passed to the select_month method. + # would give birthday[month] instead of date[month] if passed to the <tt>select_month</tt> method. # * <tt>:include_blank</tt> - set to true if it should be possible to set an empty date. # * <tt>:discard_type</tt> - set to true if you want to discard the type part of the select name. If set to true, - # the select_month method would use simply "date" (which can be overwritten using <tt>:prefix</tt>) instead of - # "date[month]". + # the <tt>select_month</tt> method would use simply "date" (which can be overwritten using <tt>:prefix</tt>) instead + # of "date[month]". module DateHelper - # Reports the approximate distance in time between two Time or Date objects or integers as seconds. - # Set <tt>include_seconds</tt> to true if you want more detailed approximations when distance < 1 min, 29 secs + # Reports the approximate distance in time between two Time, Date or DateTime objects or integers as seconds. + # Set <tt>include_seconds</tt> to true if you want more detailed approximations when distance < 1 min, 29 secs. # Distances are reported based on the following table: # # 0 <-> 29 secs # => less than a minute @@ -119,7 +119,7 @@ module ActionView end end - # Like distance_of_time_in_words, but where <tt>to_time</tt> is fixed to <tt>Time.now</tt>. + # Like <tt>distance_of_time_in_words</tt>, but where <tt>to_time</tt> is fixed to <tt>Time.now</tt>. # # ==== Examples # time_ago_in_words(3.minutes.from_now) # => 3 minutes @@ -176,37 +176,37 @@ module ActionView # NOTE: Discarded selects will default to 1. So if no month select is available, January will be assumed. # # ==== Examples - # # Generates a date select that when POSTed is stored in the post variable, in the written_on attribute - # date_select("post", "written_on") + # # Generates a date select that when POSTed is stored in the article variable, in the written_on attribute. + # date_select("article", "written_on") # - # # Generates a date select that when POSTed is stored in the post variable, in the written_on attribute, + # # Generates a date select that when POSTed is stored in the article variable, in the written_on attribute, # # with the year in the year drop down box starting at 1995. - # date_select("post", "written_on", :start_year => 1995) + # date_select("article", "written_on", :start_year => 1995) # - # # Generates a date select that when POSTed is stored in the post variable, in the written_on attribute, + # # Generates a date select that when POSTed is stored in the article variable, in the written_on attribute, # # with the year in the year drop down box starting at 1995, numbers used for months instead of words, # # and without a day select box. - # date_select("post", "written_on", :start_year => 1995, :use_month_numbers => true, + # date_select("article", "written_on", :start_year => 1995, :use_month_numbers => true, # :discard_day => true, :include_blank => true) # - # # Generates a date select that when POSTed is stored in the post variable, in the written_on attribute + # # Generates a date select that when POSTed is stored in the article variable, in the written_on attribute # # with the fields ordered as day, month, year rather than month, day, year. - # date_select("post", "written_on", :order => [:day, :month, :year]) + # date_select("article", "written_on", :order => [:day, :month, :year]) # # # Generates a date select that when POSTed is stored in the user variable, in the birthday attribute # # lacking a year field. # date_select("user", "birthday", :order => [:month, :day]) # - # # Generates a date select that when POSTed is stored in the user variable, in the birthday attribute + # # Generates a date select that when POSTed is stored in the article variable, in the written_on attribute # # which is initially set to the date 3 days from the current date - # date_select("post", "written_on", :default => 3.days.from_now) + # date_select("article", "written_on", :default => 3.days.from_now) # # # Generates a date select that when POSTed is stored in the credit_card variable, in the bill_due attribute # # that will have a default day of 20. # date_select("credit_card", "bill_due", :default => { :day => 20 }) # - # # Generates a date select with custom prompts - # date_select("post", "written_on", :prompt => { :day => 'Select day', :month => 'Select month', :year => 'Select year' }) + # # Generates a date select with custom prompts. + # date_select("article", "written_on", :prompt => { :day => 'Select day', :month => 'Select month', :year => 'Select year' }) # # The selects are prepared for multi-parameter assignment to an Active Record object. # @@ -222,25 +222,26 @@ module ActionView # with <tt>:ampm</tt> option. # # This method will also generate 3 input hidden tags, for the actual year, month and day unless the option - # <tt>:ignore_date</tt> is set to +true+. + # <tt>:ignore_date</tt> is set to +true+. If you set the <tt>:ignore_date</tt> to +true+, you must have a + # +date_select+ on the same method within the form otherwise an exception will be raised. # # If anything is passed in the html_options hash it will be applied to every select tag in the set. # # ==== Examples - # # Creates a time select tag that, when POSTed, will be stored in the post variable in the sunrise attribute - # time_select("post", "sunrise") + # # Creates a time select tag that, when POSTed, will be stored in the article variable in the sunrise attribute. + # time_select("article", "sunrise") # - # # Creates a time select tag with a seconds field that, when POSTed, will be stored in the post variables in + # # Creates a time select tag with a seconds field that, when POSTed, will be stored in the article variables in # # the sunrise attribute. - # time_select("post", "start_time", :include_seconds => true) + # time_select("article", "start_time", :include_seconds => true) # - # # You can set the :minute_step to 15 which will give you: 00, 15, 30 and 45. + # # You can set the <tt>:minute_step</tt> to 15 which will give you: 00, 15, 30 and 45. # time_select 'game', 'game_time', {:minute_step => 15} # - # # Creates a time select tag with a custom prompt. Use :prompt => true for generic prompts. - # time_select("post", "written_on", :prompt => {:hour => 'Choose hour', :minute => 'Choose minute', :second => 'Choose seconds'}) - # time_select("post", "written_on", :prompt => {:hour => true}) # generic prompt for hours - # time_select("post", "written_on", :prompt => true) # generic prompts for all + # # Creates a time select tag with a custom prompt. Use <tt>:prompt => true</tt> for generic prompts. + # time_select("article", "written_on", :prompt => {:hour => 'Choose hour', :minute => 'Choose minute', :second => 'Choose seconds'}) + # time_select("article", "written_on", :prompt => {:hour => true}) # generic prompt for hours + # time_select("article", "written_on", :prompt => true) # generic prompts for all # # # You can set :ampm option to true which will show the hours as: 12 PM, 01 AM .. 11 PM. # time_select 'game', 'game_time', {:ampm => true} @@ -260,36 +261,36 @@ module ActionView # If anything is passed in the html_options hash it will be applied to every select tag in the set. # # ==== Examples - # # Generates a datetime select that, when POSTed, will be stored in the post variable in the written_on - # # attribute - # datetime_select("post", "written_on") + # # Generates a datetime select that, when POSTed, will be stored in the article variable in the written_on + # # attribute. + # datetime_select("article", "written_on") # # # Generates a datetime select with a year select that starts at 1995 that, when POSTed, will be stored in the - # # post variable in the written_on attribute. - # datetime_select("post", "written_on", :start_year => 1995) + # # article variable in the written_on attribute. + # datetime_select("article", "written_on", :start_year => 1995) # # # Generates a datetime select with a default value of 3 days from the current time that, when POSTed, will # # be stored in the trip variable in the departing attribute. # datetime_select("trip", "departing", :default => 3.days.from_now) # # # Generate a datetime select with hours in the AM/PM format - # datetime_select("post", "written_on", :ampm => true) + # datetime_select("article", "written_on", :ampm => true) # - # # Generates a datetime select that discards the type that, when POSTed, will be stored in the post variable + # # Generates a datetime select that discards the type that, when POSTed, will be stored in the article variable # # as the written_on attribute. - # datetime_select("post", "written_on", :discard_type => true) + # datetime_select("article", "written_on", :discard_type => true) # - # # Generates a datetime select with a custom prompt. Use :prompt=>true for generic prompts. - # datetime_select("post", "written_on", :prompt => {:day => 'Choose day', :month => 'Choose month', :year => 'Choose year'}) - # datetime_select("post", "written_on", :prompt => {:hour => true}) # generic prompt for hours - # datetime_select("post", "written_on", :prompt => true) # generic prompts for all + # # Generates a datetime select with a custom prompt. Use <tt>:prompt => true</tt> for generic prompts. + # datetime_select("article", "written_on", :prompt => {:day => 'Choose day', :month => 'Choose month', :year => 'Choose year'}) + # datetime_select("article", "written_on", :prompt => {:hour => true}) # generic prompt for hours + # datetime_select("article", "written_on", :prompt => true) # generic prompts for all # # The selects are prepared for multi-parameter assignment to an Active Record object. def datetime_select(object_name, method, options = {}, html_options = {}) InstanceTag.new(object_name, method, self, options.delete(:object)).to_datetime_select_tag(options, html_options) end - # Returns a set of html select-tags (one for year, month, day, hour, and minute) pre-selected with the + # Returns a set of html select-tags (one for year, month, day, hour, minute, and second) pre-selected with the # +datetime+. It's also possible to explicitly set the order of the tags using the <tt>:order</tt> option with # an array of symbols <tt>:year</tt>, <tt>:month</tt> and <tt>:day</tt> in the desired order. If you do not # supply a Symbol, it will be appended onto the <tt>:order</tt> passed in. You can also add @@ -301,7 +302,7 @@ module ActionView # ==== Examples # my_date_time = Time.now + 4.days # - # # Generates a datetime select that defaults to the datetime in my_date_time (four days after today) + # # Generates a datetime select that defaults to the datetime in my_date_time (four days after today). # select_datetime(my_date_time) # # # Generates a datetime select that defaults to today (no specified datetime) @@ -331,7 +332,7 @@ module ActionView # # prefixed with 'payday' rather than 'date' # select_datetime(my_date_time, :prefix => 'payday') # - # # Generates a datetime select with a custom prompt. Use :prompt=>true for generic prompts. + # # Generates a datetime select with a custom prompt. Use <tt>:prompt => true</tt> for generic prompts. # select_datetime(my_date_time, :prompt => {:day => 'Choose day', :month => 'Choose month', :year => 'Choose year'}) # select_datetime(my_date_time, :prompt => {:hour => true}) # generic prompt for hours # select_datetime(my_date_time, :prompt => true) # generic prompts for all @@ -342,18 +343,18 @@ module ActionView # Returns a set of html select-tags (one for year, month, and day) pre-selected with the +date+. # It's possible to explicitly set the order of the tags using the <tt>:order</tt> option with an array of - # symbols <tt>:year</tt>, <tt>:month</tt> and <tt>:day</tt> in the desired order. If you do not supply a Symbol, - # it will be appended onto the <tt>:order</tt> passed in. + # symbols <tt>:year</tt>, <tt>:month</tt> and <tt>:day</tt> in the desired order. + # If the array passed to the <tt>:order</tt> option does not contain all the three symbols, all tags will be hidden. # # If anything is passed in the html_options hash it will be applied to every select tag in the set. # # ==== Examples - # my_date = Time.today + 6.days + # my_date = Time.now + 6.days # - # # Generates a date select that defaults to the date in my_date (six days after today) + # # Generates a date select that defaults to the date in my_date (six days after today). # select_date(my_date) # - # # Generates a date select that defaults to today (no specified date) + # # Generates a date select that defaults to today (no specified date). # select_date() # # # Generates a date select that defaults to the date in my_date (six days after today) @@ -361,18 +362,18 @@ module ActionView # select_date(my_date, :order => [:year, :month, :day]) # # # Generates a date select that discards the type of the field and defaults to the date in - # # my_date (six days after today) + # # my_date (six days after today). # select_date(my_date, :discard_type => true) # # # Generates a date select that defaults to the date in my_date, - # # which has fields separated by '/' + # # which has fields separated by '/'. # select_date(my_date, :date_separator => '/') # # # Generates a date select that defaults to the datetime in my_date (six days after today) - # # prefixed with 'payday' rather than 'date' + # # prefixed with 'payday' rather than 'date'. # select_date(my_date, :prefix => 'payday') # - # # Generates a date select with a custom prompt. Use :prompt=>true for generic prompts. + # # Generates a date select with a custom prompt. Use <tt>:prompt => true</tt> for generic prompts. # select_date(my_date, :prompt => {:day => 'Choose day', :month => 'Choose month', :year => 'Choose year'}) # select_date(my_date, :prompt => {:hour => true}) # generic prompt for hours # select_date(my_date, :prompt => true) # generic prompts for all @@ -381,7 +382,7 @@ module ActionView DateTimeSelector.new(date, options, html_options).select_date end - # Returns a set of html select-tags (one for hour and minute) + # Returns a set of html select-tags (one for hour and minute). # You can set <tt>:time_separator</tt> key to format the output, and # the <tt>:include_seconds</tt> option to include an input for seconds. # @@ -390,28 +391,28 @@ module ActionView # ==== Examples # my_time = Time.now + 5.days + 7.hours + 3.minutes + 14.seconds # - # # Generates a time select that defaults to the time in my_time + # # Generates a time select that defaults to the time in my_time. # select_time(my_time) # - # # Generates a time select that defaults to the current time (no specified time) + # # Generates a time select that defaults to the current time (no specified time). # select_time() # # # Generates a time select that defaults to the time in my_time, - # # which has fields separated by ':' + # # which has fields separated by ':'. # select_time(my_time, :time_separator => ':') # # # Generates a time select that defaults to the time in my_time, - # # that also includes an input for seconds + # # that also includes an input for seconds. # select_time(my_time, :include_seconds => true) # # # Generates a time select that defaults to the time in my_time, that has fields - # # separated by ':' and includes an input for seconds + # # separated by ':' and includes an input for seconds. # select_time(my_time, :time_separator => ':', :include_seconds => true) # # # Generate a time select field with hours in the AM/PM format # select_time(my_time, :ampm => true) # - # # Generates a time select with a custom prompt. Use :prompt=>true for generic prompts. + # # Generates a time select with a custom prompt. Use <tt>:prompt</tt> to true for generic prompts. # select_time(my_time, :prompt => {:day => 'Choose day', :month => 'Choose month', :year => 'Choose year'}) # select_time(my_time, :prompt => {:hour => true}) # generic prompt for hours # select_time(my_time, :prompt => true) # generic prompts for all @@ -421,25 +422,25 @@ module ActionView end # Returns a select tag with options for each of the seconds 0 through 59 with the current second selected. - # The <tt>second</tt> can also be substituted for a second number. + # The <tt>datetime</tt> can be either a +Time+ or +DateTime+ object or an integer. # Override the field name using the <tt>:field_name</tt> option, 'second' by default. # # ==== Examples # my_time = Time.now + 16.minutes # - # # Generates a select field for seconds that defaults to the seconds for the time in my_time + # # Generates a select field for seconds that defaults to the seconds for the time in my_time. # select_second(my_time) # - # # Generates a select field for seconds that defaults to the number given + # # Generates a select field for seconds that defaults to the number given. # select_second(33) # # # Generates a select field for seconds that defaults to the seconds for the time in my_time - # # that is named 'interval' rather than 'second' + # # that is named 'interval' rather than 'second'. # select_second(my_time, :field_name => 'interval') # - # # Generates a select field for seconds with a custom prompt. Use :prompt=>true for a + # # Generates a select field for seconds with a custom prompt. Use <tt>:prompt => true</tt> for a # # generic prompt. - # select_minute(14, :prompt => 'Choose seconds') + # select_second(14, :prompt => 'Choose seconds') # def select_second(datetime, options = {}, html_options = {}) DateTimeSelector.new(datetime, options, html_options).select_second @@ -447,23 +448,23 @@ module ActionView # Returns a select tag with options for each of the minutes 0 through 59 with the current minute selected. # Also can return a select tag with options by <tt>minute_step</tt> from 0 through 59 with the 00 minute - # selected. The <tt>minute</tt> can also be substituted for a minute number. + # selected. The <tt>datetime</tt> can be either a +Time+ or +DateTime+ object or an integer. # Override the field name using the <tt>:field_name</tt> option, 'minute' by default. # # ==== Examples # my_time = Time.now + 6.hours # - # # Generates a select field for minutes that defaults to the minutes for the time in my_time + # # Generates a select field for minutes that defaults to the minutes for the time in my_time. # select_minute(my_time) # - # # Generates a select field for minutes that defaults to the number given + # # Generates a select field for minutes that defaults to the number given. # select_minute(14) # # # Generates a select field for minutes that defaults to the minutes for the time in my_time - # # that is named 'stride' rather than 'second' - # select_minute(my_time, :field_name => 'stride') + # # that is named 'moment' rather than 'minute'. + # select_minute(my_time, :field_name => 'moment') # - # # Generates a select field for minutes with a custom prompt. Use :prompt=>true for a + # # Generates a select field for minutes with a custom prompt. Use <tt>:prompt => true</tt> for a # # generic prompt. # select_minute(14, :prompt => 'Choose minutes') # @@ -472,23 +473,23 @@ module ActionView end # Returns a select tag with options for each of the hours 0 through 23 with the current hour selected. - # The <tt>hour</tt> can also be substituted for a hour number. + # The <tt>datetime</tt> can be either a +Time+ or +DateTime+ object or an integer. # Override the field name using the <tt>:field_name</tt> option, 'hour' by default. # # ==== Examples # my_time = Time.now + 6.hours # - # # Generates a select field for hours that defaults to the hour for the time in my_time + # # Generates a select field for hours that defaults to the hour for the time in my_time. # select_hour(my_time) # - # # Generates a select field for hours that defaults to the number given + # # Generates a select field for hours that defaults to the number given. # select_hour(13) # - # # Generates a select field for hours that defaults to the minutes for the time in my_time - # # that is named 'stride' rather than 'second' + # # Generates a select field for hours that defaults to the hour for the time in my_time + # # that is named 'stride' rather than 'hour'. # select_hour(my_time, :field_name => 'stride') # - # # Generates a select field for hours with a custom prompt. Use :prompt => true for a + # # Generates a select field for hours with a custom prompt. Use <tt>:prompt => true</tt> for a # # generic prompt. # select_hour(13, :prompt => 'Choose hour') # @@ -500,23 +501,23 @@ module ActionView end # Returns a select tag with options for each of the days 1 through 31 with the current day selected. - # The <tt>date</tt> can also be substituted for a hour number. + # The <tt>date</tt> can also be substituted for a day number. # Override the field name using the <tt>:field_name</tt> option, 'day' by default. # # ==== Examples - # my_date = Time.today + 2.days + # my_date = Time.now + 2.days # - # # Generates a select field for days that defaults to the day for the date in my_date + # # Generates a select field for days that defaults to the day for the date in my_date. # select_day(my_time) # - # # Generates a select field for days that defaults to the number given + # # Generates a select field for days that defaults to the number given. # select_day(5) # # # Generates a select field for days that defaults to the day for the date in my_date - # # that is named 'due' rather than 'day' + # # that is named 'due' rather than 'day'. # select_day(my_time, :field_name => 'due') # - # # Generates a select field for days with a custom prompt. Use :prompt => true for a + # # Generates a select field for days with a custom prompt. Use <tt>:prompt => true</tt> for a # # generic prompt. # select_day(5, :prompt => 'Choose day') # @@ -539,7 +540,7 @@ module ActionView # select_month(Date.today) # # # Generates a select field for months that defaults to the current month that - # # is named "start" rather than "month" + # # is named "start" rather than "month". # select_month(Date.today, :field_name => 'start') # # # Generates a select field for months that defaults to the current month that @@ -558,7 +559,7 @@ module ActionView # # will use keys like "Januar", "Marts." # select_month(Date.today, :use_month_names => %w(Januar Februar Marts ...)) # - # # Generates a select field for months with a custom prompt. Use :prompt => true for a + # # Generates a select field for months with a custom prompt. Use <tt>:prompt => true</tt> for a # # generic prompt. # select_month(14, :prompt => 'Choose month') # @@ -574,22 +575,22 @@ module ActionView # # ==== Examples # # Generates a select field for years that defaults to the current year that - # # has ascending year values + # # has ascending year values. # select_year(Date.today, :start_year => 1992, :end_year => 2007) # # # Generates a select field for years that defaults to the current year that - # # is named 'birth' rather than 'year' + # # is named 'birth' rather than 'year'. # select_year(Date.today, :field_name => 'birth') # # # Generates a select field for years that defaults to the current year that - # # has descending year values + # # has descending year values. # select_year(Date.today, :start_year => 2005, :end_year => 1900) # # # Generates a select field for years that defaults to the year 2006 that - # # has ascending year values + # # has ascending year values. # select_year(2006, :start_year => 2000, :end_year => 2010) # - # # Generates a select field for years with a custom prompt. Use :prompt => true for a + # # Generates a select field for years with a custom prompt. Use <tt>:prompt => true</tt> for a # # generic prompt. # select_year(14, :prompt => 'Choose year') # @@ -620,7 +621,6 @@ module ActionView end class DateTimeSelector #:nodoc: - extend ActiveSupport::Memoizable include ActionView::Helpers::TagHelper DEFAULT_PREFIX = 'date'.freeze @@ -765,11 +765,16 @@ module ActionView if @options[:use_hidden] || @options[:discard_year] build_hidden(:year, val) else - options = {} - options[:start] = @options[:start_year] || middle_year - 5 - options[:end] = @options[:end_year] || middle_year + 5 - options[:step] = options[:start] < options[:end] ? 1 : -1 - options[:leading_zeros] = false + options = {} + options[:start] = @options[:start_year] || middle_year - 5 + options[:end] = @options[:end_year] || middle_year + 5 + options[:step] = options[:start] < options[:end] ? 1 : -1 + options[:leading_zeros] = false + options[:max_years_allowed] = @options[:max_years_allowed] || 1000 + + if (options[:end] - options[:start]).abs > options[:max_years_allowed] + raise ArgumentError, "There're too many years options to be built. Are you sure you haven't mistyped something? You can provide the :max_years_allowed parameter" + end build_options_and_select(:year, val, options) end @@ -783,21 +788,22 @@ module ActionView end # Returns translated month names, but also ensures that a custom month - # name array has a leading nil element + # name array has a leading nil element. def month_names - month_names = @options[:use_month_names] || translated_month_names - month_names.unshift(nil) if month_names.size < 13 - month_names + @month_names ||= begin + month_names = @options[:use_month_names] || translated_month_names + month_names.unshift(nil) if month_names.size < 13 + month_names + end end - memoize :month_names - # Returns translated month names + # Returns translated month names. # => [nil, "January", "February", "March", # "April", "May", "June", "July", # "August", "September", "October", # "November", "December"] # - # If :use_short_month option is set + # If <tt>:use_short_month</tt> option is set # => [nil, "Jan", "Feb", "Mar", "Apr", "May", "Jun", # "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] def translated_month_names @@ -805,13 +811,13 @@ module ActionView I18n.translate(key, :locale => @options[:locale]) end - # Lookup month name for number + # Lookup month name for number. # month_name(1) => "January" # - # If :use_month_numbers option is passed + # If <tt>:use_month_numbers</tt> option is passed # month_name(1) => 1 # - # If :add_month_numbers option is passed + # If <tt>:add_month_numbers</tt> option is passed # month_name(1) => "1 - January" def month_name(number) if @options[:use_month_numbers] @@ -824,24 +830,29 @@ module ActionView end def date_order - @options[:order] || translated_date_order + @date_order ||= @options[:order] || translated_date_order end - memoize :date_order def translated_date_order I18n.translate(:'date.order', :locale => @options[:locale]) || [] end - # Build full select tag from date type and options + # Build full select tag from date type and options. def build_options_and_select(type, selected, options = {}) build_select(type, build_options(selected, options)) end - # Build select option html from date value and options + # Build select option html from date value and options. # build_options(15, :start => 1, :end => 31) # => "<option value="1">1</option> - # <option value=\"2\">2</option> - # <option value=\"3\">3</option>..." + # <option value="2">2</option> + # <option value="3">3</option>..." + # + # If <tt>:step</tt> options is passed + # build_options(15, :start => 1, :end => 31, :step => 2) + # => "<option value="1">1</option> + # <option value="3">3</option> + # <option value="5">5</option>..." def build_options(selected, options = {}) start = options.delete(:start) || 0 stop = options.delete(:end) || 59 @@ -860,7 +871,7 @@ module ActionView (select_options.join("\n") + "\n").html_safe end - # Builds select tag from date type and html select options + # Builds select tag from date type and html select options. # build_select(:month, "<option value="1">January</option>...") # => "<select id="post_written_on_2i" name="post[written_on(2i)]"> # <option value="1">January</option>... @@ -880,7 +891,7 @@ module ActionView (content_tag(:select, select_html.html_safe, select_options) + "\n").html_safe end - # Builds a prompt option tag with supplied options or from default options + # Builds a prompt option tag with supplied options or from default options. # prompt_option_tag(:month, :prompt => 'Select month') # => "<option value="">Select month</option>" def prompt_option_tag(type, options) @@ -897,7 +908,7 @@ module ActionView prompt ? content_tag(:option, prompt, :value => '') : '' end - # Builds hidden input tag for date part and value + # Builds hidden input tag for date part and value. # build_hidden(:year, 2008) # => "<input id="post_written_on_1i" name="post[written_on(1i)]" type="hidden" value="2008" />" def build_hidden(type, value) @@ -909,7 +920,7 @@ module ActionView }.merge(@html_options.slice(:disabled))) + "\n").html_safe end - # Returns the name attribute for the input tag + # Returns the name attribute for the input tag. # => post[written_on(1i)] def input_name_from_type(type) prefix = @options[:prefix] || ActionView::Helpers::DateTimeSelector::DEFAULT_PREFIX @@ -923,7 +934,7 @@ module ActionView @options[:discard_type] ? prefix : "#{prefix}[#{field_name}]" end - # Returns the id attribute for the input tag + # Returns the id attribute for the input tag. # => "post_written_on_1i" def input_id_from_type(type) input_name_from_type(type).gsub(/([\[\(])|(\]\[)/, '_').gsub(/[\]\)]/, '') @@ -940,7 +951,7 @@ module ActionView select.html_safe end - # Returns the separator for a given datetime component + # Returns the separator for a given datetime component. def separator(type) case type when :year diff --git a/actionpack/lib/action_view/helpers/debug_helper.rb b/actionpack/lib/action_view/helpers/debug_helper.rb index cd67851642..c0cc7d347c 100644 --- a/actionpack/lib/action_view/helpers/debug_helper.rb +++ b/actionpack/lib/action_view/helpers/debug_helper.rb @@ -30,7 +30,7 @@ module ActionView begin Marshal::dump(object) "<pre class='debug_dump'>#{h(object.to_yaml).gsub(" ", " ")}</pre>".html_safe - rescue Exception => e # errors from Marshal or YAML + rescue Exception # errors from Marshal or YAML # Object couldn't be dumped, perhaps because of singleton methods -- this is the fallback "<code class='debug_dump'>#{h(object.inspect)}</code>".html_safe end diff --git a/actionpack/lib/action_view/helpers/form_helper.rb b/actionpack/lib/action_view/helpers/form_helper.rb index b6bb90a3ce..f22c466666 100644 --- a/actionpack/lib/action_view/helpers/form_helper.rb +++ b/actionpack/lib/action_view/helpers/form_helper.rb @@ -4,6 +4,7 @@ require 'action_view/helpers/tag_helper' require 'action_view/helpers/form_tag_helper' require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/hash/slice' +require 'active_support/core_ext/module/method_names' require 'active_support/core_ext/object/blank' require 'active_support/core_ext/string/output_safety' require 'active_support/core_ext/array/extract_options' @@ -48,7 +49,7 @@ module ActionView # <label for="person_last_name">Last name</label>: # <input id="person_last_name" name="person[last_name]" size="30" type="text" /><br /> # - # <input id="person_submit" name="commit" type="submit" value="Create Person" /> + # <input name="commit" type="submit" value="Create Person" /> # </form> # # As you see, the HTML reflects knowledge about the resource in several spots, @@ -79,7 +80,7 @@ module ActionView # <label for="person_last_name">Last name</label>: # <input id="person_last_name" name="person[last_name]" size="30" type="text" value="Smith" /><br /> # - # <input id="person_submit" name="commit" type="submit" value="Update Person" /> + # <input name="commit" type="submit" value="Update Person" /> # </form> # # Note that the endpoint, default values, and submit button label are tailored for <tt>@person</tt>. @@ -202,13 +203,13 @@ module ActionView # # is equivalent to something like: # - # <%= form_for @post, :as => :post, :url => post_path(@post), :html => { :class => "new_post", :id => "new_post" } do |f| %> + # <%= form_for @post, :as => :post, :url => posts_path, :html => { :class => "new_post", :id => "new_post" } do |f| %> # ... # <% end %> # # You can also overwrite the individual conventions, like this: # - # <%= form_for(@post, :url => super_post_path(@post)) do |f| %> + # <%= form_for(@post, :url => super_posts_path) do |f| %> # ... # <% end %> # @@ -219,9 +220,9 @@ module ActionView # <% end %> # # If you have an object that needs to be represented as a different - # parameter, like a Client that acts as a Person: + # parameter, like a Person that acts as a Client: # - # <%= form_for(@post, :as => :client) do |f| %> + # <%= form_for(@person, :as => :client) do |f| %> # ... # <% end %> # @@ -232,7 +233,7 @@ module ActionView # <% end %> # # If your resource has associations defined, for example, you want to add comments - # to the post given that the routes are set correctly: + # to the document given that the routes are set correctly: # # <%= form_for([@document, @comment]) do |f| %> # ... @@ -243,7 +244,7 @@ module ActionView # # === Setting the method # - # You can force the form to use the full array of HTTP verbs by setting + # You can force the form to use the full array of HTTP verbs by setting # # :method => (:get|:post|:put|:delete) # @@ -258,8 +259,8 @@ module ActionView # :remote => true # # in the options hash creates a form that will allow the unobtrusive JavaScript drivers to modify its - # behaviour. The expected default behaviour is an XMLHttpRequest in the background instead of the regular - # POST arrangement, but ultimately the behaviour is the choice of the JavaScript driver implementor. + # behavior. The expected default behavior is an XMLHttpRequest in the background instead of the regular + # POST arrangement, but ultimately the behavior is the choice of the JavaScript driver implementor. # Even though it's using JavaScript to serialize the form elements, the form submission will work just like # a regular submission as viewed by the receiving side (all elements available in <tt>params</tt>). # @@ -290,7 +291,7 @@ module ActionView # # Example: # - # <%= form(@post) do |f| %> + # <%= form_for(@post) do |f| %> # <% f.fields_for(:comments, :include_id => false) do |cf| %> # ... # <% end %> @@ -364,7 +365,7 @@ module ActionView apply_form_for_options!(record, options) end - options[:html][:remote] = options.delete(:remote) + options[:html][:remote] = options.delete(:remote) if options.has_key?(:remote) options[:html][:method] = options.delete(:method) if options.has_key?(:method) options[:html][:authenticity_token] = options.delete(:authenticity_token) @@ -517,6 +518,18 @@ module ActionView # end # end # + # Note that the <tt>projects_attributes=</tt> writer method is in fact + # required for fields_for to correctly identify <tt>:projects</tt> as a + # collection, and the correct indices to be set in the form markup. + # + # When projects is already an association on Person you can use + # +accepts_nested_attributes_for+ to define the writer method for you: + # + # class Person < ActiveRecord::Base + # has_many :projects + # accepts_nested_attributes_for :projects + # end + # # This model can now be used with a nested fields_for. The block given to # the nested fields_for call will be repeated for each instance in the # collection: @@ -584,8 +597,8 @@ module ActionView # <% end %> # ... # <% end %> - def fields_for(record, record_object = nil, options = {}, &block) - builder = instantiate_builder(record, record_object, options, &block) + def fields_for(record_name, record_object = nil, options = {}, &block) + builder = instantiate_builder(record_name, record_object, options, &block) output = capture(builder, &block) output.concat builder.hidden_field(:id) if output && options[:hidden_field_id] && !builder.emitted_hidden_id? output @@ -846,7 +859,28 @@ module ActionView InstanceTag.new(object_name, method, self, options.delete(:object)).to_radio_button_tag(tag_value, options) end - # Returns a text_field of type "search". + # Returns an input of type "search" for accessing a specified attribute (identified by +method+) on an object + # assigned to the template (identified by +object_name+). Inputs of type "search" may be styled differently by + # some browsers. + # + # ==== Examples + # + # search_field(:user, :name) + # # => <input id="user_name" name="user[name]" size="30" type="search" /> + # search_field(:user, :name, :autosave => false) + # # => <input autosave="false" id="user_name" name="user[name]" size="30" type="search" /> + # search_field(:user, :name, :results => 3) + # # => <input id="user_name" name="user[name]" results="3" size="30" type="search" /> + # # Assume request.host returns "www.example.com" + # search_field(:user, :name, :autosave => true) + # # => <input autosave="com.example.www" id="user_name" name="user[name]" results="10" size="30" type="search" /> + # search_field(:user, :name, :onsearch => true) + # # => <input id="user_name" incremental="true" name="user[name]" onsearch="true" size="30" type="search" /> + # search_field(:user, :name, :autosave => false, :onsearch => true) + # # => <input autosave="false" id="user_name" incremental="true" name="user[name]" onsearch="true" size="30" type="search" /> + # search_field(:user, :name, :autosave => true, :onsearch => true) + # # => <input autosave="com.example.www" id="user_name" incremental="true" name="user[name]" onsearch="true" results="10" size="30" type="search" /> + # def search_field(object_name, method, options = {}) options = options.stringify_keys @@ -865,17 +899,29 @@ module ActionView end # Returns a text_field of type "tel". + # + # telephone_field("user", "phone") + # # => <input id="user_phone" name="user[phone]" size="30" type="tel" /> + # def telephone_field(object_name, method, options = {}) InstanceTag.new(object_name, method, self, options.delete(:object)).to_input_field_tag("tel", options) end alias phone_field telephone_field # Returns a text_field of type "url". + # + # url_field("user", "homepage") + # # => <input id="user_homepage" size="30" name="user[homepage]" type="url" /> + # def url_field(object_name, method, options = {}) InstanceTag.new(object_name, method, self, options.delete(:object)).to_input_field_tag("url", options) end # Returns a text_field of type "email". + # + # email_field("user", "address") + # # => <input id="user_address" size="30" name="user[address]" type="email" /> + # def email_field(object_name, method, options = {}) InstanceTag.new(object_name, method, self, options.delete(:object)).to_input_field_tag("email", options) end @@ -898,16 +944,13 @@ module ActionView private - def instantiate_builder(record, *args, &block) - options = args.extract_options! - record_object = args.shift - - case record + def instantiate_builder(record_name, record_object, options, &block) + case record_name when String, Symbol object = record_object - object_name = record + object_name = record_name else - object = record + object = record_name object_name = ActiveModel::Naming.param_key(object) end @@ -1139,7 +1182,7 @@ module ActionView options["name"] ||= tag_name_with_index(@auto_index) options["id"] = options.fetch("id"){ tag_id_with_index(@auto_index) } else - options["name"] ||= tag_name + (options.has_key?('multiple') ? '[]' : '') + options["name"] ||= tag_name + (options['multiple'] ? '[]' : '') options["id"] = options.fetch("id"){ tag_id } end end @@ -1184,8 +1227,12 @@ module ActionView parent_builder.multipart = multipart if parent_builder end - def self.model_name - @model_name ||= Struct.new(:partial_path).new(name.demodulize.underscore.sub!(/_builder$/, '')) + def self._to_partial_path + @_to_partial_path ||= name.demodulize.underscore.sub!(/_builder$/, '') + end + + def to_partial_path + self.class._to_partial_path end def to_model @@ -1219,35 +1266,30 @@ module ActionView RUBY_EVAL end - def fields_for(record_or_name_or_array, *args, &block) - if options.has_key?(:index) - index = "[#{options[:index]}]" - elsif defined?(@auto_index) - self.object_name = @object_name.to_s.sub(/\[\]$/,"") - index = "[#{@auto_index}]" - else - index = "" - end - - args << {} unless args.last.is_a?(Hash) - args.last[:builder] ||= options[:builder] - args.last[:parent_builder] = self + def fields_for(record_name, record_object = nil, fields_options = {}, &block) + fields_options, record_object = record_object, nil if record_object.is_a?(Hash) && record_object.extractable_options? + fields_options[:builder] ||= options[:builder] + fields_options[:parent_builder] = self - case record_or_name_or_array + case record_name when String, Symbol - if nested_attributes_association?(record_or_name_or_array) - return fields_for_with_nested_attributes(record_or_name_or_array, args, block) - else - name = record_or_name_or_array + if nested_attributes_association?(record_name) + return fields_for_with_nested_attributes(record_name, record_object, fields_options, block) end else - object = record_or_name_or_array.is_a?(Array) ? record_or_name_or_array.last : record_or_name_or_array - name = ActiveModel::Naming.param_key(object) - args.unshift(object) + record_object = record_name.is_a?(Array) ? record_name.last : record_name + record_name = ActiveModel::Naming.param_key(record_object) + end + + index = if options.has_key?(:index) + "[#{options[:index]}]" + elsif defined?(@auto_index) + self.object_name = @object_name.to_s.sub(/\[\]$/,"") + "[#{@auto_index}]" end - name = "#{object_name}#{index}[#{name}]" + record_name = "#{object_name}#{index}[#{record_name}]" - @template.fields_for(name, *args, &block) + @template.fields_for(record_name, record_object, fields_options, &block) end def label(method, text = nil, options = {}, &block) @@ -1336,10 +1378,8 @@ module ActionView @object.respond_to?("#{association_name}_attributes=") end - def fields_for_with_nested_attributes(association_name, args, block) + def fields_for_with_nested_attributes(association_name, association, options, block) name = "#{object_name}[#{association_name}_attributes]" - options = args.extract_options! - association = args.shift association = convert_to_model(association) if association.respond_to?(:persisted?) diff --git a/actionpack/lib/action_view/helpers/form_options_helper.rb b/actionpack/lib/action_view/helpers/form_options_helper.rb index 7698602022..d636702111 100644 --- a/actionpack/lib/action_view/helpers/form_options_helper.rb +++ b/actionpack/lib/action_view/helpers/form_options_helper.rb @@ -105,7 +105,10 @@ module ActionView # Create a select tag and a series of contained option tags for the provided object and method. # The option currently held by the object will be selected, provided that the object is available. - # See options_for_select for the required format of the choices parameter. + # + # There are two possible formats for the choices parameter, corresponding to other helpers' output: + # * A flat collection: see options_for_select + # * A nested collection: see grouped_options_for_select # # Example with @post.person_id => 1: # select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, { :include_blank => true }) @@ -125,9 +128,31 @@ module ActionView # This allows the user to submit a form page more than once with the expected results of creating multiple records. # In addition, this allows a single partial to be used to generate form inputs for both edit and create forms. # - # By default, <tt>post.person_id</tt> is the selected option. Specify <tt>:selected => value</tt> to use a different selection + # By default, <tt>post.person_id</tt> is the selected option. Specify <tt>:selected => value</tt> to use a different selection # or <tt>:selected => nil</tt> to leave all options unselected. Similarly, you can specify values to be disabled in the option # tags by specifying the <tt>:disabled</tt> option. This can either be a single value or an array of values to be disabled. + # + # ==== Gotcha + # + # The HTML specification says when +multiple+ parameter passed to select and all options got deselected + # web browsers do not send any value to server. Unfortunately this introduces a gotcha: + # if an +User+ model has many +roles+ and have +role_ids+ accessor, and in the form that edits roles of the user + # the user deselects all roles from +role_ids+ multiple select box, no +role_ids+ parameter is sent. So, + # any mass-assignment idiom like + # + # @user.update_attributes(params[:user]) + # + # wouldn't update roles. + # + # To prevent this the helper generates an auxiliary hidden field before + # every multiple select. The hidden field has the same name as multiple select and blank value. + # + # This way, the client either sends only the hidden field (representing + # the deselected multiple select box), or both fields. Since the HTML specification + # says key/value pairs have to be sent in the same order they appear in the + # form, and parameters extraction gets the last occurrence of any repeated + # key in the query string, that works for ordinary forms. + # def select(object, method, choices, options = {}, html_options = {}) InstanceTag.new(object, method, self, options.delete(:object)).to_select_tag(choices, options, html_options) end @@ -255,7 +280,7 @@ module ActionView # Accepts a container (hash, array, enumerable, your type) and returns a string of option tags. Given a container # where the elements respond to first and last (such as a two-element array), the "lasts" serve as option values and # the "firsts" as option text. Hashes are turned into this form automatically, so the keys become "firsts" and values - # become lasts. If +selected+ is specified, the matching "last" or element will get the selected option-tag. +selected+ + # become lasts. If +selected+ is specified, the matching "last" or element will get the selected option-tag. +selected+ # may also be an array of values to be selected when using a multiple select. # # Examples (call, result): @@ -274,10 +299,10 @@ module ActionView # You can optionally provide html attributes as the last element of the array. # # Examples: - # options_for_select([ "Denmark", ["USA", {:class=>'bold'}], "Sweden" ], ["USA", "Sweden"]) + # options_for_select([ "Denmark", ["USA", {:class => 'bold'}], "Sweden" ], ["USA", "Sweden"]) # <option value="Denmark">Denmark</option>\n<option value="USA" class="bold" selected="selected">USA</option>\n<option value="Sweden" selected="selected">Sweden</option> # - # options_for_select([["Dollar", "$", {:class=>"bold"}], ["Kroner", "DKK", {:onclick => "alert('HI');"}]]) + # options_for_select([["Dollar", "$", {:class => "bold"}], ["Kroner", "DKK", {:onclick => "alert('HI');"}]]) # <option value="$" class="bold">Dollar</option>\n<option value="DKK" onclick="alert('HI');">Kroner</option> # # If you wish to specify disabled option tags, set +selected+ to be a hash, with <tt>:disabled</tt> being either a value @@ -298,12 +323,12 @@ module ActionView return container if String === container selected, disabled = extract_selected_and_disabled(selected).map do | r | - Array.wrap(r).map(&:to_s) + Array.wrap(r).map { |item| item.to_s } end container.map do |element| html_attributes = option_html_attributes(element) - text, value = option_text_and_value(element).map(&:to_s) + text, value = option_text_and_value(element).map { |item| item.to_s } selected_attribute = ' selected="selected"' if option_value_selected?(value, selected) disabled_attribute = ' disabled="disabled"' if disabled && option_value_selected?(value, disabled) %(<option value="#{ERB::Util.html_escape(value)}"#{selected_attribute}#{disabled_attribute}#{html_attributes}>#{ERB::Util.html_escape(text)}</option>) @@ -406,13 +431,13 @@ module ActionView # wraps them with <tt><optgroup></tt> tags. # # Parameters: - # * +grouped_options+ - Accepts a nested array or hash of strings. The first value serves as the + # * +grouped_options+ - Accepts a nested array or hash of strings. The first value serves as the # <tt><optgroup></tt> label while the second value must be an array of options. The second value can be a # nested array of text-value pairs. See <tt>options_for_select</tt> for more info. # Ex. ["North America",[["United States","US"],["Canada","CA"]]] # * +selected_key+ - A value equal to the +value+ attribute for one of the <tt><option></tt> tags, # which will have the +selected+ attribute set. Note: It is possible for this value to match multiple options - # as you might have the same option in multiple groups. Each will then get <tt>selected="selected"</tt>. + # as you might have the same option in multiple groups. Each will then get <tt>selected="selected"</tt>. # * +prompt+ - set to true or a prompt string. When the select element doesn't have a value yet, this # prepends an option with a generic prompt - "Please select" - or the given prompt string. # @@ -427,7 +452,7 @@ module ActionView # # Sample usage (Hash): # grouped_options = { - # 'North America' => [['United States','US], 'Canada'], + # 'North America' => [['United States','US'], 'Canada'], # 'Europe' => ['Denmark','Germany','France'] # } # grouped_options_for_select(grouped_options) @@ -552,43 +577,33 @@ module ActionView include FormOptionsHelper def to_select_tag(choices, options, html_options) - html_options = html_options.stringify_keys - add_default_name_and_id(html_options) - value = value(object) - selected_value = options.has_key?(:selected) ? options[:selected] : value - disabled_value = options.has_key?(:disabled) ? options[:disabled] : nil - content_tag("select", add_options(options_for_select(choices, :selected => selected_value, :disabled => disabled_value), options, selected_value), html_options) + selected_value = options.has_key?(:selected) ? options[:selected] : value(object) + + if !choices.empty? && Array === choices.first + option_tags = grouped_options_for_select(choices, :selected => selected_value, :disabled => options[:disabled]) + else + option_tags = options_for_select(choices, :selected => selected_value, :disabled => options[:disabled]) + end + + select_content_tag(option_tags, options, html_options) end def to_collection_select_tag(collection, value_method, text_method, options, html_options) - html_options = html_options.stringify_keys - add_default_name_and_id(html_options) - value = value(object) - disabled_value = options.has_key?(:disabled) ? options[:disabled] : nil - selected_value = options.has_key?(:selected) ? options[:selected] : value - content_tag( - "select", add_options(options_from_collection_for_select(collection, value_method, text_method, :selected => selected_value, :disabled => disabled_value), options, value), html_options + selected_value = options.has_key?(:selected) ? options[:selected] : value(object) + select_content_tag( + options_from_collection_for_select(collection, value_method, text_method, :selected => selected_value, :disabled => options[:disabled]), options, html_options ) end def to_grouped_collection_select_tag(collection, group_method, group_label_method, option_key_method, option_value_method, options, html_options) - html_options = html_options.stringify_keys - add_default_name_and_id(html_options) - value = value(object) - content_tag( - "select", add_options(option_groups_from_collection_for_select(collection, group_method, group_label_method, option_key_method, option_value_method, value), options, value), html_options + select_content_tag( + option_groups_from_collection_for_select(collection, group_method, group_label_method, option_key_method, option_value_method, value(object)), options, html_options ) end def to_time_zone_select_tag(priority_zones, options, html_options) - html_options = html_options.stringify_keys - add_default_name_and_id(html_options) - value = value(object) - content_tag("select", - add_options( - time_zone_options_for_select(value || options[:default], priority_zones, options[:model] || ActiveSupport::TimeZone), - options, value - ), html_options + select_content_tag( + time_zone_options_for_select(value(object) || options[:default], priority_zones, options[:model] || ActiveSupport::TimeZone), options, html_options ) end @@ -603,6 +618,17 @@ module ActionView end option_tags.html_safe end + + def select_content_tag(option_tags, options, html_options) + html_options = html_options.stringify_keys + add_default_name_and_id(html_options) + select = content_tag("select", add_options(option_tags, options, value(object)), html_options) + if html_options["multiple"] + tag("input", :disabled => html_options["disabled"], :name => html_options["name"], :type => "hidden", :value => "") + select + else + select + end + end end class FormBuilder diff --git a/actionpack/lib/action_view/helpers/form_tag_helper.rb b/actionpack/lib/action_view/helpers/form_tag_helper.rb index 9e0f8f32b7..13b9dc8553 100644 --- a/actionpack/lib/action_view/helpers/form_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/form_tag_helper.rb @@ -30,7 +30,7 @@ module ActionView # (by passing <tt>false</tt>). # * A list of parameters to feed to the URL the form will be posted to. # * <tt>:remote</tt> - If set to true, will allow the Unobtrusive JavaScript drivers to control the - # submit behaviour. By default this behaviour is an ajax submit. + # submit behavior. By default this behavior is an ajax submit. # # ==== Examples # form_tag('/posts') @@ -56,8 +56,8 @@ module ActionView # form_tag('http://far.away.com/form', :authenticity_token => "cf50faa3fe97702ca1ae") # # form with custom authenticity token # - def form_tag(url_for_options = {}, options = {}, *parameters_for_url, &block) - html_options = html_options_for_form(url_for_options, options, *parameters_for_url) + def form_tag(url_for_options = {}, options = {}, &block) + html_options = html_options_for_form(url_for_options, options) if block_given? form_tag_in_block(html_options, &block) else @@ -82,22 +82,22 @@ module ActionView # select_tag "people", options_from_collection_for_select(@people, "id", "name") # # <select id="people" name="people"><option value="1">David</option></select> # - # select_tag "people", "<option>David</option>" + # select_tag "people", "<option>David</option>".html_safe # # => <select id="people" name="people"><option>David</option></select> # - # select_tag "count", "<option>1</option><option>2</option><option>3</option><option>4</option>" + # select_tag "count", "<option>1</option><option>2</option><option>3</option><option>4</option>".html_safe # # => <select id="count" name="count"><option>1</option><option>2</option> # # <option>3</option><option>4</option></select> # - # select_tag "colors", "<option>Red</option><option>Green</option><option>Blue</option>", :multiple => true + # select_tag "colors", "<option>Red</option><option>Green</option><option>Blue</option>".html_safe, :multiple => true # # => <select id="colors" multiple="multiple" name="colors[]"><option>Red</option> # # <option>Green</option><option>Blue</option></select> # - # select_tag "locations", "<option>Home</option><option selected="selected">Work</option><option>Out</option>" + # select_tag "locations", "<option>Home</option><option selected="selected">Work</option><option>Out</option>".html_safe # # => <select id="locations" name="locations"><option>Home</option><option selected='selected'>Work</option> # # <option>Out</option></select> # - # select_tag "access", "<option>Read</option><option>Write</option>", :multiple => true, :class => 'form_input' + # select_tag "access", "<option>Read</option><option>Write</option>".html_safe, :multiple => true, :class => 'form_input' # # => <select class="form_input" id="access" multiple="multiple" name="access[]"><option>Read</option> # # <option>Write</option></select> # @@ -107,7 +107,7 @@ module ActionView # select_tag "people", options_from_collection_for_select(@people, "id", "name"), :prompt => "Select something" # # => <select id="people" name="people"><option value="">Select something</option><option value="1">David</option></select> # - # select_tag "destination", "<option>NYC</option><option>Paris</option><option>Rome</option>", :disabled => true + # select_tag "destination", "<option>NYC</option><option>Paris</option><option>Rome</option>".html_safe, :disabled => true # # => <select disabled="disabled" id="destination" name="destination"><option>NYC</option> # # <option>Paris</option><option>Rome</option></select> def select_tag(name, option_tags = nil, options = {}) @@ -177,9 +177,12 @@ module ActionView # label_tag 'name', nil, :class => 'small_label' # # => <label for="name" class="small_label">Name</label> def label_tag(name = nil, content_or_options = nil, options = nil, &block) - options = content_or_options if block_given? && content_or_options.is_a?(Hash) - options ||= {} - options.stringify_keys! + if block_given? && content_or_options.is_a?(Hash) + options = content_or_options = content_or_options.stringify_keys + else + options ||= {} + options = options.stringify_keys + end options["for"] = sanitize_to_id(name) unless name.blank? || options.has_key?("for") content_tag :label, content_or_options || name.to_s.humanize, options, &block end @@ -204,7 +207,7 @@ module ActionView text_field_tag(name, value, options.stringify_keys.update("type" => "hidden")) end - # Creates a file upload field. If you are using file uploads then you will also need + # Creates a file upload field. If you are using file uploads then you will also need # to set the multipart option for the form tag: # # <%= form_tag '/upload', :multipart => true do %> @@ -304,7 +307,7 @@ module ActionView # text_area_tag 'comment', nil, :class => 'comment_input' # # => <textarea class="comment_input" id="comment" name="comment"></textarea> def text_area_tag(name, content = nil, options = {}) - options.stringify_keys! + options = options.stringify_keys if size = options.delete("size") options["cols"], options["rows"] = size.split("x") if size.respond_to?(:split) @@ -407,7 +410,7 @@ module ActionView # data-confirm="Are you sure?" /> # def submit_tag(value = "Save changes", options = {}) - options.stringify_keys! + options = options.stringify_keys if disable_with = options.delete("disable_with") options["data-disable-with"] = disable_with @@ -417,7 +420,7 @@ module ActionView options["data-confirm"] = confirm end - tag :input, { "type" => "submit", "name" => "commit", "value" => value }.update(options.stringify_keys) + tag :input, { "type" => "submit", "name" => "commit", "value" => value }.update(options) end # Creates a button element that defines a <tt>submit</tt> button, @@ -458,7 +461,7 @@ module ActionView def button_tag(content_or_options = nil, options = nil, &block) options = content_or_options if block_given? && content_or_options.is_a?(Hash) options ||= {} - options.stringify_keys! + options = options.stringify_keys if disable_with = options.delete("disable_with") options["data-disable-with"] = disable_with @@ -497,13 +500,13 @@ module ActionView # image_submit_tag("agree.png", :disabled => true, :class => "agree_disagree_button") # # => <input class="agree_disagree_button" disabled="disabled" src="/images/agree.png" type="image" /> def image_submit_tag(source, options = {}) - options.stringify_keys! + options = options.stringify_keys if confirm = options.delete("confirm") options["data-confirm"] = confirm end - tag :input, { "type" => "image", "src" => path_to_image(source) }.update(options.stringify_keys) + tag :input, { "type" => "image", "src" => path_to_image(source) }.update(options) end # Creates a field set for grouping HTML form elements. @@ -597,13 +600,19 @@ module ActionView number_field_tag(name, value, options.stringify_keys.update("type" => "range")) end + # Creates the hidden UTF8 enforcer tag. Override this method in a helper + # to customize the tag. + def utf8_enforcer_tag + tag(:input, :type => "hidden", :name => "utf8", :value => "✓".html_safe) + end + private - def html_options_for_form(url_for_options, options, *parameters_for_url) + def html_options_for_form(url_for_options, options) options.stringify_keys.tap do |html_options| html_options["enctype"] = "multipart/form-data" if html_options.delete("multipart") # The following URL is unescaped, this is just a hash of options, and it is the # responsibility of the caller to escape all the values. - html_options["action"] = url_for(url_for_options, *parameters_for_url) + html_options["action"] = url_for(url_for_options) html_options["accept-charset"] = "UTF-8" html_options["data-remote"] = true if html_options.delete("remote") html_options["authenticity_token"] = html_options.delete("authenticity_token") if html_options.has_key?("authenticity_token") @@ -611,9 +620,6 @@ module ActionView end def extra_tags_for_form(html_options) - snowman_tag = tag(:input, :type => "hidden", - :name => "utf8", :value => "✓".html_safe) - authenticity_token = html_options.delete("authenticity_token") method = html_options.delete("method").to_s @@ -629,7 +635,7 @@ module ActionView tag(:input, :type => "hidden", :name => "_method", :value => method) + token_tag(authenticity_token) end - tags = snowman_tag << method_tag + tags = utf8_enforcer_tag << method_tag content_tag(:div, tags, :style => 'margin:0;padding:0;display:inline') end @@ -650,7 +656,7 @@ module ActionView if token == false || !protect_against_forgery? '' else - token = form_authenticity_token if token.nil? + token ||= form_authenticity_token tag(:input, :type => "hidden", :name => request_forgery_protection_token.to_s, :value => token) end end diff --git a/actionpack/lib/action_view/helpers/javascript_helper.rb b/actionpack/lib/action_view/helpers/javascript_helper.rb index d7228bab67..1adcd716f8 100644 --- a/actionpack/lib/action_view/helpers/javascript_helper.rb +++ b/actionpack/lib/action_view/helpers/javascript_helper.rb @@ -1,4 +1,5 @@ require 'action_view/helpers/tag_helper' +require 'active_support/core_ext/string/encoding' module ActionView module Helpers @@ -10,15 +11,24 @@ module ActionView "\n" => '\n', "\r" => '\n', '"' => '\\"', - "'" => "\\'" } + "'" => "\\'" + } - # Escape carrier returns and single and double quotes for JavaScript segments. + if "ruby".encoding_aware? + JS_ESCAPE_MAP["\342\200\250".force_encoding('UTF-8').encode!] = '
' + else + JS_ESCAPE_MAP["\342\200\250"] = '
' + end + + # Escapes carriage returns and single and double quotes for JavaScript segments. + # # Also available through the alias j(). This is particularly helpful in JavaScript responses, like: # # $('some_element').replaceWith('<%=j render 'some/element_template' %>'); def escape_javascript(javascript) if javascript - javascript.gsub(/(\\|<\/|\r\n|[\n\r"'])/) { JS_ESCAPE_MAP[$1] } + result = javascript.gsub(/(\\|<\/|\r\n|\342\200\250|[\n\r"'])/u) {|match| JS_ESCAPE_MAP[match] } + javascript.html_safe? ? result.html_safe : result else '' end diff --git a/actionpack/lib/action_view/helpers/number_helper.rb b/actionpack/lib/action_view/helpers/number_helper.rb index 63d13a0f0b..ec6c2c8db3 100644 --- a/actionpack/lib/action_view/helpers/number_helper.rb +++ b/actionpack/lib/action_view/helpers/number_helper.rb @@ -1,3 +1,5 @@ +# encoding: utf-8 + require 'active_support/core_ext/big_decimal/conversions' require 'active_support/core_ext/float/rounding' require 'active_support/core_ext/object/blank' @@ -211,7 +213,7 @@ module ActionView defaults = I18n.translate(:'number.format', :locale => options[:locale], :default => {}) options = options.reverse_merge(defaults) - parts = number.to_s.split('.') + parts = number.to_s.to_str.split('.') parts[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{options[:delimiter]}") parts.join(options[:separator]).html_safe @@ -342,7 +344,7 @@ module ActionView options[:strip_insignificant_zeros] = true if not options.key?(:strip_insignificant_zeros) storage_units_format = I18n.translate(:'number.human.storage_units.format', :locale => options[:locale], :raise => true) - + base = options[:prefix] == :si ? 1000 : 1024 if number.to_i < base diff --git a/actionpack/lib/action_view/helpers/record_tag_helper.rb b/actionpack/lib/action_view/helpers/record_tag_helper.rb index 142a25f118..b351302d01 100644 --- a/actionpack/lib/action_view/helpers/record_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/record_tag_helper.rb @@ -17,6 +17,19 @@ module ActionView # # <div id="person_123" class="person foo"> Joe Bloggs </div> # + # You can also pass an array of Active Record objects, which will then + # get iterated over and yield each record as an argument for the block. + # For example: + # + # <%= div_for(@people, :class => "foo") do |person| %> + # <%= person.name %> + # <% end %> + # + # produces: + # + # <div id="person_123" class="person foo"> Joe Bloggs </div> + # <div id="person_124" class="person foo"> Jane Bloggs </div> + # def div_for(record, *args, &block) content_tag_for(:div, record, *args, &block) end @@ -42,6 +55,21 @@ module ActionView # # <tr id="foo_person_123" class="person">... # + # You can also pass an array of objects which this method will loop through + # and yield the current object to the supplied block, reducing the need for + # having to iterate through the object (using <tt>each</tt>) beforehand. + # For example (assuming @people is an array of Person objects): + # + # <%= content_tag_for(:tr, @people) do |person| %> + # <td><%= person.first_name %></td> + # <td><%= person.last_name %></td> + # <% end %> + # + # produces: + # + # <tr id="person_123" class="person">...</tr> + # <tr id="person_124" class="person">...</tr> + # # content_tag_for also accepts a hash of options, which will be converted to # additional HTML attributes. If you specify a <tt>:class</tt> value, it will be combined # with the default class name for your object. For example: @@ -52,12 +80,30 @@ module ActionView # # <li id="person_123" class="person bar">... # - def content_tag_for(tag_name, record, prefix = nil, options = nil, &block) - options, prefix = prefix, nil if prefix.is_a?(Hash) - options ||= {} - options.merge!({ :class => "#{dom_class(record, prefix)} #{options[:class]}".strip, :id => dom_id(record, prefix) }) - content_tag(tag_name, options, &block) + def content_tag_for(tag_name, single_or_multiple_records, prefix = nil, options = nil, &block) + if single_or_multiple_records.respond_to?(:to_ary) + single_or_multiple_records.to_ary.map do |single_record| + capture { content_tag_for_single_record(tag_name, single_record, prefix, options, &block) } + end.join("\n").html_safe + else + content_tag_for_single_record(tag_name, single_or_multiple_records, prefix, options, &block) + end end + + private + + # Called by <tt>content_tag_for</tt> internally to render a content tag + # for each record. + def content_tag_for_single_record(tag_name, record, prefix, options, &block) + options, prefix = prefix, nil if prefix.is_a?(Hash) + options ||= {} + options.merge!({ :class => "#{dom_class(record, prefix)} #{options[:class]}".strip, :id => dom_id(record, prefix) }) + if block.arity == 0 + content_tag(tag_name, capture(&block), options) + else + content_tag(tag_name, capture(record, &block), options) + end + end end end end diff --git a/actionpack/lib/action_view/helpers/rendering_helper.rb b/actionpack/lib/action_view/helpers/rendering_helper.rb index 47efdded42..626e1a1ab7 100644 --- a/actionpack/lib/action_view/helpers/rendering_helper.rb +++ b/actionpack/lib/action_view/helpers/rendering_helper.rb @@ -8,7 +8,7 @@ module ActionView module RenderingHelper # Returns the result of a render that's dictated by the options hash. The primary options are: # - # * <tt>:partial</tt> - See ActionView::Partials. + # * <tt>:partial</tt> - See <tt>ActionView::PartialRenderer</tt>. # * <tt>:file</tt> - Renders an explicit template file (this used to be the old default), add :locals to pass in those. # * <tt>:inline</tt> - Renders an inline template similar to how it's done in the controller. # * <tt>:text</tt> - Renders the text passed in out. @@ -87,4 +87,4 @@ module ActionView end end end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_view/helpers/sanitize_helper.rb b/actionpack/lib/action_view/helpers/sanitize_helper.rb index 841be0a567..bcc8f6fbcb 100644 --- a/actionpack/lib/action_view/helpers/sanitize_helper.rb +++ b/actionpack/lib/action_view/helpers/sanitize_helper.rb @@ -14,13 +14,13 @@ module ActionView # # It also strips href/src tags with invalid protocols, like javascript: especially. # It does its best to counter any tricks that hackers may use, like throwing in - # unicode/ascii/hex values to get past the javascript: filters. Check out + # unicode/ascii/hex values to get past the javascript: filters. Check out # the extensive test suite. # # <%= sanitize @article.body %> # # You can add or remove tags/attributes if you want to customize it a bit. - # See ActionView::Base for full docs on the available options. You can add + # See ActionView::Base for full docs on the available options. You can add # tags/attributes for single uses of +sanitize+ by passing either the # <tt>:attributes</tt> or <tt>:tags</tt> options: # @@ -66,7 +66,7 @@ module ActionView self.class.white_list_sanitizer.sanitize_css(style) end - # Strips all HTML tags from the +html+, including comments. This uses the + # Strips all HTML tags from the +html+, including comments. This uses the # html-scanner tokenizer and so its HTML parsing ability is limited by # that of html-scanner. # @@ -142,7 +142,7 @@ module ActionView white_list_sanitizer.protocol_separator = value end - # Gets the HTML::FullSanitizer instance used by +strip_tags+. Replace with + # Gets the HTML::FullSanitizer instance used by +strip_tags+. Replace with # any object that responds to +sanitize+. # # class Application < Rails::Application @@ -153,7 +153,7 @@ module ActionView @full_sanitizer ||= HTML::FullSanitizer.new end - # Gets the HTML::LinkSanitizer instance used by +strip_links+. Replace with + # Gets the HTML::LinkSanitizer instance used by +strip_links+. Replace with # any object that responds to +sanitize+. # # class Application < Rails::Application diff --git a/actionpack/lib/action_view/helpers/sprockets_helper.rb b/actionpack/lib/action_view/helpers/sprockets_helper.rb deleted file mode 100644 index ab98da9624..0000000000 --- a/actionpack/lib/action_view/helpers/sprockets_helper.rb +++ /dev/null @@ -1,69 +0,0 @@ -require 'uri' -require 'action_view/helpers/asset_paths' - -module ActionView - module Helpers - module SprocketsHelper - def asset_path(source, default_ext = nil) - sprockets_asset_paths.compute_public_path(source, 'assets', default_ext, true) - end - - def sprockets_javascript_include_tag(source, options = {}) - options = { - 'type' => "text/javascript", - 'src' => asset_path(source, 'js') - }.merge(options.stringify_keys) - - content_tag 'script', "", options - end - - def sprockets_stylesheet_link_tag(source, options = {}) - options = { - 'rel' => "stylesheet", - 'type' => "text/css", - 'media' => "screen", - 'href' => asset_path(source, 'css') - }.merge(options.stringify_keys) - - tag 'link', options - end - - private - - def sprockets_asset_paths - @sprockets_asset_paths ||= begin - config = self.config if respond_to?(:config) - controller = self.controller if respond_to?(:controller) - SprocketsHelper::AssetPaths.new(config, controller) - end - end - - class AssetPaths < ActionView::Helpers::AssetPaths #:nodoc: - def rewrite_asset_path(source, dir) - if source[0] == ?/ - source - else - assets.path(source, performing_caching?, dir) - end - end - - def rewrite_extension(source, dir, ext) - if ext && File.extname(source).empty? - "#{source}.#{ext}" - else - source - end - end - - def assets - Rails.application.assets - end - - # When included in Sprockets::Context, we need to ask the top-level config as the controller is not available - def performing_caching? - @config ? @config.perform_caching : Rails.application.config.action_controller.perform_caching - end - end - end - end -end
\ No newline at end of file diff --git a/actionpack/lib/action_view/helpers/tag_helper.rb b/actionpack/lib/action_view/helpers/tag_helper.rb index 786af5ca58..8c33ef09fa 100644 --- a/actionpack/lib/action_view/helpers/tag_helper.rb +++ b/actionpack/lib/action_view/helpers/tag_helper.rb @@ -94,7 +94,7 @@ module ActionView end end - # Returns a CDATA section with the given +content+. CDATA sections + # Returns a CDATA section with the given +content+. CDATA sections # are used to escape blocks of text containing characters which would # otherwise be recognized as markup. CDATA sections begin with the string # <tt><![CDATA[</tt> and end with (and may not contain) the string <tt>]]></tt>. diff --git a/actionpack/lib/action_view/helpers/text_helper.rb b/actionpack/lib/action_view/helpers/text_helper.rb index ca09c77b5c..21074efe86 100644 --- a/actionpack/lib/action_view/helpers/text_helper.rb +++ b/actionpack/lib/action_view/helpers/text_helper.rb @@ -232,7 +232,7 @@ module ActionView # considered as a linebreak and a <tt><br /></tt> tag is appended. This # method does not remove the newlines from the +text+. # - # You can pass any HTML attributes into <tt>html_options</tt>. These + # You can pass any HTML attributes into <tt>html_options</tt>. These # will be added to all created paragraphs. # # ==== Options @@ -255,9 +255,11 @@ module ActionView # simple_format("<span>I'm allowed!</span> It's true.", {}, :sanitize => false) # # => "<p><span>I'm allowed!</span> It's true.</p>" def simple_format(text, html_options={}, options={}) - text = ''.html_safe if text.nil? + text = '' if text.nil? + text = text.dup start_tag = tag('p', html_options, true) text = sanitize(text) unless options[:sanitize] == false + text = text.to_str text.gsub!(/\r\n?/, "\n") # \r\n and \r -> \n text.gsub!(/\n\n+/, "</p>\n\n#{start_tag}") # 2+ newline -> paragraph text.gsub!(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br @@ -267,7 +269,7 @@ module ActionView # Creates a Cycle object whose _to_s_ method cycles through elements of an # array every time it is called. This can be used for example, to alternate - # classes for table rows. You can use named cycles to allow nesting in loops. + # classes for table rows. You can use named cycles to allow nesting in loops. # Passing a Hash as the last parameter with a <tt>:name</tt> key will create a # named cycle. The default name for a cycle without a +:name+ key is # <tt>"default"</tt>. You can manually reset a cycle by calling reset_cycle @@ -279,7 +281,7 @@ module ActionView # @items = [1,2,3,4] # <table> # <% @items.each do |item| %> - # <tr class="<%= cycle("even", "odd") -%>"> + # <tr class="<%= cycle("odd", "even") -%>"> # <td>item</td> # </tr> # <% end %> diff --git a/actionpack/lib/action_view/helpers/translation_helper.rb b/actionpack/lib/action_view/helpers/translation_helper.rb index ec9bdd5320..be64dc823e 100644 --- a/actionpack/lib/action_view/helpers/translation_helper.rb +++ b/actionpack/lib/action_view/helpers/translation_helper.rb @@ -5,7 +5,7 @@ module I18n class ExceptionHandler include Module.new { def call(exception, locale, key, options) - exception.is_a?(MissingTranslation) ? super.html_safe : super + exception.is_a?(MissingTranslation) && options[:rescue_format] == :html ? super.html_safe : super end } end @@ -15,17 +15,17 @@ module ActionView # = Action View Translation Helpers module Helpers module TranslationHelper - # Delegates to I18n#translate but also performs three additional functions. + # Delegates to <tt>I18n#translate</tt> but also performs three additional functions. # - # First, it'll pass the :rescue_format => :html option to I18n so that any - # thrown MissingTranslation messages will be turned into inline spans that + # First, it'll pass the <tt>:rescue_format => :html</tt> option to I18n so that any + # thrown +MissingTranslation+ messages will be turned into inline spans that # # * have a "translation-missing" class set, # * contain the missing key as a title attribute and # * a titleized version of the last key segment as a text. # # E.g. the value returned for a missing translation key :"blog.post.title" will be - # <span class="translation_missing" title="translation missing: blog.post.title">Title</span>. + # <span class="translation_missing" title="translation missing: en.blog.post.title">Title</span>. # This way your views will display rather reasonable strings but it will still # be easy to spot missing translations. # @@ -54,7 +54,7 @@ module ActionView end alias :t :translate - # Delegates to I18n.localize with no additional functionality. + # Delegates to <tt>I18n.localize</tt> with no additional functionality. def localize(*args) I18n.localize(*args) end diff --git a/actionpack/lib/action_view/helpers/url_helper.rb b/actionpack/lib/action_view/helpers/url_helper.rb index ffa9a5bb0b..0c2e1aa3a9 100644 --- a/actionpack/lib/action_view/helpers/url_helper.rb +++ b/actionpack/lib/action_view/helpers/url_helper.rb @@ -55,9 +55,9 @@ module ActionView # # ==== Relying on named routes # - # If you instead of a hash pass a record (like an Active Record or Active Resource) as the options parameter, - # you'll trigger the named route for that record. The lookup will happen on the name of the class. So passing - # a Workshop object will attempt to use the +workshop_path+ route. If you have a nested route, such as + # Passing a record (like an Active Record or Active Resource) instead of a Hash as the options parameter will + # trigger the named route for that record. The lookup will happen on the name of the class. So passing a + # Workshop object will attempt to use the +workshop_path+ route. If you have a nested route, such as # +admin_workshop_path+ you'll have to call that explicitly (it's impossible for +url_for+ to guess that route). # # ==== Examples @@ -112,13 +112,13 @@ module ActionView end end - # Creates a link tag of the given +name+ using a URL created by the set - # of +options+. See the valid options in the documentation for - # +url_for+. It's also possible to pass a string instead - # of an options hash to get a link tag that uses the value of the string as the - # href for the link, or use <tt>:back</tt> to link to the referrer - a JavaScript back - # link will be used in place of a referrer if none exists. If +nil+ is passed as - # a name, the link itself will become the name. + # Creates a link tag of the given +name+ using a URL created by the set of +options+. + # See the valid options in the documentation for +url_for+. It's also possible to + # pass a String instead of an options hash, which generates a link tag that uses the + # value of the String as the href for the link. Using a <tt>:back</tt> Symbol instead + # of an options hash will generate a link to the referrer (a JavaScript back link + # will be used in place of a referrer if none exists). If +nil+ is passed as the name + # the value of the link itself will become the name. # # ==== Signatures # @@ -160,7 +160,7 @@ module ActionView # # ==== Examples # Because it relies on +url_for+, +link_to+ supports both older-style controller/action/id arguments - # and newer RESTful routes. Current Rails style favors RESTful routes whenever possible, so base + # and newer RESTful routes. Current Rails style favors RESTful routes whenever possible, so base # your application on resources and use # # link_to "Profile", profile_path(@profile) @@ -268,7 +268,7 @@ module ActionView # to change the HTTP verb used to submit the form. # # ==== Options - # The +options+ hash accepts the same options as url_for. + # The +options+ hash accepts the same options as +url_for+. # # There are a few special +html_options+: # * <tt>:method</tt> - Symbol of HTTP verb. Supported verbs are <tt>:post</tt>, <tt>:get</tt>, @@ -278,7 +278,8 @@ module ActionView # prompt with the question specified. If the user accepts, the link is # processed normally, otherwise no action is taken. # * <tt>:remote</tt> - If set to true, will allow the Unobtrusive JavaScript drivers to control the - # submit behaviour. By default this behaviour is an ajax submit. + # submit behavior. By default this behavior is an ajax submit. + # * <tt>:form</tt> - This hash will be form attributes # * <tt>:form_class</tt> - This controls the class of the form within which the submit button will # be placed # @@ -295,6 +296,12 @@ module ActionView # # </form>" # # + # <%= button_to "Create", :action => "create", :remote => true, :form => { "data-type" => "json" } %> + # # => "<form method="post" action="/images/create" class="button_to" data-remote="true" data-type="json"> + # # <div><input value="Create" type="submit" /></div> + # # </form>" + # + # # <%= button_to "Delete Image", { :action => "delete", :id => @image.id }, # :confirm => "Are you sure?", :method => :delete %> # # => "<form method="post" action="/images/delete/1" class="button_to"> @@ -324,10 +331,11 @@ module ActionView end form_method = method.to_s == 'get' ? 'get' : 'post' - form_class = html_options.delete('form_class') || 'button_to' - + form_options = html_options.delete('form') || {} + form_options[:class] ||= html_options.delete('form_class') || 'button_to' + remote = html_options.delete('remote') - + request_token_tag = '' if form_method == 'post' && protect_against_forgery? request_token_tag = tag(:input, :type => "hidden", :name => request_forgery_protection_token.to_s, :value => form_authenticity_token) @@ -340,15 +348,17 @@ module ActionView html_options.merge!("type" => "submit", "value" => name) - ("<form method=\"#{form_method}\" action=\"#{ERB::Util.html_escape(url)}\" #{"data-remote=\"true\"" if remote} class=\"#{ERB::Util.html_escape(form_class)}\"><div>" + - method_tag + tag("input", html_options) + request_token_tag + "</div></form>").html_safe + form_options.merge!(:method => form_method, :action => url) + form_options.merge!("data-remote" => "true") if remote + + "#{tag(:form, form_options, true)}<div>#{method_tag}#{tag("input", html_options)}#{request_token_tag}</div></form>".html_safe end # Creates a link tag of the given +name+ using a URL created by the set of # +options+ unless the current request URI is the same as the links, in # which case only the name is returned (or the given block is yielded, if - # one exists). You can give +link_to_unless_current+ a block which will + # one exists). You can give +link_to_unless_current+ a block which will # specialize the default behavior (e.g., show a "Start Here" link rather # than the link's text). # @@ -375,7 +385,7 @@ module ActionView # </ul> # # The implicit block given to +link_to_unless_current+ is evaluated if the current - # action is the action given. So, if we had a comments page and wanted to render a + # action is the action given. So, if we had a comments page and wanted to render a # "Go Back" link instead of a link to the comments page, we could do something like this... # # <%= @@ -420,7 +430,7 @@ module ActionView end # Creates a link tag of the given +name+ using a URL created by the set of - # +options+ if +condition+ is true, in which case only the name is + # +options+ if +condition+ is true, otherwise only the name is # returned. To specialize the default behavior, you can pass a block that # accepts the name or the full argument list for +link_to_unless+ (see the examples # in +link_to_unless+). @@ -497,14 +507,14 @@ module ActionView }.compact extras = extras.empty? ? '' : '?' + ERB::Util.html_escape(extras.join('&')) - email_address_obfuscated = email_address.dup + email_address_obfuscated = email_address.to_str email_address_obfuscated.gsub!(/@/, html_options.delete("replace_at")) if html_options.key?("replace_at") email_address_obfuscated.gsub!(/\./, html_options.delete("replace_dot")) if html_options.key?("replace_dot") case encode when "javascript" string = '' html = content_tag("a", name || email_address_obfuscated.html_safe, html_options.merge("href" => "mailto:#{email_address}#{extras}".html_safe)) - html = escape_javascript(html) + html = escape_javascript(html.to_str) "document.write('#{html}');".each_byte do |c| string << sprintf("%%%x", c) end @@ -569,6 +579,12 @@ module ActionView # # current_page?(:controller => 'library', :action => 'checkout') # # => false + # + # Let's say we're in the <tt>/products</tt> action with method POST in case of invalid product. + # + # current_page?(:controller => 'product', :action => 'index') + # # => false + # def current_page?(options) unless request raise "You cannot use helpers that need to determine the current " \ @@ -576,10 +592,12 @@ module ActionView "in a #request method" end + return false unless request.get? + url_string = url_for(options) # We ignore any extra parameters in the request_uri if the - # submitted url doesn't have any either. This lets the function + # submitted url doesn't have any either. This lets the function # work with things like ?order=asc if url_string.index("?") request_uri = request.fullpath @@ -596,9 +614,7 @@ module ActionView private def convert_options_to_data_attributes(options, html_options) - if html_options.nil? - link_to_remote_options?(options) ? {'data-remote' => 'true'} : {} - else + if html_options html_options = html_options.stringify_keys html_options['data-remote'] = 'true' if link_to_remote_options?(options) || link_to_remote_options?(html_options) @@ -611,6 +627,8 @@ module ActionView add_method_to_attributes!(html_options, method) if method html_options + else + link_to_remote_options?(options) ? {'data-remote' => 'true'} : {} end end @@ -619,7 +637,9 @@ module ActionView end def add_method_to_attributes!(html_options, method) - html_options["rel"] = "nofollow" if method.to_s.downcase != "get" + if method && method.to_s.downcase != "get" && html_options["rel"] !~ /nofollow/ + html_options["rel"] = "#{html_options["rel"]} nofollow".strip + end html_options["data-method"] = method end @@ -641,7 +661,7 @@ module ActionView # Processes the +html_options+ hash, converting the boolean # attributes from true/false form into the form required by - # HTML/XHTML. (An attribute is considered to be boolean if + # HTML/XHTML. (An attribute is considered to be boolean if # its name is listed in the given +bool_attrs+ array.) # # More specifically, for each boolean attribute in +html_options+ @@ -651,7 +671,7 @@ module ActionView # # if the associated +bool_value+ evaluates to true, it is # replaced with the attribute's name; otherwise the attribute is - # removed from the +html_options+ hash. (See the XHTML 1.0 spec, + # removed from the +html_options+ hash. (See the XHTML 1.0 spec, # section 4.5 "Attribute Minimization" for more: # http://www.w3.org/TR/xhtml1/#h-4.5) # diff --git a/actionpack/lib/action_view/log_subscriber.rb b/actionpack/lib/action_view/log_subscriber.rb index 29ffbd6fdd..bf90d012bf 100644 --- a/actionpack/lib/action_view/log_subscriber.rb +++ b/actionpack/lib/action_view/log_subscriber.rb @@ -4,7 +4,7 @@ module ActionView # Provides functionality so that Rails can output logs from Action View. class LogSubscriber < ActiveSupport::LogSubscriber def render_template(event) - message = "Rendered #{from_rails_root(event.payload[:identifier])}" + message = " Rendered #{from_rails_root(event.payload[:identifier])}" message << " within #{from_rails_root(event.payload[:layout])}" if event.payload[:layout] message << (" (%.1fms)" % event.duration) info(message) diff --git a/actionpack/lib/action_view/lookup_context.rb b/actionpack/lib/action_view/lookup_context.rb index f0ed3425de..fa4bf70f77 100644 --- a/actionpack/lib/action_view/lookup_context.rb +++ b/actionpack/lib/action_view/lookup_context.rb @@ -1,5 +1,6 @@ require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/object/blank' +require 'active_support/core_ext/module/remove_method' module ActionView # = Action View Lookup Context @@ -10,30 +11,32 @@ module ActionView # this key is generated just once during the request, it speeds up all cache accesses. class LookupContext #:nodoc: attr_accessor :prefixes - + mattr_accessor :fallbacks @@fallbacks = FallbackFileSystemResolver.instances mattr_accessor :registered_details self.registered_details = [] - mattr_accessor :registered_detail_setters - self.registered_detail_setters = [] - def self.register_detail(name, options = {}, &block) self.registered_details << name - self.registered_detail_setters << [name, "#{name}="] + initialize = registered_details.map { |n| "self.#{n} = details[:#{n}]" } - Accessors.send :define_method, :"_#{name}_defaults", &block + Accessors.send :define_method, :"default_#{name}", &block Accessors.module_eval <<-METHOD, __FILE__, __LINE__ + 1 def #{name} @details[:#{name}] end def #{name}=(value) - value = Array.wrap(value.presence || _#{name}_defaults) + value = Array.wrap(value.presence || default_#{name}) _set_detail(:#{name}, value) if value != @details[:#{name}] end + + remove_possible_method :initialize_details + def initialize_details(details) + #{initialize.join("\n")} + end METHOD end @@ -41,8 +44,9 @@ module ActionView module Accessors #:nodoc: end - register_detail(:formats) { Mime::SET.symbols } register_detail(:locale) { [I18n.locale, I18n.default_locale] } + register_detail(:formats) { Mime::SET.symbols } + register_detail(:handlers){ Template::Handlers.extensions } class DetailsKey #:nodoc: alias :eql? :equal? @@ -60,38 +64,54 @@ module ActionView end end - def initialize(view_paths, details = {}, prefixes = []) - @details, @details_key = { :handlers => default_handlers }, nil - @frozen_formats, @skip_default_locale = false, false - @cache = true - @prefixes = prefixes + # Add caching behavior on top of Details. + module DetailsCache + attr_accessor :cache - self.view_paths = view_paths - self.registered_detail_setters.each do |key, setter| - send(setter, details[key]) + # Calculate the details key. Remove the handlers from calculation to improve performance + # since the user cannot modify it explicitly. + def details_key #:nodoc: + @details_key ||= DetailsKey.get(@details) if @cache + end + + # Temporary skip passing the details_key forward. + def disable_cache + old_value, @cache = @cache, false + yield + ensure + @cache = old_value + end + + protected + + def _set_detail(key, value) + @details_key = nil + @details = @details.dup if @details.frozen? + @details[key] = value.freeze end end + # Helpers related to template lookup using the lookup context information. module ViewPaths attr_reader :view_paths # Whenever setting view paths, makes a copy so we can manipulate then in # instance objects as we wish. def view_paths=(paths) - @view_paths = ActionView::Base.process_view_paths(paths) + @view_paths = ActionView::PathSet.new(Array.wrap(paths)) end - def find(name, prefixes = [], partial = false, keys = []) - @view_paths.find(*args_for_lookup(name, prefixes, partial, keys)) + def find(name, prefixes = [], partial = false, keys = [], options = {}) + @view_paths.find(*args_for_lookup(name, prefixes, partial, keys, options)) end alias :find_template :find - def find_all(name, prefixes = [], partial = false, keys = []) - @view_paths.find_all(*args_for_lookup(name, prefixes, partial, keys)) + def find_all(name, prefixes = [], partial = false, keys = [], options = {}) + @view_paths.find_all(*args_for_lookup(name, prefixes, partial, keys, options)) end - def exists?(name, prefixes = [], partial = false, keys = []) - @view_paths.exists?(*args_for_lookup(name, prefixes, partial, keys)) + def exists?(name, prefixes = [], partial = false, keys = [], options = {}) + @view_paths.exists?(*args_for_lookup(name, prefixes, partial, keys, options)) end alias :template_exists? :exists? @@ -110,16 +130,29 @@ module ActionView protected - def args_for_lookup(name, prefixes, partial, keys) #:nodoc: + def args_for_lookup(name, prefixes, partial, keys, details_options) #:nodoc: name, prefixes = normalize_name(name, prefixes) - [name, prefixes, partial || false, @details, details_key, keys] + details, details_key = detail_args_for(details_options) + [name, prefixes, partial || false, details, details_key, keys] + end + + # Compute details hash and key according to user options (e.g. passed from #render). + def detail_args_for(options) + return @details, details_key if options.empty? # most common path. + user_details = @details.merge(options) + [user_details, DetailsKey.get(user_details)] end # Support legacy foo.erb names even though we now ignore .erb # as well as incorrectly putting part of the path in the template # name instead of the prefix. def normalize_name(name, prefixes) #:nodoc: - name = name.to_s.gsub(handlers_regexp, '') + name = name.to_s.sub(handlers_regexp) do |match| + ActiveSupport::Deprecation.warn "Passing a template handler in the template name is deprecated. " \ + "You can simply remove the handler name or pass render :handlers => [:#{match[1..-1]}] instead.", caller + "" + end + parts = name.split('/') name = parts.pop @@ -132,120 +165,82 @@ module ActionView return name, prefixes end - def default_handlers #:nodoc: - @@default_handlers ||= Template::Handlers.extensions - end - def handlers_regexp #:nodoc: @@handlers_regexp ||= /\.(?:#{default_handlers.join('|')})$/ end end - module Details - attr_accessor :cache - - # Calculate the details key. Remove the handlers from calculation to improve performance - # since the user cannot modify it explicitly. - def details_key #:nodoc: - @details_key ||= DetailsKey.get(@details) if @cache - end - - # Temporary skip passing the details_key forward. - def disable_cache - old_value, @cache = @cache, false - yield - ensure - @cache = old_value - end + include Accessors + include DetailsCache + include ViewPaths - # Freeze the current formats in the lookup context. By freezing them, you are guaranteeing - # that next template lookups are not going to modify the formats. The controller can also - # use this, to ensure that formats won't be further modified (as it does in respond_to blocks). - def freeze_formats(formats, unless_frozen=false) #:nodoc: - return if unless_frozen && @frozen_formats - self.formats = formats - @frozen_formats = true - end + def initialize(view_paths, details = {}, prefixes = []) + @details, @details_key = {}, nil + @frozen_formats, @skip_default_locale = false, false + @cache = true + @prefixes = prefixes - # Overload formats= to expand ["*/*"] values and automatically - # add :html as fallback to :js. - def formats=(values) - if values - values.concat(_formats_defaults) if values.delete "*/*" - values << :html if values == [:js] - end - super(values) - end + self.view_paths = view_paths + initialize_details(details) + end - # Do not use the default locale on template lookup. - def skip_default_locale! - @skip_default_locale = true - self.locale = nil - end + # Freeze the current formats in the lookup context. By freezing them, you + # that next template lookups are not going to modify the formats. The con + # use this, to ensure that formats won't be further modified (as it does + def freeze_formats(formats, unless_frozen=false) #:nodoc: + return if unless_frozen && @frozen_formats + self.formats = formats + @frozen_formats = true + end - # Overload locale to return a symbol instead of array. - def locale - @details[:locale].first + # Override formats= to expand ["*/*"] values and automatically + # add :html as fallback to :js. + def formats=(values) + if values + values.concat(default_formats) if values.delete "*/*" + values << :html if values == [:js] end + super(values) + end - # Overload locale= to also set the I18n.locale. If the current I18n.config object responds - # to original_config, it means that it's has a copy of the original I18n configuration and it's - # acting as proxy, which we need to skip. - def locale=(value) - if value - config = I18n.config.respond_to?(:original_config) ? I18n.config.original_config : I18n.config - config.locale = value - end + # Do not use the default locale on template lookup. + def skip_default_locale! + @skip_default_locale = true + self.locale = nil + end - super(@skip_default_locale ? I18n.locale : _locale_defaults) - end + # Override locale to return a symbol instead of array. + def locale + @details[:locale].first + end - # A method which only uses the first format in the formats array for layout lookup. - # This method plays straight with instance variables for performance reasons. - def with_layout_format - if formats.size == 1 - yield - else - old_formats = formats - _set_detail(:formats, formats[0,1]) - - begin - yield - ensure - _set_detail(:formats, old_formats) - end - end + # Overload locale= to also set the I18n.locale. If the current I18n.config object responds + # to original_config, it means that it's has a copy of the original I18n configuration and it's + # acting as proxy, which we need to skip. + def locale=(value) + if value + config = I18n.config.respond_to?(:original_config) ? I18n.config.original_config : I18n.config + config.locale = value end - # Update the details keys by merging the given hash into the current - # details hash. If a block is given, the details are modified just during - # the execution of the block and reverted to the previous value after. - def update_details(new_details) - old_details = @details.dup + super(@skip_default_locale ? I18n.locale : default_locale) + end - registered_detail_setters.each do |key, setter| - send(setter, new_details[key]) if new_details.key?(key) - end + # A method which only uses the first format in the formats array for layout lookup. + # This method plays straight with instance variables for performance reasons. + def with_layout_format + if formats.size == 1 + yield + else + old_formats = formats + _set_detail(:formats, formats[0,1]) begin yield ensure - @details_key = nil - @details = old_details + _set_detail(:formats, old_formats) end end - - protected - - def _set_detail(key, value) - @details_key = nil - @details = @details.dup if @details.frozen? - @details[key] = value.freeze - end end - - include Accessors - include Details - include ViewPaths end end diff --git a/actionpack/lib/action_view/path_set.rb b/actionpack/lib/action_view/path_set.rb index e0cb5d6a70..bbb1af8154 100644 --- a/actionpack/lib/action_view/path_set.rb +++ b/actionpack/lib/action_view/path_set.rb @@ -1,11 +1,55 @@ module ActionView #:nodoc: # = Action View PathSet - class PathSet < Array #:nodoc: - %w(initialize << concat insert push unshift).each do |method| + class PathSet #:nodoc: + include Enumerable + + attr_reader :paths + + def initialize(paths = []) + @paths = typecast paths + end + + def initialize_copy(other) + @paths = other.paths.dup + self + end + + def [](i) + paths[i] + end + + def to_ary + paths.dup + end + + def include?(item) + paths.include? item + end + + def pop + paths.pop + end + + def size + paths.size + end + + def each(&block) + paths.each(&block) + end + + def compact + PathSet.new paths.compact + end + + def +(array) + PathSet.new(paths + array) + end + + %w(<< concat push insert unshift).each do |method| class_eval <<-METHOD, __FILE__, __LINE__ + 1 def #{method}(*args) - super - typecast! + paths.#{method}(*typecast(args)) end METHOD end @@ -17,7 +61,7 @@ module ActionView #:nodoc: def find_all(path, prefixes = [], *args) prefixes = [prefixes] if String === prefixes prefixes.each do |prefix| - each do |resolver| + paths.each do |resolver| templates = resolver.find_all(path, prefix, *args) return templates unless templates.empty? end @@ -25,17 +69,20 @@ module ActionView #:nodoc: [] end - def exists?(*args) - find_all(*args).any? + def exists?(path, prefixes, *args) + find_all(path, prefixes, *args).any? end - protected + private - def typecast! - each_with_index do |path, i| - path = path.to_s if path.is_a?(Pathname) - next unless path.is_a?(String) - self[i] = OptimizedFileSystemResolver.new(path) + def typecast(paths) + paths.map do |path| + case path + when Pathname, String + OptimizedFileSystemResolver.new path.to_s + else + path + end end end end diff --git a/actionpack/lib/action_view/renderer/abstract_renderer.rb b/actionpack/lib/action_view/renderer/abstract_renderer.rb index 60c527beeb..c0936441ac 100644 --- a/actionpack/lib/action_view/renderer/abstract_renderer.rb +++ b/actionpack/lib/action_view/renderer/abstract_renderer.rb @@ -11,15 +11,22 @@ module ActionView raise NotImplementedError end - # Checks if the given path contains a format and if so, change - # the lookup context to take this new format into account. - def wrap_formats(value) - return yield unless value.is_a?(String) - - if value.sub!(formats_regexp, "") - update_details(:formats => [$1.to_sym]){ yield } - else - yield + protected + + def extract_details(options) + details = {} + @lookup_context.registered_details.each do |key| + next unless value = options[key] + details[key] = Array.wrap(value) + end + details + end + + def extract_format(value, details) + if value.is_a?(String) && value.sub!(formats_regexp, "") + ActiveSupport::Deprecation.warn "Passing the format in the template name is deprecated. " \ + "Please pass render with :formats => [:#{$1}] instead.", caller + details[:formats] ||= [$1.to_sym] end end @@ -27,8 +34,6 @@ module ActionView @@formats_regexp ||= /\.(#{Mime::SET.symbols.join('|')})$/ end - protected - def instrument(name, options={}) ActiveSupport::Notifications.instrument("render_#{name}.action_view", options){ yield } end diff --git a/actionpack/lib/action_view/renderer/partial_renderer.rb b/actionpack/lib/action_view/renderer/partial_renderer.rb index a351fbc04f..15cb9d0f76 100644 --- a/actionpack/lib/action_view/renderer/partial_renderer.rb +++ b/actionpack/lib/action_view/renderer/partial_renderer.rb @@ -12,8 +12,7 @@ module ActionView # # <%= render :partial => "account" %> # - # This would render "advertiser/_account.html.erb" and pass the instance variable @account in as a local variable - # +account+ to the template for display. + # This would render "advertiser/_account.html.erb". # # In another template for Advertiser#buy, we could have: # @@ -28,32 +27,24 @@ module ActionView # # == The :as and :object options # - # By default <tt>ActionView::Partials::PartialRenderer</tt> has its object in a local variable with the same - # name as the template. So, given + # By default <tt>ActionView::PartialRenderer</tt> doesn't have any local variables. + # The <tt>:object</tt> option can be used to pass an object to the partial. For instance: # - # <%= render :partial => "contract" %> + # <%= render :partial => "account", :object => @buyer %> # - # within contract we'll get <tt>@contract</tt> in the local variable +contract+, as if we had written + # would provide the +@buyer+ object to the partial, available under the local variable +account+ and is + # equivalent to: # - # <%= render :partial => "contract", :locals => { :contract => @contract } %> + # <%= render :partial => "account", :locals => { :account => @buyer } %> # # With the <tt>:as</tt> option we can specify a different name for said local variable. For example, if we - # wanted it to be +agreement+ instead of +contract+ we'd do: - # - # <%= render :partial => "contract", :as => 'agreement' %> + # wanted it to be +user+ instead of +account+ we'd do: # - # The <tt>:object</tt> option can be used to directly specify which object is rendered into the partial; - # useful when the template's object is elsewhere, in a different ivar or in a local variable for instance. + # <%= render :partial => "account", :object => @buyer, :as => 'user' %> # - # Revisiting a previous example we could have written this code: + # This is equivalent to # - # <%= render :partial => "account", :object => @buyer %> - # - # <% @advertisements.each do |ad| %> - # <%= render :partial => "ad", :object => ad %> - # <% end %> - # - # The <tt>:object</tt> and <tt>:as</tt> options can be used together. + # <%= render :partial => "account", :locals => { :user => @buyer } %> # # == Rendering a collection of partials # @@ -215,27 +206,25 @@ module ActionView # <%- end -%> # <% end %> class PartialRenderer < AbstractRenderer #:nodoc: - PARTIAL_NAMES = Hash.new {|h,k| h[k] = {} } + PARTIAL_NAMES = Hash.new { |h,k| h[k] = {} } def initialize(*) super - @partial_names = PARTIAL_NAMES[@lookup_context.prefixes.first] + @context_prefix = @lookup_context.prefixes.first + @partial_names = PARTIAL_NAMES[@context_prefix] end def render(context, options, block) setup(context, options, block) + identifier = (@template = find_partial) ? @template.identifier : @path - wrap_formats(@path) do - identifier = ((@template = find_partial) ? @template.identifier : @path) - - if @collection - instrument(:collection, :identifier => identifier || "collection", :count => @collection.size) do - render_collection - end - else - instrument(:partial, :identifier => identifier) do - render_partial - end + if @collection + instrument(:collection, :identifier => identifier || "collection", :count => @collection.size) do + render_collection + end + else + instrument(:partial, :identifier => identifier) do + render_partial end end end @@ -279,6 +268,7 @@ module ActionView @options = options @locals = options[:locals] || {} @block = block + @details = extract_details(options) if String === partial @object = options[:object] @@ -301,6 +291,13 @@ module ActionView paths.map! { |path| retrieve_variable(path).unshift(path) } end + if String === partial && @variable.to_s !~ /^[a-z_][a-zA-Z_0-9]*$/ + raise ArgumentError.new("The partial name (#{partial}) is not a valid Ruby identifier; " + + "make sure your partial name starts with a letter or underscore, " + + "and is followed by any combinations of letters, numbers, or underscores.") + end + + extract_format(@path, @details) self end @@ -328,7 +325,7 @@ module ActionView def find_template(path=@path, locals=@locals.keys) prefixes = path.include?(?/) ? [] : @lookup_context.prefixes - @lookup_context.find_template(path, prefixes, true, locals) + @lookup_context.find_template(path, prefixes, true, locals, @details) end def collection_with_template @@ -364,13 +361,32 @@ module ActionView end def partial_path(object = @object) - @partial_names[object.class.name] ||= begin - object = object.to_model if object.respond_to?(:to_model) + object = object.to_model if object.respond_to?(:to_model) + + path = if object.respond_to?(:to_partial_path) + object.to_partial_path + else + ActiveSupport::Deprecation.warn "ActiveModel-compatible objects whose classes return a #model_name that responds to #partial_path are deprecated. Please respond to #to_partial_path directly instead." + object.class.model_name.partial_path + end - object.class.model_name.partial_path.dup.tap do |partial| - path = @lookup_context.prefixes.first - partial.insert(0, "#{File.dirname(path)}/") if partial.include?(?/) && path.include?(?/) + @partial_names[path] ||= path.dup.tap do |object_path| + merge_prefix_into_object_path(@context_prefix, object_path) + end + end + + def merge_prefix_into_object_path(prefix, object_path) + if prefix.include?(?/) && object_path.include?(?/) + overlap = [] + prefix_array = File.dirname(prefix).split('/') + object_path_array = object_path.split('/')[0..-3] # skip model dir & partial + + prefix_array.each_with_index do |dir, index| + overlap << dir if dir == object_path_array[index] end + + object_path.gsub!(/^#{overlap.join('/')}\//,'') + object_path.insert(0, "#{File.dirname(prefix)}/") end end diff --git a/actionpack/lib/action_view/renderer/template_renderer.rb b/actionpack/lib/action_view/renderer/template_renderer.rb index a09cef8fef..ac91d333ba 100644 --- a/actionpack/lib/action_view/renderer/template_renderer.rb +++ b/actionpack/lib/action_view/renderer/template_renderer.rb @@ -4,13 +4,12 @@ require 'active_support/core_ext/array/wrap' module ActionView class TemplateRenderer < AbstractRenderer #:nodoc: def render(context, options) - @view = context - - wrap_formats(options[:template] || options[:file]) do - template = determine_template(options) - freeze_formats(template.formats, true) - render_template(template, options[:layout], options[:locals]) - end + @view = context + @details = extract_details(options) + extract_format(options[:file] || options[:template], @details) + template = determine_template(options) + freeze_formats(template.formats, true) + render_template(template, options[:layout], options[:locals]) end # Determine the template to be rendered using the given options. @@ -20,13 +19,13 @@ module ActionView if options.key?(:text) Template::Text.new(options[:text], formats.try(:first)) elsif options.key?(:file) - with_fallbacks { find_template(options[:file], nil, false, keys) } + with_fallbacks { find_template(options[:file], nil, false, keys, @details) } elsif options.key?(:inline) handler = Template.handler_for_extension(options[:type] || "erb") Template.new(options[:inline], "inline template", handler, :locals => keys) elsif options.key?(:template) options[:template].respond_to?(:render) ? - options[:template] : find_template(options[:template], options[:prefixes], false, keys) + options[:template] : find_template(options[:template], options[:prefixes], false, keys, @details) end end @@ -62,12 +61,11 @@ module ActionView begin with_layout_format do layout =~ /^\// ? - with_fallbacks { find_template(layout, nil, false, keys) } : find_template(layout, nil, false, keys) - end - rescue ActionView::MissingTemplate => e - update_details(:formats => nil) do - raise unless template_exists?(layout) + with_fallbacks { find_template(layout, nil, false, keys, @details) } : find_template(layout, nil, false, keys, @details) end + rescue ActionView::MissingTemplate + all_details = @details.merge(:formats => @lookup_context.default_formats) + raise unless template_exists?(layout, nil, false, keys, all_details) end end end diff --git a/actionpack/lib/action_view/template.rb b/actionpack/lib/action_view/template.rb index 98ecd15aa0..10797c010f 100644 --- a/actionpack/lib/action_view/template.rb +++ b/actionpack/lib/action_view/template.rb @@ -79,9 +79,9 @@ module ActionView # you are handling out-of-band metadata, you are # also responsible for alerting the user to any # problems with converting the user's data to - # the default_internal. + # the <tt>default_internal</tt>. # - # To do so, simply raise the raise WrongEncodingError + # To do so, simply raise the raise +WrongEncodingError+ # as follows: # # raise WrongEncodingError.new( @@ -91,7 +91,6 @@ module ActionView eager_autoload do autoload :Error - autoload :Handler autoload :Handlers autoload :Text end @@ -198,7 +197,7 @@ module ActionView # Among other things, this method is responsible for properly setting # the encoding of the source. Until this point, we assume that the # source is BINARY data. If no additional information is supplied, - # we assume the encoding is the same as Encoding.default_external. + # we assume the encoding is the same as <tt>Encoding.default_external</tt>. # # The user can also specify the encoding via a comment on the first # line of the template (# encoding: NAME-OF-ENCODING). This will work @@ -212,8 +211,8 @@ module ActionView # specifying the encoding. For instance, ERB supports <%# encoding: %> # # Otherwise, after we figure out the correct encoding, we then - # encode the source into Encoding.default_internal. In general, - # this means that templates will be UTF-8 inside of Rails, + # encode the source into <tt>Encoding.default_internal</tt>. + # In general, this means that templates will be UTF-8 inside of Rails, # regardless of the original source encoding. def compile(view, mod) #:nodoc: method_name = self.method_name diff --git a/actionpack/lib/action_view/template/error.rb b/actionpack/lib/action_view/template/error.rb index d4448a7b33..587e37a84f 100644 --- a/actionpack/lib/action_view/template/error.rb +++ b/actionpack/lib/action_view/template/error.rb @@ -31,7 +31,6 @@ module ActionView def initialize(paths, path, prefixes, partial, details, *) @path = path prefixes = Array.wrap(prefixes) - display_paths = paths.compact.map{ |p| p.to_s.inspect }.join(", ") template_type = if partial "partial" elsif path =~ /layouts/i diff --git a/actionpack/lib/action_view/template/handler.rb b/actionpack/lib/action_view/template/handler.rb deleted file mode 100644 index 636f3ebbad..0000000000 --- a/actionpack/lib/action_view/template/handler.rb +++ /dev/null @@ -1,49 +0,0 @@ -require 'action_dispatch/http/mime_type' -require 'active_support/core_ext/class/attribute' - -# Legacy TemplateHandler stub -module ActionView - class Template - module Handlers #:nodoc: - module Compilable - def self.included(base) - ActiveSupport::Deprecation.warn "Including Compilable in your template handler is deprecated. " << - "Since Rails 3, all the API your template handler needs to implement is to respond to #call." - base.extend(ClassMethods) - end - - module ClassMethods - def call(template) - new.compile(template) - end - end - - def compile(template) - raise "Need to implement #{self.class.name}#compile(template)" - end - end - end - - class Template::Handler - class_attribute :default_format - self.default_format = Mime::HTML - - def self.inherited(base) - ActiveSupport::Deprecation.warn "Inheriting from ActionView::Template::Handler is deprecated. " << - "Since Rails 3, all the API your template handler needs to implement is to respond to #call." - super - end - - def self.call(template) - raise "Need to implement #{self.class.name}#call(template)" - end - - def render(template, local_assigns) - raise "Need to implement #{self.class.name}#render(template, local_assigns)" - end - end - end - - TemplateHandlers = Template::Handlers - TemplateHandler = Template::Handler -end diff --git a/actionpack/lib/action_view/template/handlers.rb b/actionpack/lib/action_view/template/handlers.rb index 959afa734e..aa693335e3 100644 --- a/actionpack/lib/action_view/template/handlers.rb +++ b/actionpack/lib/action_view/template/handlers.rb @@ -41,12 +41,6 @@ module ActionView #:nodoc: @@default_template_handlers = klass end - def handler_class_for_extension(extension) - ActiveSupport::Deprecation.warn "handler_class_for_extension is deprecated. " << - "Please use handler_for_extension instead", caller - handler_for_extension(extension) - end - def handler_for_extension(extension) registered_template_handler(extension) || @@default_template_handlers end diff --git a/actionpack/lib/action_view/template/handlers/erb.rb b/actionpack/lib/action_view/template/handlers/erb.rb index 7e9e4e518a..77720e2bc8 100644 --- a/actionpack/lib/action_view/template/handlers/erb.rb +++ b/actionpack/lib/action_view/template/handlers/erb.rb @@ -1,6 +1,5 @@ +require 'action_dispatch/http/mime_type' require 'active_support/core_ext/class/attribute_accessors' -require 'action_view/template' -require 'action_view/template/handler' require 'erubis' module ActionView diff --git a/actionpack/lib/action_view/template/resolver.rb b/actionpack/lib/action_view/template/resolver.rb index 2b9427ace5..f855ea257c 100644 --- a/actionpack/lib/action_view/template/resolver.rb +++ b/actionpack/lib/action_view/template/resolver.rb @@ -1,5 +1,6 @@ require "pathname" require "active_support/core_ext/class" +require "active_support/core_ext/io" require "action_view/template" module ActionView @@ -68,7 +69,7 @@ module ActionView # before returning it. def cached(key, path_info, details, locals) #:nodoc: name, prefix, partial = path_info - locals = sort_locals(locals) + locals = locals.map { |x| x.to_s }.sort! if key && caching? @cached[key][name][prefix][partial][locals] ||= decorate(yield, path_info, details, locals) @@ -97,18 +98,6 @@ module ActionView t.virtual_path ||= (cached ||= build_path(*path_info)) end end - - if :symbol.respond_to?("<=>") - def sort_locals(locals) #:nodoc: - locals.sort.freeze - end - else - def sort_locals(locals) #:nodoc: - locals = locals.map{ |l| l.to_s } - locals.sort! - locals.freeze - end - end end # An abstract class that implements a Resolver with path semantics. @@ -130,27 +119,35 @@ module ActionView def query(path, details, formats) query = build_query(path, details) - templates = [] - sanitizer = Hash.new { |h,k| h[k] = Dir["#{File.dirname(k)}/*"] } - Dir[query].each do |p| - next if File.directory?(p) || !sanitizer[p].include?(p) + # deals with case-insensitive file systems. + sanitizer = Hash.new { |h,dir| h[dir] = Dir["#{dir}/*"] } - handler, format = extract_handler_and_format(p, formats) - contents = File.open(p, "rb") { |io| io.read } + template_paths = Dir[query].reject { |filename| + File.directory?(filename) || + !sanitizer[File.dirname(filename)].include?(filename) + } - templates << Template.new(contents, File.expand_path(p), handler, - :virtual_path => path.virtual, :format => format, :updated_at => mtime(p)) - end + template_paths.map { |template| + handler, format = extract_handler_and_format(template, formats) + contents = File.binread template - templates + Template.new(contents, File.expand_path(template), handler, + :virtual_path => path.virtual, + :format => format, + :updated_at => mtime(template)) + } end # Helper for building query glob string based on resolver's pattern. def build_query(path, details) query = @pattern.dup - query.gsub!(/\:prefix(\/)?/, path.prefix.empty? ? "" : "#{path.prefix}\\1") # prefix can be empty... - query.gsub!(/\:action/, path.partial? ? "_#{path.name}" : path.name) + + prefix = path.prefix.empty? ? "" : "#{escape_entry(path.prefix)}\\1" + query.gsub!(/\:prefix(\/)?/, prefix) + + partial = escape_entry(path.partial? ? "_#{path.name}" : path.name) + query.gsub!(/\:action/, partial) details.each do |ext, variants| query.gsub!(/\:#{ext}/, "{#{variants.compact.uniq.join(',')}}") @@ -159,9 +156,13 @@ module ActionView File.expand_path(query, @path) end + def escape_entry(entry) + entry.gsub(/[*?{}\[\]]/, '\\\\\\&') + end + # Returns the file mtime from the filesystem. def mtime(p) - File.stat(p).mtime + File.mtime(p) end # Extract handler and formats from path. If a format cannot be a found neither @@ -235,15 +236,11 @@ module ActionView class OptimizedFileSystemResolver < FileSystemResolver #:nodoc: def build_query(path, details) exts = EXTENSIONS.map { |ext| details[ext] } - query = File.join(@path, path) - - exts.each do |ext| - query << "{" - ext.compact.each { |e| query << ".#{e}," } - query << "}" - end + query = escape_entry(File.join(@path, path)) - query + query + exts.map { |ext| + "{#{ext.compact.uniq.map { |e| ".#{e}," }.join}}" + }.join end end diff --git a/actionpack/lib/action_view/test_case.rb b/actionpack/lib/action_view/test_case.rb index d0317a148b..1db079d0e3 100644 --- a/actionpack/lib/action_view/test_case.rb +++ b/actionpack/lib/action_view/test_case.rb @@ -54,10 +54,8 @@ module ActionView end def determine_default_helper_class(name) - mod = name.sub(/Test$/, '').constantize + mod = name.sub(/Test$/, '').safe_constantize mod.is_a?(Class) ? nil : mod - rescue NameError - nil end def helper_method(*methods) @@ -65,7 +63,7 @@ module ActionView methods.flatten.each do |method| _helpers.module_eval <<-end_eval def #{method}(*args, &block) # def current_user(*args, &block) - _test_case.send(%(#{method}), *args, &block) # test_case.send(%(current_user), *args, &block) + _test_case.send(%(#{method}), *args, &block) # _test_case.send(%(current_user), *args, &block) end # end end_eval end @@ -218,12 +216,6 @@ module ActionView end] end - def _assigns - ActiveSupport::Deprecation.warn "ActionView::TestCase#_assigns is deprecated and will be removed in future versions. " << - "Please use view_assigns instead." - view_assigns - end - def _routes @controller._routes if @controller.respond_to?(:_routes) end diff --git a/actionpack/lib/action_view/testing/resolvers.rb b/actionpack/lib/action_view/testing/resolvers.rb index 773dfcbb1d..7afa2fa613 100644 --- a/actionpack/lib/action_view/testing/resolvers.rb +++ b/actionpack/lib/action_view/testing/resolvers.rb @@ -34,7 +34,7 @@ module ActionView #:nodoc: templates << Template.new(source, _path, handler, :virtual_path => path.virtual, :format => format, :updated_at => updated_at) end - + templates.sort_by {|t| -t.identifier.match(/^#{query}$/).captures.reject(&:blank?).size } end end diff --git a/actionpack/lib/sprockets/assets.rake b/actionpack/lib/sprockets/assets.rake new file mode 100644 index 0000000000..a5145080c2 --- /dev/null +++ b/actionpack/lib/sprockets/assets.rake @@ -0,0 +1,95 @@ +require "fileutils" + +namespace :assets do + def ruby_rake_task(task) + env = ENV['RAILS_ENV'] || 'production' + groups = ENV['RAILS_GROUPS'] || 'assets' + args = [$0, task,"RAILS_ENV=#{env}","RAILS_GROUPS=#{groups}"] + args << "--trace" if Rake.application.options.trace + ruby *args + end + + # We are currently running with no explicit bundler group + # and/or no explicit environment - we have to reinvoke rake to + # execute this task. + def invoke_or_reboot_rake_task(task) + if ENV['RAILS_GROUPS'].to_s.empty? || ENV['RAILS_ENV'].to_s.empty? + ruby_rake_task task + else + Rake::Task[task].invoke + end + end + + desc "Compile all the assets named in config.assets.precompile" + task :precompile do + invoke_or_reboot_rake_task "assets:precompile:all" + end + + namespace :precompile do + def internal_precompile(digest=nil) + unless Rails.application.config.assets.enabled + warn "Cannot precompile assets if sprockets is disabled. Please set config.assets.enabled to true" + exit + end + + # Ensure that action view is loaded and the appropriate + # sprockets hooks get executed + _ = ActionView::Base + + config = Rails.application.config + config.assets.compile = true + config.assets.digest = digest unless digest.nil? + config.assets.digests = {} + + env = Rails.application.assets + target = File.join(Rails.public_path, config.assets.prefix) + compiler = Sprockets::StaticCompiler.new(env, + target, + config.assets.precompile, + :manifest_path => config.assets.manifest, + :digest => config.assets.digest, + :manifest => digest.nil?) + compiler.compile + end + + task :all do + Rake::Task["assets:precompile:primary"].invoke + # We need to reinvoke in order to run the secondary digestless + # asset compilation run - a fresh Sprockets environment is + # required in order to compile digestless assets as the + # environment has already cached the assets on the primary + # run. + ruby_rake_task "assets:precompile:nondigest" if Rails.application.config.assets.digest + end + + task :primary => ["assets:environment", "tmp:cache:clear"] do + internal_precompile + end + + task :nondigest => ["assets:environment", "tmp:cache:clear"] do + internal_precompile(false) + end + end + + desc "Remove compiled assets" + task :clean do + invoke_or_reboot_rake_task "assets:clean:all" + end + + namespace :clean do + task :all => ["assets:environment", "tmp:cache:clear"] do + config = Rails.application.config + public_asset_path = File.join(Rails.public_path, config.assets.prefix) + rm_rf public_asset_path, :secure => true + end + end + + task :environment do + if Rails.application.config.assets.initialize_on_precompile + Rake::Task["environment"].invoke + else + Rails.application.initialize!(:assets) + Sprockets::Bootstrap.new(Rails.application).run + end + end +end diff --git a/actionpack/lib/sprockets/bootstrap.rb b/actionpack/lib/sprockets/bootstrap.rb new file mode 100644 index 0000000000..395b264fe7 --- /dev/null +++ b/actionpack/lib/sprockets/bootstrap.rb @@ -0,0 +1,37 @@ +module Sprockets + class Bootstrap + def initialize(app) + @app = app + end + + # TODO: Get rid of config.assets.enabled + def run + app, config = @app, @app.config + return unless app.assets + + config.assets.paths.each { |path| app.assets.append_path(path) } + + if config.assets.compress + # temporarily hardcode default JS compressor to uglify. Soon, it will work + # the same as SCSS, where a default plugin sets the default. + unless config.assets.js_compressor == false + app.assets.js_compressor = LazyCompressor.new { Sprockets::Compressors.registered_js_compressor(config.assets.js_compressor || :uglifier) } + end + + unless config.assets.css_compressor == false + app.assets.css_compressor = LazyCompressor.new { Sprockets::Compressors.registered_css_compressor(config.assets.css_compressor) } + end + end + + if config.assets.compile + app.routes.prepend do + mount app.assets => config.assets.prefix + end + end + + if config.assets.digest + app.assets = app.assets.index + end + end + end +end diff --git a/actionpack/lib/sprockets/compressors.rb b/actionpack/lib/sprockets/compressors.rb new file mode 100644 index 0000000000..cb3e13314b --- /dev/null +++ b/actionpack/lib/sprockets/compressors.rb @@ -0,0 +1,83 @@ +module Sprockets + module Compressors + @@css_compressors = {} + @@js_compressors = {} + @@default_css_compressor = nil + @@default_js_compressor = nil + + def self.register_css_compressor(name, klass, options = {}) + @@default_css_compressor = name.to_sym if options[:default] || @@default_css_compressor.nil? + @@css_compressors[name.to_sym] = {:klass => klass.to_s, :require => options[:require]} + end + + def self.register_js_compressor(name, klass, options = {}) + @@default_js_compressor = name.to_sym if options[:default] || @@default_js_compressor.nil? + @@js_compressors[name.to_sym] = {:klass => klass.to_s, :require => options[:require]} + end + + def self.registered_css_compressor(name) + if name.respond_to?(:to_sym) + compressor = @@css_compressors[name.to_sym] || @@css_compressors[@@default_css_compressor] + require compressor[:require] if compressor[:require] + compressor[:klass].constantize.new + else + name + end + end + + def self.registered_js_compressor(name) + if name.respond_to?(:to_sym) + compressor = @@js_compressors[name.to_sym] || @@js_compressors[@@default_js_compressor] + require compressor[:require] if compressor[:require] + compressor[:klass].constantize.new + else + name + end + end + + # The default compressors must be registered in default plugins (ex. Sass-Rails) + register_css_compressor(:scss, 'Sass::Rails::Compressor', :require => 'sass/rails/compressor', :default => true) + register_js_compressor(:uglifier, 'Uglifier', :require => 'uglifier', :default => true) + + # Automaticaly register some compressors + register_css_compressor(:yui, 'YUI::CssCompressor', :require => 'yui/compressor') + register_js_compressor(:closure, 'Closure::Compiler', :require => 'closure-compiler') + register_js_compressor(:yui, 'YUI::JavaScriptCompressor', :require => 'yui/compressor') + end + + # An asset compressor which does nothing. + # + # This compressor simply returns the asset as-is, without any compression + # whatsoever. It is useful in development mode, when compression isn't + # needed but using the same asset pipeline as production is desired. + class NullCompressor #:nodoc: + def compress(content) + content + end + end + + # An asset compressor which only initializes the underlying compression + # engine when needed. + # + # This postpones the initialization of the compressor until + # <code>#compress</code> is called the first time. + class LazyCompressor #:nodoc: + # Initializes a new LazyCompressor. + # + # The block should return a compressor when called, i.e. an object + # which responds to <code>#compress</code>. + def initialize(&block) + @block = block + end + + def compress(content) + compressor.compress(content) + end + + private + + def compressor + @compressor ||= (@block.call || NullCompressor.new) + end + end +end diff --git a/actionpack/lib/sprockets/helpers.rb b/actionpack/lib/sprockets/helpers.rb new file mode 100644 index 0000000000..fee48386e0 --- /dev/null +++ b/actionpack/lib/sprockets/helpers.rb @@ -0,0 +1,6 @@ +module Sprockets + module Helpers + autoload :RailsHelper, "sprockets/helpers/rails_helper" + autoload :IsolatedHelper, "sprockets/helpers/isolated_helper" + end +end diff --git a/actionpack/lib/sprockets/helpers/isolated_helper.rb b/actionpack/lib/sprockets/helpers/isolated_helper.rb new file mode 100644 index 0000000000..3adb928c45 --- /dev/null +++ b/actionpack/lib/sprockets/helpers/isolated_helper.rb @@ -0,0 +1,13 @@ +module Sprockets + module Helpers + module IsolatedHelper + def controller + nil + end + + def config + Rails.application.config.action_controller + end + end + end +end diff --git a/actionpack/lib/sprockets/helpers/rails_helper.rb b/actionpack/lib/sprockets/helpers/rails_helper.rb new file mode 100644 index 0000000000..ddf9b08b54 --- /dev/null +++ b/actionpack/lib/sprockets/helpers/rails_helper.rb @@ -0,0 +1,166 @@ +require "action_view" + +module Sprockets + module Helpers + module RailsHelper + extend ActiveSupport::Concern + include ActionView::Helpers::AssetTagHelper + + def asset_paths + @asset_paths ||= begin + paths = RailsHelper::AssetPaths.new(config, controller) + paths.asset_environment = asset_environment + paths.asset_digests = asset_digests + paths.compile_assets = compile_assets? + paths.digest_assets = digest_assets? + paths + end + end + + def javascript_include_tag(*sources) + options = sources.extract_options! + debug = options.key?(:debug) ? options.delete(:debug) : debug_assets? + body = options.key?(:body) ? options.delete(:body) : false + digest = options.key?(:digest) ? options.delete(:digest) : digest_assets? + + sources.collect do |source| + if debug && asset = asset_paths.asset_for(source, 'js') + asset.to_a.map { |dep| + super(dep.to_s, { :src => asset_path(dep, :ext => 'js', :body => true, :digest => digest) }.merge!(options)) + } + else + super(source.to_s, { :src => asset_path(source, :ext => 'js', :body => body, :digest => digest) }.merge!(options)) + end + end.join("\n").html_safe + end + + def stylesheet_link_tag(*sources) + options = sources.extract_options! + debug = options.key?(:debug) ? options.delete(:debug) : debug_assets? + body = options.key?(:body) ? options.delete(:body) : false + digest = options.key?(:digest) ? options.delete(:digest) : digest_assets? + + sources.collect do |source| + if debug && asset = asset_paths.asset_for(source, 'css') + asset.to_a.map { |dep| + super(dep.to_s, { :href => asset_path(dep, :ext => 'css', :body => true, :protocol => :request, :digest => digest) }.merge!(options)) + } + else + super(source.to_s, { :href => asset_path(source, :ext => 'css', :body => body, :protocol => :request, :digest => digest) }.merge!(options)) + end + end.join("\n").html_safe + end + + def asset_path(source, options = {}) + source = source.logical_path if source.respond_to?(:logical_path) + path = asset_paths.compute_public_path(source, asset_prefix, options.merge(:body => true)) + options[:body] ? "#{path}?body=1" : path + end + + def image_path(source) + asset_path(source) + end + alias_method :path_to_image, :image_path # aliased to avoid conflicts with an image_path named route + + def javascript_path(source) + asset_path(source) + end + alias_method :path_to_javascript, :javascript_path # aliased to avoid conflicts with an javascript_path named route + + def stylesheet_path(source) + asset_path(source) + end + alias_method :path_to_stylesheet, :stylesheet_path # aliased to avoid conflicts with an stylesheet_path named route + + private + def debug_assets? + compile_assets? && (Rails.application.config.assets.debug || params[:debug_assets]) + rescue NoMethodError + false + end + + # Override to specify an alternative prefix for asset path generation. + # When combined with a custom +asset_environment+, this can be used to + # implement themes that can take advantage of the asset pipeline. + # + # If you only want to change where the assets are mounted, refer to + # +config.assets.prefix+ instead. + def asset_prefix + Rails.application.config.assets.prefix + end + + def asset_digests + Rails.application.config.assets.digests + end + + def compile_assets? + Rails.application.config.assets.compile + end + + def digest_assets? + Rails.application.config.assets.digest + end + + # Override to specify an alternative asset environment for asset + # path generation. The environment should already have been mounted + # at the prefix returned by +asset_prefix+. + def asset_environment + Rails.application.assets + end + + class AssetPaths < ::ActionView::AssetPaths #:nodoc: + attr_accessor :asset_environment, :asset_prefix, :asset_digests, :compile_assets, :digest_assets + + class AssetNotPrecompiledError < StandardError; end + + # Return the filesystem path for the source + def compute_source_path(source, ext) + asset_for(source, ext) + end + + def asset_for(source, ext) + source = source.to_s + return nil if is_uri?(source) + source = rewrite_extension(source, nil, ext) + asset_environment[source] + rescue Sprockets::FileOutsidePaths + nil + end + + def digest_for(logical_path) + if digest_assets && asset_digests && (digest = asset_digests[logical_path]) + return digest + end + + if compile_assets + if digest_assets && asset = asset_environment[logical_path] + return asset.digest_path + end + return logical_path + else + raise AssetNotPrecompiledError.new("#{logical_path} isn't precompiled") + end + end + + def rewrite_asset_path(source, dir, options = {}) + if source[0] == ?/ + source + else + source = digest_for(source) unless options[:digest] == false + source = File.join(dir, source) + source = "/#{source}" unless source =~ /^\// + source + end + end + + def rewrite_extension(source, dir, ext) + if ext && File.extname(source).empty? + "#{source}.#{ext}" + else + source + end + end + end + end + end +end diff --git a/actionpack/lib/sprockets/railtie.rb b/actionpack/lib/sprockets/railtie.rb index 0b4b0638b2..3d330bd91a 100644 --- a/actionpack/lib/sprockets/railtie.rb +++ b/actionpack/lib/sprockets/railtie.rb @@ -1,100 +1,61 @@ -module Sprockets - class Railtie < Rails::Railtie - def self.using_coffee? - require 'coffee-script' - defined?(CoffeeScript) - rescue LoadError - false - end +require "action_controller/railtie" - def self.using_scss? - require 'sass' - defined?(Sass) - rescue LoadError - false +module Sprockets + autoload :Bootstrap, "sprockets/bootstrap" + autoload :Helpers, "sprockets/helpers" + autoload :Compressors, "sprockets/compressors" + autoload :LazyCompressor, "sprockets/compressors" + autoload :NullCompressor, "sprockets/compressors" + autoload :StaticCompiler, "sprockets/static_compiler" + + # TODO: Get rid of config.assets.enabled + class Railtie < ::Rails::Railtie + config.action_controller.default_asset_host_protocol = :relative + + rake_tasks do + load "sprockets/assets.rake" end - config.app_generators.javascript_engine :coffee if using_coffee? - config.app_generators.stylesheet_engine :scss if using_scss? - - # Configure ActionController to use sprockets. - initializer "sprockets.set_configs", :after => "action_controller.set_configs" do |app| - ActiveSupport.on_load(:action_controller) do - self.use_sprockets = app.config.assets.enabled - end - end + initializer "sprockets.environment", :group => :all do |app| + config = app.config + next unless config.assets.enabled - # We need to configure this after initialization to ensure we collect - # paths from all engines. This hook is invoked exactly before routes - # are compiled. - config.after_initialize do |app| - assets = app.config.assets - next unless assets.enabled + require 'sprockets' - app.assets = asset_environment(app) + app.assets = Sprockets::Environment.new(app.root.to_s) do |env| + env.logger = ::Rails.logger + env.version = ::Rails.env + "-#{config.assets.version}" - ActiveSupport.on_load(:action_view) do - app.assets.context.instance_eval do - include ::ActionView::Helpers::SprocketsHelper + if config.assets.cache_store != false + env.cache = ActiveSupport::Cache.lookup_store(config.assets.cache_store) || ::Rails.cache end end - app.routes.prepend do - mount app.assets => assets.prefix + if config.assets.manifest + path = File.join(config.assets.manifest, "manifest.yml") + else + path = File.join(Rails.public_path, config.assets.prefix, "manifest.yml") end - if config.action_controller.perform_caching - app.assets = app.assets.index + if File.exist?(path) + config.assets.digests = YAML.load_file(path) end - end - protected - - def asset_environment(app) - require "sprockets" - assets = app.config.assets - env = Sprockets::Environment.new(app.root.to_s) - env.static_root = File.join(app.root.join("public"), assets.prefix) - env.paths.concat assets.paths - env.logger = Rails.logger - env.js_compressor = expand_js_compressor(assets.js_compressor) - env.css_compressor = expand_css_compressor(assets.css_compressor) - env - end - - def expand_js_compressor(sym) - case sym - when :closure - require 'closure-compiler' - Closure::Compiler.new - when :uglifier - require 'uglifier' - Uglifier.new - when :yui - require 'yui/compressor' - YUI::JavaScriptCompressor.new - else - sym + ActiveSupport.on_load(:action_view) do + include ::Sprockets::Helpers::RailsHelper + app.assets.context_class.instance_eval do + include ::Sprockets::Helpers::IsolatedHelper + include ::Sprockets::Helpers::RailsHelper + end end end - def expand_css_compressor(sym) - case sym - when :scss - require 'sass' - compressor = Object.new - def compressor.compress(source) - Sass::Engine.new(source, - :syntax => :scss, :style => :compressed - ).render - end - compressor - when :yui - require 'yui/compressor' - YUI::JavaScriptCompressor.new(:munge => true) - else - sym - end + # We need to configure this after initialization to ensure we collect + # paths from all engines. This hook is invoked exactly before routes + # are compiled, and so that other Railties have an opportunity to + # register compressors. + config.after_initialize do |app| + Sprockets::Bootstrap.new(app).run end end end diff --git a/actionpack/lib/sprockets/static_compiler.rb b/actionpack/lib/sprockets/static_compiler.rb new file mode 100644 index 0000000000..32a9d66e6e --- /dev/null +++ b/actionpack/lib/sprockets/static_compiler.rb @@ -0,0 +1,61 @@ +require 'fileutils' + +module Sprockets + class StaticCompiler + attr_accessor :env, :target, :paths + + def initialize(env, target, paths, options = {}) + @env = env + @target = target + @paths = paths + @digest = options.key?(:digest) ? options.delete(:digest) : true + @manifest = options.key?(:manifest) ? options.delete(:manifest) : true + @manifest_path = options.delete(:manifest_path) || target + end + + def compile + manifest = {} + env.each_logical_path do |logical_path| + next unless compile_path?(logical_path) + if asset = env.find_asset(logical_path) + manifest[logical_path] = write_asset(asset) + end + end + write_manifest(manifest) if @manifest + end + + def write_manifest(manifest) + FileUtils.mkdir_p(@manifest_path) + File.open("#{@manifest_path}/manifest.yml", 'wb') do |f| + YAML.dump(manifest, f) + end + end + + def write_asset(asset) + path_for(asset).tap do |path| + filename = File.join(target, path) + FileUtils.mkdir_p File.dirname(filename) + asset.write_to(filename) + asset.write_to("#{filename}.gz") if filename.to_s =~ /\.(css|js)$/ + end + end + + def compile_path?(logical_path) + paths.each do |path| + case path + when Regexp + return true if path.match(logical_path) + when Proc + return true if path.call(logical_path) + else + return true if File.fnmatch(path.to_s, logical_path) + end + end + false + end + + def path_for(asset) + @digest ? asset.digest_path : asset.logical_path + end + end +end |