diff options
Diffstat (limited to 'actionpack/lib')
161 files changed, 3054 insertions, 2017 deletions
diff --git a/actionpack/lib/abstract_controller/asset_paths.rb b/actionpack/lib/abstract_controller/asset_paths.rb index c2a6809f58..dd5f9a1942 100644 --- a/actionpack/lib/abstract_controller/asset_paths.rb +++ b/actionpack/lib/abstract_controller/asset_paths.rb @@ -4,7 +4,7 @@ module AbstractController included do config_accessor :asset_host, :asset_path, :assets_dir, :javascripts_dir, - :stylesheets_dir, :default_asset_host_protocol + :stylesheets_dir, :default_asset_host_protocol, :relative_url_root end end end diff --git a/actionpack/lib/abstract_controller/callbacks.rb b/actionpack/lib/abstract_controller/callbacks.rb index 7004e607a1..44c9ea34ba 100644 --- a/actionpack/lib/abstract_controller/callbacks.rb +++ b/actionpack/lib/abstract_controller/callbacks.rb @@ -8,7 +8,7 @@ module AbstractController include ActiveSupport::Callbacks included do - define_callbacks :process_action, :terminator => "response_body" + define_callbacks :process_action, :terminator => "response_body", :skip_after_callbacks_if_terminated => true end # Override AbstractController::Base's process_action to run the @@ -21,11 +21,9 @@ module AbstractController module ClassMethods # If :only or :except are used, convert the options into the - # primitive form (:per_key) used by ActiveSupport::Callbacks. + # :unless and :if options of ActiveSupport::Callbacks. # The basic idea is that :only => :index gets converted to - # :if => proc {|c| c.action_name == "index" }, but that the - # proc is only evaluated once per action for the lifetime of - # a Rails process. + # :if => proc {|c| c.action_name == "index" }. # # ==== Options # * <tt>only</tt> - The callback should be run only for this action @@ -33,11 +31,11 @@ module AbstractController def _normalize_callback_options(options) if only = options[:only] only = Array(only).map {|o| "action_name == '#{o}'"}.join(" || ") - options[:per_key] = {:if => only} + options[:if] = Array(options[:if]) << only end if except = options[:except] except = Array(except).map {|e| "action_name == '#{e}'"}.join(" || ") - options[:per_key] = {:unless => except} + options[:unless] = Array(options[:unless]) << except end end @@ -167,7 +165,6 @@ module AbstractController # for details on the allowed parameters. 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 @@ -176,7 +173,6 @@ module AbstractController # for details on the allowed parameters. 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 diff --git a/actionpack/lib/abstract_controller/layouts.rb b/actionpack/lib/abstract_controller/layouts.rb index 10aa34c76b..6a6387632c 100644 --- a/actionpack/lib/abstract_controller/layouts.rb +++ b/actionpack/lib/abstract_controller/layouts.rb @@ -66,27 +66,40 @@ module AbstractController # == Inheritance Examples # # class BankController < ActionController::Base - # layout "bank_standard" + # # bank.html.erb exists + # + # class ExchangeController < BankController + # # exchange.html.erb exists + # + # class CurrencyController < BankController # # class InformationController < BankController + # layout "information" # - # class TellerController < BankController + # class TellerController < InformationController # # teller.html.erb exists # - # class TillController < TellerController + # class EmployeeController < InformationController + # # employee.html.erb exists + # layout nil # # class VaultController < BankController # layout :access_level_layout # - # class EmployeeController < BankController - # layout nil + # class TillController < BankController + # layout false # - # 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. + # In these examples, we have three implicit lookup scenrios: + # * The BankController uses the "bank" layout. + # * The ExchangeController uses the "exchange" layout. + # * The CurrencyController inherits the layout from BankController. + # + # However, when a layout is explicitly set, the explicitly set layout wins: + # * The InformationController uses the "information" layout, explicitly set. + # * The TellerController also uses the "information" layout, because the parent explicitly set it. + # * The EmployeeController uses the "employee" layout, because it set the layout to nil, resetting the parent configuration. # * The VaultController chooses a layout dynamically by calling the <tt>access_level_layout</tt> method. - # * The EmployeeController does not use a layout at all. + # * The TillController does not use a layout at all. # # == Types of layouts # @@ -126,6 +139,22 @@ module AbstractController # If no directory is specified for the template name, the template will by default be looked for in <tt>app/views/layouts/</tt>. # Otherwise, it will be looked up relative to the template root. # + # Setting the layout to nil forces it to be looked up in the filesystem and fallbacks to the parent behavior if none exists. + # Setting it to nil is useful to re-enable template lookup overriding a previous configuration set in the parent: + # + # class ApplicationController < ActionController::Base + # layout "application" + # end + # + # class PostsController < ApplicationController + # # Will use "application" layout + # end + # + # class CommentsController < ApplicationController + # # Will search for "comments" layout and fallback "application" layout + # layout nil + # end + # # == Conditional layouts # # If you have a layout that by default is applied to all the actions of a controller, you still have the option of rendering @@ -166,13 +195,15 @@ module AbstractController include Rendering included do - class_attribute :_layout_conditions - remove_possible_method :_layout_conditions - delegate :_layout_conditions, :to => :'self.class' + class_attribute :_layout, :_layout_conditions, + :instance_reader => false, :instance_writer => false + self._layout = nil self._layout_conditions = {} _write_layout_method end + delegate :_layout_conditions, :to => "self.class" + module ClassMethods def inherited(klass) super @@ -188,7 +219,7 @@ module AbstractController # # ==== Returns # * <tt> Boolean</tt> - True if the action has a layout, false otherwise. - def action_has_layout? + def conditional_layout? return unless super conditions = _layout_conditions @@ -213,7 +244,7 @@ module AbstractController # true:: raise an ArgumentError # # ==== Parameters - # * <tt>String, Symbol, false</tt> - The layout to use. + # * <tt>layout</tt> - The layout to use. # # ==== Options (conditions) # * :only - A list of actions to apply this layout to. @@ -224,7 +255,7 @@ module AbstractController conditions.each {|k, v| conditions[k] = Array(v).map {|a| a.to_s} } self._layout_conditions = conditions - @_layout = layout || false # Converts nil to false + self._layout = layout _write_layout_method end @@ -244,43 +275,46 @@ module AbstractController def _write_layout_method remove_possible_method(:_layout) - case defined?(@_layout) ? @_layout : nil - when String - self.class_eval %{def _layout; #{@_layout.inspect} end}, __FILE__, __LINE__ - when Symbol - self.class_eval <<-ruby_eval, __FILE__, __LINE__ + 1 - def _layout - #{@_layout}.tap do |layout| + prefixes = _implied_layout_name =~ /\blayouts/ ? [] : ["layouts"] + name_clause = if name + <<-RUBY + lookup_context.find_all("#{_implied_layout_name}", #{prefixes.inspect}).first || super + RUBY + end + + layout_definition = case _layout + when String + _layout.inspect + when Symbol + <<-RUBY + #{_layout}.tap do |layout| unless layout.is_a?(String) || !layout - raise ArgumentError, "Your layout method :#{@_layout} returned \#{layout}. It " \ + raise ArgumentError, "Your layout method :#{_layout} returned \#{layout}. It " \ "should have returned a String, false, or nil" end end - end - ruby_eval - when Proc - define_method :_layout_from_proc, &@_layout - self.class_eval %{def _layout; _layout_from_proc(self) end}, __FILE__, __LINE__ - when false - self.class_eval %{def _layout; end}, __FILE__, __LINE__ - when true - raise ArgumentError, "Layouts must be specified as a String, Symbol, false, or nil" - when nil - if name - _prefixes = _implied_layout_name =~ /\blayouts/ ? [] : ["layouts"] - - self.class_eval <<-RUBY, __FILE__, __LINE__ + 1 - def _layout - if template_exists?("#{_implied_layout_name}", #{_prefixes.inspect}) - "#{_implied_layout_name}" - else - super - end - end RUBY - end + when Proc + define_method :_layout_from_proc, &_layout + "_layout_from_proc(self)" + when false + nil + when true + raise ArgumentError, "Layouts must be specified as a String, Symbol, false, or nil" + when nil + name_clause end - self.class_eval { private :_layout } + + self.class_eval <<-RUBY, __FILE__, __LINE__ + 1 + def _layout + if conditional_layout? + #{layout_definition} + else + #{name_clause} + end + end + private :_layout + RUBY end end @@ -289,8 +323,7 @@ module AbstractController if _include_layout?(options) layout = options.key?(:layout) ? options.delete(:layout) : :default - value = _layout_for_option(layout) - options[:layout] = (value =~ /\blayouts/ ? value : "layouts/#{value}") if value + options[:layout] = _layout_for_option(layout) end end @@ -305,24 +338,25 @@ module AbstractController @_action_has_layout end + def conditional_layout? + true + end + private # This will be overwritten by _write_layout_method def _layout; end - # Determine the layout for a given name and details, taking into account - # the name type. + # Determine the layout for a given name, taking into account the name type. # # ==== Parameters # * <tt>name</tt> - The name of the template - # * <tt>details</tt> - A list of details to restrict - # the lookup to. By default, layout lookup is limited to the - # formats specified for the current request. def _layout_for_option(name) case name - when String then name - when true then _default_layout(true) - when :default then _default_layout(false) + when String then _normalize_layout(name) + when Proc then name + when true then Proc.new { _default_layout(true) } + when :default then Proc.new { _default_layout(false) } when false, nil then nil else raise ArgumentError, @@ -330,31 +364,32 @@ module AbstractController end end - # Returns the default layout for this controller and a given set of details. + def _normalize_layout(value) + value.is_a?(String) && value !~ /\blayouts/ ? "layouts/#{value}" : value + end + + # Returns the default layout for this controller. # Optionally raises an exception if the layout could not be found. # # ==== Parameters - # * <tt>details</tt> - A list of details to restrict the search by. This - # might include details like the format or locale of the template. - # * <tt>require_layout</tt> - If this is true, raise an ArgumentError - # with details about the fact that the exception could not be - # found (defaults to false) + # * <tt>require_layout</tt> - If set to true and layout is not found, + # an ArgumentError exception is raised (defaults to false) # # ==== Returns # * <tt>template</tt> - The template object for the default layout (or nil) def _default_layout(require_layout = false) begin - layout_name = _layout if action_has_layout? + value = _layout if action_has_layout? rescue NameError => e raise e, "Could not render layout: #{e.message}" end - if require_layout && action_has_layout? && !layout_name + if require_layout && action_has_layout? && !value raise ArgumentError, "There was no default layout for #{self.class} in #{view_paths.inspect}" end - layout_name + _normalize_layout(value) end def _include_layout?(options) diff --git a/actionpack/lib/abstract_controller/logger.rb b/actionpack/lib/abstract_controller/logger.rb index 0b196119f4..a4e31cd2e5 100644 --- a/actionpack/lib/abstract_controller/logger.rb +++ b/actionpack/lib/abstract_controller/logger.rb @@ -1,4 +1,3 @@ -require "active_support/core_ext/logger" require "active_support/benchmarkable" module AbstractController @@ -7,7 +6,7 @@ module AbstractController included do config_accessor :logger - extend ActiveSupport::Benchmarkable + include ActiveSupport::Benchmarkable end end end diff --git a/actionpack/lib/abstract_controller/railties/routes_helpers.rb b/actionpack/lib/abstract_controller/railties/routes_helpers.rb index dec1e9d6d9..6684f46f64 100644 --- a/actionpack/lib/abstract_controller/railties/routes_helpers.rb +++ b/actionpack/lib/abstract_controller/railties/routes_helpers.rb @@ -5,8 +5,8 @@ module AbstractController Module.new do define_method(:inherited) do |klass| super(klass) - if namespace = klass.parents.detect {|m| m.respond_to?(:_railtie) } - klass.send(:include, namespace._railtie.routes.url_helpers) + if namespace = klass.parents.detect { |m| m.respond_to?(:railtie_routes_url_helpers) } + klass.send(:include, namespace.railtie_routes_url_helpers) else klass.send(:include, routes.url_helpers) end diff --git a/actionpack/lib/abstract_controller/rendering.rb b/actionpack/lib/abstract_controller/rendering.rb index 41fdc11196..ddc93464cd 100644 --- a/actionpack/lib/abstract_controller/rendering.rb +++ b/actionpack/lib/abstract_controller/rendering.rb @@ -1,6 +1,5 @@ require "abstract_controller/base" require "action_view" -require "active_support/core_ext/object/instance_variables" module AbstractController class DoubleRenderError < Error @@ -35,7 +34,7 @@ module AbstractController include AbstractController::ViewPaths included do - config_accessor :protected_instance_variables, :instance_reader => false + class_attribute :protected_instance_variables self.protected_instance_variables = [] end @@ -59,19 +58,8 @@ module AbstractController attr_internal_writer :view_context_class - # Explicitly define protected_instance_variables so it can be - # inherited and overwritten by other modules if needed. - def protected_instance_variables - config.protected_instance_variables - end - def view_context_class - @_view_context_class || self.class.view_context_class - end - - def initialize(*) - @_view_context_class = nil - super + @_view_context_class ||= self.class.view_context_class end # An instance of a view class. The default view class is ActionView::Base @@ -120,20 +108,20 @@ module AbstractController view_renderer.render(view_context, options) end - DEFAULT_PROTECTED_INSTANCE_VARIABLES = %w( - @_action_name @_response_body @_formats @_prefixes @_config - @_view_context_class @_view_renderer @_lookup_context - ) + DEFAULT_PROTECTED_INSTANCE_VARIABLES = [ + :@_action_name, :@_response_body, :@_formats, :@_prefixes, :@_config, + :@_view_context_class, :@_view_renderer, :@_lookup_context + ] # This method should return a hash with assigns. # You can overwrite this configuration per controller. # :api: public def view_assigns hash = {} - variables = instance_variable_names + variables = instance_variables variables -= protected_instance_variables variables -= DEFAULT_PROTECTED_INSTANCE_VARIABLES - variables.each { |name| hash[name.to_s[1, name.length]] = instance_variable_get(name) } + variables.each { |name| hash[name[1..-1]] = instance_variable_get(name) } hash end diff --git a/actionpack/lib/abstract_controller/view_paths.rb b/actionpack/lib/abstract_controller/view_paths.rb index e8394447a7..c08b3a0e2a 100644 --- a/actionpack/lib/abstract_controller/view_paths.rb +++ b/actionpack/lib/abstract_controller/view_paths.rb @@ -10,7 +10,7 @@ module AbstractController self._view_paths.freeze end - delegate :find_template, :template_exists?, :view_paths, :formats, :formats=, + delegate :template_exists?, :view_paths, :formats, :formats=, :locale, :locale=, :to => :lookup_context module ClassMethods @@ -89,7 +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::PathSet.new(Array.wrap(paths)) + self._view_paths = ActionView::PathSet.new(Array(paths)) end end end diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb index f4eaa2fd1b..a0c54dbd84 100644 --- a/actionpack/lib/action_controller.rb +++ b/actionpack/lib/action_controller.rb @@ -40,7 +40,6 @@ module ActionController 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' diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index 98bfe72fef..3b82231b15 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -116,12 +116,12 @@ module ActionController # # Title: <%= @post.title %> # - # You don't have to rely on the automated rendering. For example, actions that could result in the rendering of different templates + # 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]) - # case @results + # case @results.count # when 0 then render :action => "no_results" # when 1 then render :action => "show" # when 2..10 then render :action => "show_many" @@ -133,7 +133,7 @@ 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 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 + # 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 @@ -228,8 +228,11 @@ module ActionController include mod end - # Rails 2.x compatibility - include ActionController::Compatibility + # Define some internal variables that should not be propagated to the view. + self.protected_instance_variables = [ + :@_status, :@_headers, :@_params, :@_env, :@_response, :@_request, + :@_view_runtime, :@_stream, :@_url_options, :@_action_has_layout + ] ActiveSupport.run_load_hooks(:action_controller, self) end diff --git a/actionpack/lib/action_controller/caching.rb b/actionpack/lib/action_controller/caching.rb index 14137f2886..112573a38d 100644 --- a/actionpack/lib/action_controller/caching.rb +++ b/actionpack/lib/action_controller/caching.rb @@ -24,7 +24,6 @@ module ActionController #:nodoc: # # config.action_controller.cache_store = :memory_store # config.action_controller.cache_store = :file_store, "/path/to/cache/directory" - # config.action_controller.cache_store = :drb_store, "druby://localhost:9192" # config.action_controller.cache_store = :mem_cache_store, "localhost" # config.action_controller.cache_store = :mem_cache_store, Memcached::Rails.new("localhost:11211") # config.action_controller.cache_store = MyOwnStore.new("parameter") diff --git a/actionpack/lib/action_controller/caching/actions.rb b/actionpack/lib/action_controller/caching/actions.rb index 0031d2701f..ba96735e56 100644 --- a/actionpack/lib/action_controller/caching/actions.rb +++ b/actionpack/lib/action_controller/caching/actions.rb @@ -47,7 +47,8 @@ module ActionController #:nodoc: # And you can also use <tt>:if</tt> (or <tt>:unless</tt>) to pass a # proc that specifies when the action should be cached. # - # Finally, if you are using memcached, you can also pass <tt>:expires_in</tt>. + # As of Rails 3.0, you can also pass <tt>:expires_in</tt> with a time + # interval (in seconds) to schedule expiration of the cached item. # # The following example depicts some of the points made above: # @@ -56,14 +57,14 @@ module ActionController #:nodoc: # # caches_page :public # - # caches_action :index, :if => proc do + # caches_action :index, :if => Proc.new do # !request.format.json? # cache if is not a JSON request # end # # caches_action :show, :cache_path => { :project => 1 }, # :expires_in => 1.hour # - # caches_action :feed, :cache_path => proc do + # caches_action :feed, :cache_path => Proc.new do # if params[:user_id] # user_list_url(params[:user_id, params[:id]) # else @@ -102,8 +103,10 @@ module ActionController #:nodoc: end def _save_fragment(name, options) - content = response_body - content = content.join if content.is_a?(Array) + content = "" + response_body.each do |parts| + content << parts + end if caching_allowed? write_fragment(name, content, options) @@ -116,9 +119,8 @@ module ActionController #:nodoc: def expire_action(options = {}) return unless cache_configured? - actions = options[:action] - if actions.is_a?(Array) - actions.each {|action| expire_action(options.merge(:action => action)) } + if options.is_a?(Hash) && options[:action].is_a?(Array) + options[:action].each {|action| expire_action(options.merge(:action => action)) } else expire_fragment(ActionCachePath.new(self, options, false).path) end @@ -175,7 +177,7 @@ module ActionController #:nodoc: private def normalize!(path) path << 'index' if path[-1] == ?/ - path << ".#{extension}" if extension and !path.ends_with?(extension) + path << ".#{extension}" if extension and !path.split('?').first.ends_with?(".#{extension}") URI.parser.unescape(path) end end diff --git a/actionpack/lib/action_controller/caching/pages.rb b/actionpack/lib/action_controller/caching/pages.rb index 957bb7de6b..159f718029 100644 --- a/actionpack/lib/action_controller/caching/pages.rb +++ b/actionpack/lib/action_controller/caching/pages.rb @@ -16,7 +16,7 @@ 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 + # 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. @@ -38,23 +38,25 @@ module ActionController #:nodoc: extend ActiveSupport::Concern included do - ## - # :singleton-method: # The cache directory should be the document root for the web server and is set using <tt>Base.page_cache_directory = "/document/root"</tt>. # For Rails, this directory has already been set to Rails.public_path (which is usually set to <tt>Rails.root + "/public"</tt>). Changing # this setting can be useful to avoid naming conflicts with files in <tt>public/</tt>, but doing so will likely require configuring your # web server to look in the new location for cached files. - config_accessor :page_cache_directory + class_attribute :page_cache_directory self.page_cache_directory ||= '' - ## - # :singleton-method: # Most Rails requests do not have an extension, such as <tt>/weblog/new</tt>. In these cases, the page caching mechanism will add one in # order to make it easy for the cached files to be picked up properly by the web server. By default, this cache extension is <tt>.html</tt>. # If you want something else, like <tt>.php</tt> or <tt>.shtml</tt>, just set Base.page_cache_extension. In cases where a request already has an # extension, such as <tt>.xml</tt> or <tt>.rss</tt>, page caching will not add an extension. This allows it to work well with RESTful apps. - config_accessor :page_cache_extension + class_attribute :page_cache_extension self.page_cache_extension ||= '.html' + + # The compression used for gzip. If false (default), the page is not compressed. + # If can be a symbol showing the ZLib compression method, for example, :best_compression + # or :best_speed or an integer configuring the compression level. + class_attribute :page_cache_compression + self.page_cache_compression ||= false end module ClassMethods @@ -66,24 +68,31 @@ module ActionController #:nodoc: instrument_page_cache :expire_page, path do File.delete(path) if File.exist?(path) + File.delete(path + '.gz') if File.exist?(path + '.gz') end end # Manually cache the +content+ in the key determined by +path+. Example: # cache_page "I'm the cached content", "/lists/show" - def cache_page(content, path, extension = nil) + def cache_page(content, path, extension = nil, gzip = Zlib::BEST_COMPRESSION) return unless perform_caching path = page_cache_path(path, extension) instrument_page_cache :write_page, path do FileUtils.makedirs(File.dirname(path)) File.open(path, "wb+") { |f| f.write(content) } + if gzip + Zlib::GzipWriter.open(path + '.gz', gzip) { |f| f.write(content) } + end end end - # Caches the +actions+ using the page-caching approach that'll store the cache in a path within the page_cache_directory that + # Caches the +actions+ using the page-caching approach that'll store + # the cache in a path within the page_cache_directory that # matches the triggering url. # + # You can also pass a :gzip option to override the class configuration one. + # # Usage: # # # cache the index action @@ -91,10 +100,28 @@ module ActionController #:nodoc: # # # cache the index action except for JSON requests # caches_page :index, :if => Proc.new { |c| !c.request.format.json? } + # + # # don't gzip images + # caches_page :image, :gzip => false def caches_page(*actions) return unless perform_caching options = actions.extract_options! - after_filter({:only => actions}.merge(options)) { |c| c.cache_page } + + gzip_level = options.fetch(:gzip, page_cache_compression) + gzip_level = case gzip_level + when Symbol + Zlib.const_get(gzip_level.to_s.upcase) + when Fixnum + gzip_level + when false + nil + else + Zlib::BEST_COMPRESSION + end + + after_filter({:only => actions}.merge(options)) do |c| + c.cache_page(nil, nil, gzip_level) + end end private @@ -136,7 +163,7 @@ module ActionController #:nodoc: # 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) + def cache_page(content = nil, options = nil, gzip = Zlib::BEST_COMPRESSION) return unless self.class.perform_caching && caching_allowed? path = case options @@ -152,7 +179,7 @@ module ActionController #:nodoc: extension = ".#{type_symbol}" end - self.class.cache_page(content || response.body, path, extension) + self.class.cache_page(content || response.body, path, extension, gzip) end end diff --git a/actionpack/lib/action_controller/deprecated.rb b/actionpack/lib/action_controller/deprecated.rb index aa0cfc9395..2405bebb97 100644 --- a/actionpack/lib/action_controller/deprecated.rb +++ b/actionpack/lib/action_controller/deprecated.rb @@ -1,3 +1,7 @@ ActionController::AbstractRequest = ActionController::Request = ActionDispatch::Request ActionController::AbstractResponse = ActionController::Response = ActionDispatch::Response -ActionController::Routing = ActionDispatch::Routing
\ No newline at end of file +ActionController::Routing = ActionDispatch::Routing + +ActiveSupport::Deprecation.warn 'ActionController::AbstractRequest and ActionController::Request are deprecated and will be removed, use ActionDispatch::Request instead.' +ActiveSupport::Deprecation.warn 'ActionController::AbstractResponse and ActionController::Response are deprecated and will be removed, use ActionDispatch::Response instead.' +ActiveSupport::Deprecation.warn 'ActionController::Routing is deprecated and will be removed, use ActionDispatch::Routing instead.'
\ No newline at end of file diff --git a/actionpack/lib/action_controller/deprecated/integration_test.rb b/actionpack/lib/action_controller/deprecated/integration_test.rb index 86336b6bc4..54eae48f47 100644 --- a/actionpack/lib/action_controller/deprecated/integration_test.rb +++ b/actionpack/lib/action_controller/deprecated/integration_test.rb @@ -1,2 +1,5 @@ ActionController::Integration = ActionDispatch::Integration ActionController::IntegrationTest = ActionDispatch::IntegrationTest + +ActiveSupport::Deprecation.warn 'ActionController::Integration is deprecated and will be removed, use ActionDispatch::Integration instead.' +ActiveSupport::Deprecation.warn 'ActionController::IntegrationTest is deprecated and will be removed, use ActionDispatch::IntegrationTest instead.' diff --git a/actionpack/lib/action_controller/deprecated/performance_test.rb b/actionpack/lib/action_controller/deprecated/performance_test.rb index fcf47d31a7..c7ba5a2fe7 100644 --- a/actionpack/lib/action_controller/deprecated/performance_test.rb +++ b/actionpack/lib/action_controller/deprecated/performance_test.rb @@ -1 +1,3 @@ ActionController::PerformanceTest = ActionDispatch::PerformanceTest + +ActiveSupport::Deprecation.warn 'ActionController::PerformanceTest is deprecated and will be removed, use ActionDispatch::PerformanceTest instead.' diff --git a/actionpack/lib/action_controller/log_subscriber.rb b/actionpack/lib/action_controller/log_subscriber.rb index 35e29398e6..4c76f4c43b 100644 --- a/actionpack/lib/action_controller/log_subscriber.rb +++ b/actionpack/lib/action_controller/log_subscriber.rb @@ -20,15 +20,18 @@ 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 + status = Rack::Utils.status_code(ActionDispatch::ExceptionWrapper.new({}, payload[:exception]).status_code) 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 + def halted_callback(event) + info "Filter chain halted as #{event.payload[:filter]} rendered or redirected" + end + def send_file(event) message = "Sent file %s" message << " (%.1fms)" diff --git a/actionpack/lib/action_controller/metal.rb b/actionpack/lib/action_controller/metal.rb index 0133b2ecbc..92433ab462 100644 --- a/actionpack/lib/action_controller/metal.rb +++ b/actionpack/lib/action_controller/metal.rb @@ -181,9 +181,13 @@ module ActionController @_status = Rack::Utils.status_code(status) end - def response_body=(val) - body = val.nil? ? nil : (val.respond_to?(:each) ? val : [val]) - super body + def response_body=(body) + body = [body] unless body.nil? || body.respond_to?(:each) + super + end + + def performed? + !!response_body end def dispatch(name, request) #:nodoc: diff --git a/actionpack/lib/action_controller/metal/compatibility.rb b/actionpack/lib/action_controller/metal/compatibility.rb deleted file mode 100644 index 05dca445a4..0000000000 --- a/actionpack/lib/action_controller/metal/compatibility.rb +++ /dev/null @@ -1,58 +0,0 @@ -module ActionController - module Compatibility - extend ActiveSupport::Concern - - class ::ActionController::ActionControllerError < StandardError #:nodoc: - end - - # Temporary hax - included do - ::ActionController::UnknownAction = ::AbstractController::ActionNotFound - ::ActionController::DoubleRenderError = ::AbstractController::DoubleRenderError - - # ROUTES TODO: This should be handled by a middleware and route generation - # should be able to handle SCRIPT_NAME - self.config.relative_url_root = ENV['RAILS_RELATIVE_URL_ROOT'] - - class << self - delegate :default_charset=, :to => "ActionDispatch::Response" - end - - self.protected_instance_variables = %w( - @_status @_headers @_params @_env @_response @_request - @_view_runtime @_stream @_url_options @_action_has_layout - ) - - def rescue_action(env) - raise env["action_dispatch.rescue.exception"] - end - end - - # For old tests - def initialize_template_class(*) end - def assign_shortcuts(*) end - - def _normalize_options(options) - options[:text] = nil if options.delete(:nothing) == true - options[:text] = " " if options.key?(:text) && options[:text].nil? - super - end - - def render_to_body(options) - options[:template].sub!(/^\//, '') if options.key?(:template) - super || " " - end - - def _handle_method_missing - method_missing(@_action_name.to_sym) - end - - def method_for_action(action_name) - super || (respond_to?(:method_missing) && "_handle_method_missing") - end - - def performed? - response_body - end - end -end diff --git a/actionpack/lib/action_controller/metal/conditional_get.rb b/actionpack/lib/action_controller/metal/conditional_get.rb index 809894395d..2a8e4c575e 100644 --- a/actionpack/lib/action_controller/metal/conditional_get.rb +++ b/actionpack/lib/action_controller/metal/conditional_get.rb @@ -23,8 +23,27 @@ module ActionController # This will render the show template if the request isn't sending a matching etag or # If-Modified-Since header and just a <tt>304 Not Modified</tt> response if there's a match. # - def fresh_when(options) - options.assert_valid_keys(:etag, :last_modified, :public) + # You can also just pass a record where last_modified will be set by calling updated_at and the etag by passing the object itself. Example: + # + # def show + # @article = Article.find(params[:id]) + # fresh_when(@article) + # end + # + # When passing a record, you can still set whether the public header: + # + # def show + # @article = Article.find(params[:id]) + # fresh_when(@article, :public => true) + # end + def fresh_when(record_or_options, additional_options = {}) + if record_or_options.is_a? Hash + options = record_or_options + options.assert_valid_keys(:etag, :last_modified, :public) + else + record = record_or_options + options = { :etag => record, :last_modified => record.try(:updated_at) }.merge(additional_options) + end response.etag = options[:etag] if options[:etag] response.last_modified = options[:last_modified] if options[:last_modified] @@ -55,8 +74,34 @@ module ActionController # end # end # end - def stale?(options) - fresh_when(options) + # + # You can also just pass a record where last_modified will be set by calling updated_at and the etag by passing the object itself. Example: + # + # def show + # @article = Article.find(params[:id]) + # + # if stale?(@article) + # @statistics = @article.really_expensive_call + # respond_to do |format| + # # all the supported formats + # end + # end + # end + # + # When passing a record, you can still set whether the public header: + # + # def show + # @article = Article.find(params[:id]) + # + # if stale?(@article, :public => true) + # @statistics = @article.really_expensive_call + # respond_to do |format| + # # all the supported formats + # end + # end + # end + def stale?(record_or_options, additional_options = {}) + fresh_when(record_or_options, additional_options) !request.fresh?(response) end @@ -65,7 +110,7 @@ module ActionController # # Examples: # expires_in 20.minutes - # expires_in 3.hours, :public => true + # expires_in 3.hours, :public => true, :must_revalidate => true # expires_in 3.hours, 'max-stale' => 5.hours, :public => true # # This method will overwrite an existing Cache-Control header. @@ -73,7 +118,11 @@ module ActionController # # The method will also ensure a HTTP Date header for client compatibility. def expires_in(seconds, options = {}) #:doc: - response.cache_control.merge!(:max_age => seconds, :public => options.delete(:public)) + response.cache_control.merge!( + :max_age => seconds, + :public => options.delete(:public), + :must_revalidate => options.delete(:must_revalidate) + ) options.delete(:private) response.cache_control[:extras] = options.map {|k,v| "#{k}=#{v}"} diff --git a/actionpack/lib/action_controller/metal/data_streaming.rb b/actionpack/lib/action_controller/metal/data_streaming.rb index 0670a58d97..30ddf6c16e 100644 --- a/actionpack/lib/action_controller/metal/data_streaming.rb +++ b/actionpack/lib/action_controller/metal/data_streaming.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/file/path' require 'action_controller/metal/exceptions' module ActionController #:nodoc: @@ -115,7 +114,7 @@ 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? diff --git a/actionpack/lib/action_controller/metal/exceptions.rb b/actionpack/lib/action_controller/metal/exceptions.rb index 07024d0a9a..ece9ba3725 100644 --- a/actionpack/lib/action_controller/metal/exceptions.rb +++ b/actionpack/lib/action_controller/metal/exceptions.rb @@ -43,4 +43,4 @@ module ActionController class UnknownHttpMethod < ActionControllerError #:nodoc: end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_controller/metal/force_ssl.rb b/actionpack/lib/action_controller/metal/force_ssl.rb index 0fd42f9d8a..69e37d8713 100644 --- a/actionpack/lib/action_controller/metal/force_ssl.rb +++ b/actionpack/lib/action_controller/metal/force_ssl.rb @@ -29,6 +29,8 @@ module ActionController if !request.ssl? && !Rails.env.development? redirect_options = {:protocol => 'https://', :status => :moved_permanently} redirect_options.merge!(:host => host) if host + redirect_options.merge!(:params => request.query_parameters) + flash.keep redirect_to redirect_options end end diff --git a/actionpack/lib/action_controller/metal/helpers.rb b/actionpack/lib/action_controller/metal/helpers.rb index bd515bba82..d070eaae5d 100644 --- a/actionpack/lib/action_controller/metal/helpers.rb +++ b/actionpack/lib/action_controller/metal/helpers.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/class/attribute' module ActionController @@ -56,7 +55,7 @@ module ActionController include AbstractController::Helpers included do - config_accessor :helpers_path, :include_all_helpers + class_attribute :helpers_path, :include_all_helpers self.helpers_path ||= [] self.include_all_helpers = true end @@ -94,7 +93,7 @@ module ActionController def all_helpers_from_path(path) helpers = [] - Array.wrap(path).each do |_path| + Array(path).each do |_path| extract = /^#{Regexp.quote(_path.to_s)}\/?(.*)_helper.rb$/ helpers += Dir["#{_path}/**/*_helper.rb"].map { |file| file.sub(extract, '\1') } end diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb index 264806cd36..3d46163b74 100644 --- a/actionpack/lib/action_controller/metal/http_authentication.rb +++ b/actionpack/lib/action_controller/metal/http_authentication.rb @@ -1,4 +1,4 @@ -require 'active_support/base64' +require 'base64' require 'active_support/core_ext/object/blank' module ActionController @@ -67,7 +67,7 @@ module ActionController # class PostsController < ApplicationController # REALM = "SuperSecret" # USERS = {"dhh" => "secret", #plain text password - # "dap" => Digest::MD5.hexdigest(["dap",REALM,"secret"].join(":")) #ha1 digest password + # "dap" => Digest::MD5.hexdigest(["dap",REALM,"secret"].join(":"))} #ha1 digest password # # before_filter :authenticate, :except => [:index] # @@ -141,11 +141,11 @@ module ActionController end def decode_credentials(request) - ActiveSupport::Base64.decode64(request.authorization.split(' ', 2).last || '') + ::Base64.decode64(request.authorization.split(' ', 2).last || '') end def encode_credentials(user_name, password) - "Basic #{ActiveSupport::Base64.encode64s("#{user_name}:#{password}")}" + "Basic #{::Base64.strict_encode64("#{user_name}:#{password}")}" end def authentication_request(controller, realm) @@ -192,12 +192,15 @@ module ActionController return false unless password method = request.env['rack.methodoverride.original_method'] || request.env['REQUEST_METHOD'] - uri = credentials[:uri][0,1] == '/' ? request.fullpath : request.url + uri = credentials[:uri][0,1] == '/' ? request.original_fullpath : request.original_url - [true, false].any? do |password_is_ha1| - expected = expected_response(method, uri, credentials, password, password_is_ha1) - expected == credentials[:response] - end + [true, false].any? do |trailing_question_mark| + [true, false].any? do |password_is_ha1| + _uri = trailing_question_mark ? uri + "?" : uri + expected = expected_response(method, _uri, credentials, password, password_is_ha1) + expected == credentials[:response] + end + end end end @@ -286,7 +289,7 @@ module ActionController t = time.to_i hashed = [t, secret_key] digest = ::Digest::MD5.hexdigest(hashed.join(":")) - ActiveSupport::Base64.encode64("#{t}:#{digest}").gsub("\n", '') + ::Base64.strict_encode64("#{t}:#{digest}") end # Might want a shorter timeout depending on whether the request @@ -295,7 +298,7 @@ module ActionController # allow a user to use new nonce without prompting user again for their # username and password. def validate_nonce(secret_key, request, value, seconds_to_timeout=5*60) - t = ActiveSupport::Base64.decode64(value).split(":").first.to_i + t = ::Base64.decode64(value).split(":").first.to_i nonce(secret_key, t) == value && (t - Time.now.to_i).abs <= seconds_to_timeout end diff --git a/actionpack/lib/action_controller/metal/implicit_render.rb b/actionpack/lib/action_controller/metal/implicit_render.rb index e8e465d3ba..ae04b53825 100644 --- a/actionpack/lib/action_controller/metal/implicit_render.rb +++ b/actionpack/lib/action_controller/metal/implicit_render.rb @@ -2,7 +2,7 @@ module ActionController module ImplicitRender def send_action(method, *args) ret = super - default_render unless response_body + default_render unless performed? ret end diff --git a/actionpack/lib/action_controller/metal/instrumentation.rb b/actionpack/lib/action_controller/metal/instrumentation.rb index 777a0ab343..640ebf5f00 100644 --- a/actionpack/lib/action_controller/metal/instrumentation.rb +++ b/actionpack/lib/action_controller/metal/instrumentation.rb @@ -64,7 +64,12 @@ module ActionController end end - protected + private + + # A hook invoked everytime a before callback is halted. + def halted_callback_hook(filter) + ActiveSupport::Notifications.instrument("halted_callback.action_controller", :filter => filter) + end # A hook which allows you to clean up any time taken into account in # views wrongly, like database querying time. diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index 00bd1706e7..80ecc16d53 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -182,7 +182,7 @@ module ActionController #:nodoc: # # def index # @people = Person.all - # respond_with(@person) + # respond_with(@people) # end # end # @@ -191,8 +191,9 @@ module ActionController #:nodoc: def respond_to(*mimes, &block) raise ArgumentError, "respond_to takes either types or a block, never both" if mimes.any? && block_given? - if response = retrieve_response_from_mimes(mimes, &block) - response.call(nil) + if collector = retrieve_collector_from_mimes(mimes, &block) + response = collector.response + response ? response.call : default_render({}) end end @@ -232,10 +233,18 @@ module ActionController #:nodoc: raise "In order to use respond_with, first you need to declare the formats your " << "controller responds to in the class level" if self.class.mimes_for_respond_to.empty? - if response = retrieve_response_from_mimes(&block) + if collector = retrieve_collector_from_mimes(&block) options = resources.size == 1 ? {} : resources.extract_options! - options.merge!(:default_response => response) - (options.delete(:responder) || self.class.responder).call(self, resources, options) + + if defined_response = collector.response + if action = options.delete(:action) + render :action => action + else + defined_response.call + end + else + (options.delete(:responder) || self.class.responder).call(self, resources, options) + end end end @@ -263,15 +272,16 @@ module ActionController #:nodoc: # Collects mimes and return the response for the negotiated format. Returns # nil if :not_acceptable was sent to the client. # - def retrieve_response_from_mimes(mimes=nil, &block) #:nodoc: + def retrieve_collector_from_mimes(mimes=nil, &block) #:nodoc: mimes ||= collect_mimes_from_class_level - collector = Collector.new(mimes) { |options| default_render(options || {}) } + collector = Collector.new(mimes) block.call(collector) if block_given? + format = collector.negotiate_format(request) - if format = request.negotiate_mime(collector.order) + if format self.content_type ||= format.to_s lookup_context.freeze_formats([format.to_sym]) - collector.response_for(format) + collector else head :not_acceptable nil @@ -280,10 +290,10 @@ module ActionController #:nodoc: class Collector #:nodoc: include AbstractController::Collector - attr_accessor :order + attr_accessor :order, :format - def initialize(mimes, &block) - @order, @responses, @default_response = [], {}, block + def initialize(mimes) + @order, @responses = [], {} mimes.each { |mime| send(mime) } end @@ -302,8 +312,12 @@ module ActionController #:nodoc: @responses[mime_type] ||= block end - def response_for(mime) - @responses[mime] || @responses[Mime::ALL] || @default_response + def response + @responses[format] || @responses[Mime::ALL] + end + + def negotiate_format(request) + @format = request.negotiate_mime(order) end end end diff --git a/actionpack/lib/action_controller/metal/params_wrapper.rb b/actionpack/lib/action_controller/metal/params_wrapper.rb index e0d8e1c992..fa760f2658 100644 --- a/actionpack/lib/action_controller/metal/params_wrapper.rb +++ b/actionpack/lib/action_controller/metal/params_wrapper.rb @@ -1,7 +1,6 @@ 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' @@ -43,6 +42,11 @@ module ActionController # wrap_parameters :person, :include => [:username, :password] # end # + # On ActiveRecord models with no +:include+ or +:exclude+ option set, + # if attr_accessible is set on that model, it will only wrap the accessible + # parameters, else it will only wrap the parameters returned by the class + # method attribute_names. + # # If you're going to pass the parameters to an +ActiveModel+ object (such as # +User.new(params[:user])+), you might consider passing the model class to # the method instead. The +ParamsWrapper+ will actually try to determine the @@ -141,7 +145,7 @@ module ActionController # 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 + model_name = self.name.sub(/Controller$/, '').classify begin if model_klass = model_name.safe_constantize @@ -162,7 +166,9 @@ module ActionController unless options[:include] || options[:exclude] model ||= _default_wrap_model - if model.respond_to?(:attribute_names) && model.attribute_names.present? + if model.respond_to?(:accessible_attributes) && model.accessible_attributes.present? + options[:include] = model.accessible_attributes.to_a + elsif model.respond_to?(:attribute_names) && model.attribute_names.present? options[:include] = model.attribute_names end end @@ -173,9 +179,9 @@ module ActionController controller_name.singularize end - 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]) + options[:include] = Array(options[:include]).collect(&:to_s) if options[:include] + options[:exclude] = Array(options[:exclude]).collect(&:to_s) if options[:exclude] + options[:format] = Array(options[:format]) self._wrapper_options = options end diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb index 0355c9f458..b07742e0e1 100644 --- a/actionpack/lib/action_controller/metal/redirecting.rb +++ b/actionpack/lib/action_controller/metal/redirecting.rb @@ -18,7 +18,7 @@ module ActionController # # * <tt>Hash</tt> - The URL will be generated by calling url_for with the +options+. # * <tt>Record</tt> - The URL will be generated by calling url_for with the +options+, which will reference a named URL for that record. - # * <tt>String</tt> starting with <tt>protocol://</tt> (like <tt>http://</tt>) - Is passed straight through as the target for redirection. + # * <tt>String</tt> starting with <tt>protocol://</tt> (like <tt>http://</tt>) or a protocol relative reference (like <tt>//</tt>) - Is passed straight through as the target for redirection. # * <tt>String</tt> not containing a protocol - The current protocol and host is prepended to the string. # * <tt>Proc</tt> - A block that will be executed in the controller's context. Should return any option accepted by +redirect_to+. # * <tt>:back</tt> - Back to the page that issued the request. Useful for forms that are triggered from multiple places. @@ -81,7 +81,8 @@ module ActionController # The scheme name consist of a letter followed by any combination of # letters, digits, and the plus ("+"), period ("."), or hyphen ("-") # characters; and is terminated by a colon (":"). - when %r{^\w[\w+.-]*:.*} + # The protocol relative scheme starts with a double slash "//" + when %r{^(\w[\w+.-]*:|//).*} options when String request.protocol + request.host_with_port + options diff --git a/actionpack/lib/action_controller/metal/renderers.rb b/actionpack/lib/action_controller/metal/renderers.rb index 0ad9dbeda9..6e9ce450ac 100644 --- a/actionpack/lib/action_controller/metal/renderers.rb +++ b/actionpack/lib/action_controller/metal/renderers.rb @@ -1,5 +1,6 @@ require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/object/blank' +require 'set' module ActionController # See <tt>Renderers.add</tt> @@ -12,16 +13,13 @@ module ActionController included do class_attribute :_renderers - self._renderers = {}.freeze + self._renderers = Set.new.freeze end module ClassMethods def use_renderers(*args) - new = _renderers.dup - args.each do |key| - new[key] = RENDERERS[key] - end - self._renderers = new.freeze + renderers = _renderers + args + self._renderers = renderers.freeze end alias use_renderer use_renderers end @@ -31,10 +29,10 @@ module ActionController end def _handle_render_options(options) - _renderers.each do |name, value| - if options.key?(name.to_sym) + _renderers.each do |name| + if options.key?(name) _process_options(options) - return send("_render_option_#{name}", options.delete(name.to_sym), options) + return send("_render_option_#{name}", options.delete(name), options) end end nil @@ -42,7 +40,7 @@ module ActionController # Hash of available renderers, mapping a renderer name to its proc. # Default keys are :json, :js, :xml. - RENDERERS = {} + RENDERERS = Set.new # Adds a new renderer to call within controller actions. # A renderer is invoked by passing its name as an option to @@ -79,7 +77,7 @@ module ActionController # <tt>ActionController::MimeResponds#respond_with</tt> def self.add(key, &block) define_method("_render_option_#{key}", &block) - RENDERERS[key] = block + RENDERERS << key.to_sym end module All diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb index 70fd79bb8b..a677cdf15d 100644 --- a/actionpack/lib/action_controller/metal/rendering.rb +++ b/actionpack/lib/action_controller/metal/rendering.rb @@ -29,6 +29,10 @@ module ActionController self.response_body = nil end + def render_to_body(*) + super || " " + end + private # Normalize arguments by catching blocks and setting them on :update. @@ -44,6 +48,10 @@ module ActionController options[:text] = options[:text].to_text end + if options.delete(:nothing) || (options.key?(:text) && options[:text].nil?) + options[:text] = " " + end + if options[:status] options[:status] = Rack::Utils.status_code(options[:status]) end diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index bc22e39efb..afa9243f02 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -74,7 +74,7 @@ module ActionController #:nodoc: # The actual before_filter that is used. Modify this to change how you handle unverified requests. def verify_authenticity_token unless verified_request? - logger.warn "WARNING: Can't verify CSRF token authenticity" if logger + logger.warn "Can't verify CSRF token authenticity" if logger handle_unverified_request end end diff --git a/actionpack/lib/action_controller/metal/rescue.rb b/actionpack/lib/action_controller/metal/rescue.rb index eb037aa1b0..68cc9a9c9b 100644 --- a/actionpack/lib/action_controller/metal/rescue.rb +++ b/actionpack/lib/action_controller/metal/rescue.rb @@ -1,4 +1,7 @@ module ActionController #:nodoc: + # This module is responsible to provide `rescue_from` helpers + # to controllers and configure when detailed exceptions must be + # shown. module Rescue extend ActiveSupport::Concern include ActiveSupport::Rescuable @@ -12,10 +15,20 @@ module ActionController #:nodoc: super(exception) end + # Override this method if you want to customize when detailed + # exceptions must be shown. This method is only called when + # consider_all_requests_local is false. By default, it returns + # false, but someone may set it to `request.local?` so local + # requests in production still shows the detailed exception pages. + def show_detailed_exceptions? + false + end + private def process_action(*args) super rescue Exception => exception + request.env['action_dispatch.show_detailed_exceptions'] ||= show_detailed_exceptions? rescue_with_handler(exception) || raise(exception) end end diff --git a/actionpack/lib/action_controller/metal/responder.rb b/actionpack/lib/action_controller/metal/responder.rb index b932302a60..4ad64bff20 100644 --- a/actionpack/lib/action_controller/metal/responder.rb +++ b/actionpack/lib/action_controller/metal/responder.rb @@ -84,8 +84,8 @@ module ActionController #:nodoc: # # === Custom options # - # <code>respond_with</code> also allow you to pass options that are forwarded - # to the underlying render call. Those options are only applied success + # <code>respond_with</code> also allows you to pass options that are forwarded + # to the underlying render call. Those options are only applied for success # scenarios. For instance, you can do the following in the create method above: # # def create @@ -95,7 +95,7 @@ module ActionController #:nodoc: # respond_with(@project, @task, :status => 201) # end # - # This will return status 201 if the task was saved with success. If not, + # This will return status 201 if the task was saved successfully. If not, # it will simply ignore the given options and return status 422 and the # resource errors. To customize the failure scenario, you can pass a # a block to <code>respond_with</code>: @@ -129,7 +129,6 @@ module ActionController #:nodoc: @resources = resources @options = options @action = options.delete(:action) - @default_response = options.delete(:default_response) end delegate :head, :render, :redirect_to, :to => :controller @@ -222,11 +221,11 @@ module ActionController #:nodoc: alias :navigation_location :resource_location alias :api_location :resource_location - # If a given response block was given, use it, otherwise call render on + # If a response block was given, use it, otherwise call render on # controller. # def default_render - @default_response.call(options) + controller.default_render(options) end # Display is just a shortcut to render a resource with the current format. diff --git a/actionpack/lib/action_controller/metal/streaming.rb b/actionpack/lib/action_controller/metal/streaming.rb index 5fe5334458..e9783e6919 100644 --- a/actionpack/lib/action_controller/metal/streaming.rb +++ b/actionpack/lib/action_controller/metal/streaming.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/file/path' require 'rack/chunked' module ActionController #:nodoc: @@ -195,7 +194,7 @@ module ActionController #:nodoc: # ==== Passenger # # To be described. - # + # module Streaming extend ActiveSupport::Concern @@ -217,7 +216,7 @@ module ActionController #:nodoc: end end - # Call render_to_body if we are streaming instead of usual +render+. + # Call render_body if we are streaming instead of usual +render+. def _render_template(options) #:nodoc: if options.delete(:stream) Rack::Chunked::Body.new view_renderer.render_body(view_context, options) diff --git a/actionpack/lib/action_controller/railtie.rb b/actionpack/lib/action_controller/railtie.rb index f0c29825ba..a288e69649 100644 --- a/actionpack/lib/action_controller/railtie.rb +++ b/actionpack/lib/action_controller/railtie.rb @@ -14,14 +14,17 @@ module ActionController end initializer "action_controller.initialize_framework_caches" do - ActiveSupport.on_load(:action_controller) { self.cache_store ||= RAILS_CACHE } + ActiveSupport.on_load(:action_controller) { self.cache_store ||= Rails.cache } + end + + initializer "action_controller.assets_config", :group => :all do |app| + app.config.action_controller.assets_dir ||= app.config.paths["public"].first end initializer "action_controller.set_configs" do |app| paths = app.config.paths options = app.config.action_controller - options.assets_dir ||= paths["public"].first options.javascripts_dir ||= paths["public/javascripts"].first options.stylesheets_dir ||= paths["public/stylesheets"].first options.page_cache_directory ||= paths["public"].first @@ -29,6 +32,7 @@ module ActionController # make sure readers methods get compiled options.asset_path ||= app.config.asset_path options.asset_host ||= app.config.asset_host + options.relative_url_root ||= app.config.relative_url_root ActiveSupport.on_load(:action_controller) do include app.routes.mounted_helpers diff --git a/actionpack/lib/action_controller/railties/paths.rb b/actionpack/lib/action_controller/railties/paths.rb index 699c44c62c..bbe63149ad 100644 --- a/actionpack/lib/action_controller/railties/paths.rb +++ b/actionpack/lib/action_controller/railties/paths.rb @@ -6,13 +6,14 @@ module ActionController define_method(:inherited) do |klass| super(klass) - if namespace = klass.parents.detect {|m| m.respond_to?(:_railtie) } - paths = namespace._railtie.paths["app/helpers"].existent + if namespace = klass.parents.detect { |m| m.respond_to?(:railtie_helpers_paths) } + paths = namespace.railtie_helpers_paths else - paths = app.config.helpers_paths + paths = app.helpers_paths end klass.helpers_path = paths + if klass.superclass == ActionController::Base && ActionController::Base.include_all_helpers klass.helper :all end diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index 6913c1ef4a..1e226fc336 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -69,15 +69,12 @@ module ActionController # assert_template :partial => '_customer', :locals => { :customer => @customer } # def assert_template(options = {}, message = nil) - validate_request! - case options when NilClass, String, Symbol options = options.to_s if Symbol === options rendered = @templates - msg = build_message(message, - "expecting <?> but rendering with <?>", - options, rendered.keys.join(', ')) + msg = message || sprintf("expecting <%s> but rendering with <%s>", + options, rendered.keys) assert_block(msg) do if options rendered.any? { |t,num| t.match(options) } @@ -86,6 +83,20 @@ module ActionController end end when Hash + if expected_layout = options[:layout] + msg = message || sprintf("expecting layout <%s> but action rendered <%s>", + expected_layout, @layouts.keys) + + case expected_layout + when String + assert_includes @layouts.keys, expected_layout, msg + when Regexp + assert(@layouts.keys.any? {|l| l =~ expected_layout }, msg) + when nil + assert(@layouts.empty?, msg) + end + end + if expected_partial = options[:partial] if expected_locals = options[:locals] actual_locals = @locals[expected_partial.to_s.sub(/^_/,'')] @@ -94,28 +105,13 @@ module ActionController end elsif expected_count = options[:count] actual_count = @partials[expected_partial] - msg = build_message(message, - "expecting ? to be rendered ? time(s) but rendered ? time(s)", + msg = message || sprintf("expecting %s to be rendered %s time(s) but rendered %s time(s)", expected_partial, expected_count, actual_count) assert(actual_count == expected_count.to_i, msg) - elsif options.key?(:layout) - msg = build_message(message, - "expecting layout <?> but action rendered <?>", - expected_layout, @layouts.keys) - - case layout = options[:layout] - when String - assert(@layouts.include?(expected_layout), msg) - when Regexp - assert(@layouts.any? {|l| l =~ layout }, msg) - when nil - assert(@layouts.empty?, msg) - end else - msg = build_message(message, - "expecting partial <?> but action rendered <?>", + msg = message || sprintf("expecting partial <%s> but action rendered <%s>", options[:partial], @partials.keys) - assert(@partials.include?(expected_partial), msg) + assert_includes @partials, expected_partial, msg end else assert @partials.empty?, @@ -250,6 +246,13 @@ module ActionController # end # end # + # You can also send a real document in the simulated HTTP request. + # + # def test_create + # json = {:book => { :title => "Love Hina" }}.to_json + # post :create, json + # end + # # == Special instance variables # # ActionController::TestCase will also automatically provide the following instance @@ -296,11 +299,11 @@ module ActionController # assert_equal "Dave", cookies[:name] # makes sure that a cookie called :name was set as "Dave" # assert flash.empty? # makes sure that there's nothing in the flash # - # For historic reasons, the assigns hash uses string-based keys. So assigns[:person] won't work, but assigns["person"] will. To + # For historic reasons, the assigns hash uses string-based keys. So <tt>assigns[:person]</tt> won't work, but <tt>assigns["person"]</tt> will. To # appease our yearning for symbols, though, an alternative accessor has been devised using a method call instead of index referencing. - # So assigns(:person) will work just like assigns["person"], but again, assigns[:person] will not work. + # So <tt>assigns(:person)</tt> will work just like <tt>assigns["person"]</tt>, but again, <tt>assigns[:person]</tt> will not work. # - # On top of the collections, you have the complete url that a given action redirected to available in redirect_to_url. + # On top of the collections, you have the complete url that a given action redirected to available in <tt>redirect_to_url</tt>. # # For redirects within the same controller, you can even call follow_redirect and the redirect will be followed, triggering another # action call which can then be asserted against. @@ -324,6 +327,12 @@ module ActionController # # assert_redirected_to page_url(:title => 'foo') class TestCase < ActiveSupport::TestCase + + # Use AS::TestCase for the base class when describing a model + register_spec_type(self) do |desc| + desc < ActionController::Base + end + module Behavior extend ActiveSupport::Concern include ActionDispatch::TestProcess @@ -342,7 +351,7 @@ module ActionController def tests(controller_class) case controller_class when String, Symbol - self.controller_class = "#{controller_class.to_s.underscore}_controller".camelize.constantize + self.controller_class = "#{controller_class.to_s.camelize}Controller".constantize when Class self.controller_class = controller_class else @@ -374,28 +383,28 @@ module ActionController end # Executes a request simulating GET HTTP method and set/volley the response - def get(action, parameters = nil, session = nil, flash = nil) - process(action, parameters, session, flash, "GET") + def get(action, *args) + process(action, "GET", *args) end # Executes a request simulating POST HTTP method and set/volley the response - def post(action, parameters = nil, session = nil, flash = nil) - process(action, parameters, session, flash, "POST") + def post(action, *args) + process(action, "POST", *args) end # Executes a request simulating PUT HTTP method and set/volley the response - def put(action, parameters = nil, session = nil, flash = nil) - process(action, parameters, session, flash, "PUT") + def put(action, *args) + process(action, "PUT", *args) end # Executes a request simulating DELETE HTTP method and set/volley the response - def delete(action, parameters = nil, session = nil, flash = nil) - process(action, parameters, session, flash, "DELETE") + def delete(action, *args) + process(action, "DELETE", *args) end # Executes a request simulating HEAD HTTP method and set/volley the response def head(action, parameters = nil, session = nil, flash = nil) - process(action, parameters, session, flash, "HEAD") + process(action, "HEAD", parameters, session, flash) end def xml_http_request(request_method, action, parameters = nil, session = nil, flash = nil) @@ -421,19 +430,20 @@ module ActionController end end - def process(action, parameters = nil, session = nil, flash = nil, http_method = 'GET') + def process(action, http_method = 'GET', *args) + check_required_ivars + http_method, args = handle_old_process_api(http_method, args) + + if args.first.is_a?(String) && http_method != 'HEAD' + @request.env['RAW_POST_DATA'] = args.shift + end + + parameters, session, flash = args + # 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| - if !(instance_variable_names.include?(iv_name) || instance_variable_names.include?(iv_name.to_sym)) || instance_variable_get(iv_name).nil? - raise "#{iv_name} is nil: make sure you set it in your test's setup method." - end - end - @request.recycle! @response.recycle! @controller.response_body = nil @@ -494,6 +504,26 @@ module ActionController end private + def check_required_ivars + # Sanity check for required instance variables so we can give an + # understandable error message. + [:@routes, :@controller, :@request, :@response].each do |iv_name| + if !instance_variable_defined?(iv_name) || instance_variable_get(iv_name).nil? + raise "#{iv_name} is nil: make sure you set it in your test's setup method." + end + end + end + + def handle_old_process_api(http_method, args) + # 4.0: Remove this method. + if http_method.is_a?(Hash) + ActiveSupport::Deprecation.warn("TestCase#process now expects the HTTP method as second argument: process(action, http_method, params, session, flash)") + args.unshift(http_method) + http_method = args.last.is_a?(String) ? args.last : "GET" + end + + [http_method, args] + end def build_request_uri(action, parameters) unless @request.env["PATH_INFO"] 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 af06bffa16..24ffc28710 100644 --- a/actionpack/lib/action_controller/vendor/html-scanner/html/sanitizer.rb +++ b/actionpack/lib/action_controller/vendor/html-scanner/html/sanitizer.rb @@ -171,7 +171,7 @@ module HTML def contains_bad_protocols?(attr_name, value) uri_attributes.include?(attr_name) && - (value =~ /(^[^\/:]*):|(�*58)|(p)|(%|%)3A/ && !allowed_protocols.include?(value.split(protocol_separator).first.downcase)) + (value =~ /(^[^\/:]*):|(�*58)|(p)|(%|%)3A/ && !allowed_protocols.include?(value.split(protocol_separator).first.downcase.strip)) end end end diff --git a/actionpack/lib/action_controller/vendor/html-scanner/html/tokenizer.rb b/actionpack/lib/action_controller/vendor/html-scanner/html/tokenizer.rb index c252e01cf5..8ac8d34430 100644 --- a/actionpack/lib/action_controller/vendor/html-scanner/html/tokenizer.rb +++ b/actionpack/lib/action_controller/vendor/html-scanner/html/tokenizer.rb @@ -23,7 +23,7 @@ module HTML #:nodoc: # Create a new Tokenizer for the given text. def initialize(text) - text.encode! if text.encoding_aware? + text.encode! @scanner = StringScanner.new(text) @position = 0 @line = 0 diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb index 1e92d14542..a9542a7d1b 100644 --- a/actionpack/lib/action_dispatch.rb +++ b/actionpack/lib/action_dispatch.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2011 David Heinemeier Hansson +# Copyright (c) 2004-2012 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -29,6 +29,7 @@ $:.unshift(activemodel_path) if File.directory?(activemodel_path) && !$:.include require 'active_support' require 'active_support/dependencies/autoload' +require 'active_support/core_ext/module/attribute_accessors' require 'action_pack' require 'active_model' @@ -51,17 +52,18 @@ module ActionDispatch autoload :BestStandardsSupport autoload :Callbacks autoload :Cookies + autoload :DebugExceptions + autoload :ExceptionWrapper autoload :Flash autoload :Head autoload :ParamsParser + autoload :PublicExceptions autoload :Reloader autoload :RemoteIp - autoload :Rescue autoload :ShowExceptions autoload :Static end - autoload :ClosedError, 'action_dispatch/middleware/closed_error' autoload :MiddlewareStack, 'action_dispatch/middleware/stack' autoload :Routing @@ -86,6 +88,8 @@ module ActionDispatch autoload :CacheStore, 'action_dispatch/middleware/session/cache_store' end + mattr_accessor :test_app + autoload_under 'testing' do autoload :Assertions autoload :Integration diff --git a/actionpack/lib/action_dispatch/http/cache.rb b/actionpack/lib/action_dispatch/http/cache.rb index 7d83ecb058..5ee4c044ea 100644 --- a/actionpack/lib/action_dispatch/http/cache.rb +++ b/actionpack/lib/action_dispatch/http/cache.rb @@ -4,14 +4,18 @@ module ActionDispatch module Http module Cache module Request + + HTTP_IF_MODIFIED_SINCE = 'HTTP_IF_MODIFIED_SINCE'.freeze + HTTP_IF_NONE_MATCH = 'HTTP_IF_NONE_MATCH'.freeze + def if_modified_since - if since = env['HTTP_IF_MODIFIED_SINCE'] + if since = env[HTTP_IF_MODIFIED_SINCE] Time.rfc2822(since) rescue nil end end def if_none_match - env['HTTP_IF_NONE_MATCH'] + env[HTTP_IF_NONE_MATCH] end def not_modified?(modified_at) @@ -43,17 +47,17 @@ module ActionDispatch alias :etag? :etag def last_modified - if last = headers['Last-Modified'] + if last = headers[LAST_MODIFIED] Time.httpdate(last) end end def last_modified? - headers.include?('Last-Modified') + headers.include?(LAST_MODIFIED) end def last_modified=(utc_time) - headers['Last-Modified'] = utc_time.httpdate + headers[LAST_MODIFIED] = utc_time.httpdate end def date @@ -72,16 +76,20 @@ module ActionDispatch def etag=(etag) key = ActiveSupport::Cache.expand_cache_key(etag) - @etag = self["ETag"] = %("#{Digest::MD5.hexdigest(key)}") + @etag = self[ETAG] = %("#{Digest::MD5.hexdigest(key)}") end private + LAST_MODIFIED = "Last-Modified".freeze + ETAG = "ETag".freeze + CACHE_CONTROL = "Cache-Control".freeze + def prepare_cache_control! @cache_control = {} - @etag = self["ETag"] + @etag = self[ETAG] - if cache_control = self["Cache-Control"] + if cache_control = self[CACHE_CONTROL] cache_control.split(/,\s*/).each do |segment| first, last = segment.split("=") @cache_control[first.to_sym] = last || true @@ -95,28 +103,32 @@ module ActionDispatch end end - DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate" + DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate".freeze + NO_CACHE = "no-cache".freeze + PUBLIC = "public".freeze + PRIVATE = "private".freeze + MUST_REVALIDATE = "must-revalidate".freeze def set_conditional_cache_control! - return if self["Cache-Control"].present? + return if self[CACHE_CONTROL].present? control = @cache_control if control.empty? - headers["Cache-Control"] = DEFAULT_CACHE_CONTROL + headers[CACHE_CONTROL] = DEFAULT_CACHE_CONTROL elsif control[:no_cache] - headers["Cache-Control"] = "no-cache" + headers[CACHE_CONTROL] = NO_CACHE else extras = control[:extras] max_age = control[:max_age] options = [] options << "max-age=#{max_age.to_i}" if max_age - options << (control[:public] ? "public" : "private") - options << "must-revalidate" if control[:must_revalidate] + options << (control[:public] ? PUBLIC : PRIVATE) + options << MUST_REVALIDATE if control[:must_revalidate] options.concat(extras) if extras - headers["Cache-Control"] = options.join(", ") + headers[CACHE_CONTROL] = options.join(", ") end end end diff --git a/actionpack/lib/action_dispatch/http/filter_parameters.rb b/actionpack/lib/action_dispatch/http/filter_parameters.rb index 8dd1af7f3d..132b0c82bc 100644 --- a/actionpack/lib/action_dispatch/http/filter_parameters.rb +++ b/actionpack/lib/action_dispatch/http/filter_parameters.rb @@ -50,7 +50,7 @@ module ActionDispatch end def env_filter - parameter_filter_for(Array.wrap(@env["action_dispatch.parameter_filter"]) << /RAW_POST_DATA/) + parameter_filter_for(Array(@env["action_dispatch.parameter_filter"]) + [/RAW_POST_DATA/, "rack.request.form_vars"]) end def parameter_filter_for(filters) diff --git a/actionpack/lib/action_dispatch/http/headers.rb b/actionpack/lib/action_dispatch/http/headers.rb index 505d5560b1..040b51e040 100644 --- a/actionpack/lib/action_dispatch/http/headers.rb +++ b/actionpack/lib/action_dispatch/http/headers.rb @@ -1,5 +1,3 @@ -require 'active_support/memoizable' - module ActionDispatch module Http class Headers < ::Hash diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb index 8a9f9c4315..2152351703 100644 --- a/actionpack/lib/action_dispatch/http/mime_type.rb +++ b/actionpack/lib/action_dispatch/http/mime_type.rb @@ -82,6 +82,7 @@ module Mime class << self TRAILING_STAR_REGEXP = /(text|application)\/\*/ + Q_SEPARATOR_REGEXP = /;\s*q=/ def lookup(string) LOOKUP[string] @@ -103,11 +104,12 @@ module Mime SET << Mime.const_get(symbol.to_s.upcase) ([string] + mime_type_synonyms).each { |str| LOOKUP[str] = SET.last } unless skip_lookup - ([symbol.to_s] + extension_synonyms).each { |ext| EXTENSION_LOOKUP[ext] = SET.last } + ([symbol] + extension_synonyms).each { |ext| EXTENSION_LOOKUP[ext.to_s] = SET.last } end def parse(accept_header) if accept_header !~ /,/ + accept_header = accept_header.split(Q_SEPARATOR_REGEXP).first if accept_header =~ TRAILING_STAR_REGEXP parse_data_with_trailing_star($1) else @@ -117,7 +119,7 @@ module Mime # keep track of creation order to keep the subsequent sort stable list, index = [], 0 accept_header.split(/,/).each do |header| - params, q = header.split(/;\s*q=/) + params, q = header.split(Q_SEPARATOR_REGEXP) if params.present? params.strip! diff --git a/actionpack/lib/action_dispatch/http/mime_types.rb b/actionpack/lib/action_dispatch/http/mime_types.rb index 3da4f91051..a6b3aee5e7 100644 --- a/actionpack/lib/action_dispatch/http/mime_types.rb +++ b/actionpack/lib/action_dispatch/http/mime_types.rb @@ -9,7 +9,7 @@ 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/jpeg", :jpeg, [], %w(jpg jpeg jpe pjpeg) 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) diff --git a/actionpack/lib/action_dispatch/http/parameter_filter.rb b/actionpack/lib/action_dispatch/http/parameter_filter.rb index 1480e8f77c..490b46c990 100644 --- a/actionpack/lib/action_dispatch/http/parameter_filter.rb +++ b/actionpack/lib/action_dispatch/http/parameter_filter.rb @@ -20,6 +20,8 @@ module ActionDispatch @filters.present? end + FILTERED = '[FILTERED]'.freeze + def compiled_filter @compiled_filter ||= begin regexps, blocks = compile_filter @@ -29,7 +31,7 @@ module ActionDispatch original_params.each do |key, value| if regexps.find { |r| key =~ r } - value = '[FILTERED]' + value = FILTERED elsif value.is_a?(Hash) value = filter(value) elsif value.is_a?(Array) diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb index ef5d207b26..d9b63faf5e 100644 --- a/actionpack/lib/action_dispatch/http/parameters.rb +++ b/actionpack/lib/action_dispatch/http/parameters.rb @@ -41,8 +41,6 @@ module ActionDispatch # you'll get a weird error down the road, but our form handling # should really prevent that from happening def encode_params(params) - return params unless "ruby".encoding_aware? - if params.is_a?(String) return params.force_encoding("UTF-8").encode! elsif !params.is_a?(Hash) diff --git a/actionpack/lib/action_dispatch/http/rack_cache.rb b/actionpack/lib/action_dispatch/http/rack_cache.rb index cc8edee300..003ae4029d 100644 --- a/actionpack/lib/action_dispatch/http/rack_cache.rb +++ b/actionpack/lib/action_dispatch/http/rack_cache.rb @@ -8,8 +8,7 @@ module ActionDispatch new end - # TODO: Finally deal with the RAILS_CACHE global - def initialize(store = RAILS_CACHE) + def initialize(store = Rails.cache) @store = store end @@ -33,7 +32,7 @@ module ActionDispatch new end - def initialize(store = RAILS_CACHE) + def initialize(store = Rails.cache) @store = store end diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index 7a5237dcf3..0a0ebe7fad 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -35,14 +35,6 @@ module ActionDispatch METHOD end - def self.new(env) - if request = env["action_dispatch.request"] && request.instance_of?(self) - return request - end - - super - end - def key?(key) @env.key?(key) end @@ -94,31 +86,31 @@ module ActionDispatch end # Is this a GET (or HEAD) request? - # Equivalent to <tt>request.request_method == :get</tt>. + # Equivalent to <tt>request.request_method_symbol == :get</tt>. def get? HTTP_METHOD_LOOKUP[request_method] == :get end # Is this a POST request? - # Equivalent to <tt>request.request_method == :post</tt>. + # Equivalent to <tt>request.request_method_symbol == :post</tt>. def post? HTTP_METHOD_LOOKUP[request_method] == :post end # Is this a PUT request? - # Equivalent to <tt>request.request_method == :put</tt>. + # Equivalent to <tt>request.request_method_symbol == :put</tt>. def put? HTTP_METHOD_LOOKUP[request_method] == :put end # Is this a DELETE request? - # Equivalent to <tt>request.request_method == :delete</tt>. + # Equivalent to <tt>request.request_method_symbol == :delete</tt>. def delete? HTTP_METHOD_LOOKUP[request_method] == :delete end # Is this a HEAD request? - # Equivalent to <tt>request.method == :head</tt>. + # Equivalent to <tt>request.method_symbol == :head</tt>. def head? HTTP_METHOD_LOOKUP[method] == :head end @@ -130,10 +122,18 @@ module ActionDispatch Http::Headers.new(@env) end + def original_fullpath + @original_fullpath ||= (env["ORIGINAL_FULLPATH"] || fullpath) + end + def fullpath @fullpath ||= super end + def original_url + base_url + original_fullpath + end + def media_type content_mime_type.to_s end @@ -155,24 +155,7 @@ module ActionDispatch @ip ||= super end - # Which IP addresses are "trusted proxies" that can be stripped from - # the right-hand-side of X-Forwarded-For. - # - # http://en.wikipedia.org/wiki/Private_network#Private_IPv4_address_spaces. - TRUSTED_PROXIES = %r{ - ^127\.0\.0\.1$ | # localhost - ^(10 | # private IP 10.x.x.x - 172\.(1[6-9]|2[0-9]|3[0-1]) | # private IP in the range 172.16.0.0 .. 172.31.255.255 - 192\.168 # private IP 192.168.x.x - )\. - }x - - # Determines originating IP address. REMOTE_ADDR is the standard - # but will fail if the user is behind a proxy. HTTP_CLIENT_IP and/or - # 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- - # delimited list in the case of multiple chained proxies; the last - # address which is not trusted is the originating IP. + # Originating IP address, usually set by the RemoteIp middleware. def remote_ip @remote_ip ||= (@env["action_dispatch.remote_ip"] || ip).to_s end @@ -206,7 +189,7 @@ module ActionDispatch # variable is already set, wrap it in a StringIO. def body if raw_post = @env['RAW_POST_DATA'] - raw_post.force_encoding(Encoding::BINARY) if raw_post.respond_to?(:force_encoding) + raw_post.force_encoding(Encoding::BINARY) StringIO.new(raw_post) else @env['rack.input'] diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index f1e85559a3..84732085f0 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -53,8 +53,10 @@ module ActionDispatch # :nodoc: # information. attr_accessor :charset, :content_type - CONTENT_TYPE = "Content-Type" - + CONTENT_TYPE = "Content-Type".freeze + SET_COOKIE = "Set-Cookie".freeze + LOCATION = "Location".freeze + cattr_accessor(:default_charset) { "utf-8" } include Rack::Response::Helpers @@ -66,10 +68,10 @@ module ActionDispatch # :nodoc: @sending_file = false @blank = false - if content_type = self["Content-Type"] + if content_type = self[CONTENT_TYPE] type, charset = content_type.split(/;\s*charset=/) @content_type = Mime::Type.lookup(type) - @charset = charset || "UTF-8" + @charset = charset || self.class.default_charset end prepare_cache_control! @@ -109,9 +111,9 @@ module ActionDispatch # :nodoc: end def body - str = '' - each { |part| str << part.to_s } - str + strings = [] + each { |part| strings << part.to_s } + strings.join end EMPTY = " " @@ -119,14 +121,7 @@ module ActionDispatch # :nodoc: def body=(body) @blank = true if body == EMPTY - # 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). - @body = if body.respond_to?(:to_str) || !body.respond_to?(:each) - [body] - else - body - end + @body = body.respond_to?(:each) ? body : [body] end def body_parts @@ -142,12 +137,12 @@ module ActionDispatch # :nodoc: end def location - headers['Location'] + headers[LOCATION] end alias_method :redirect_url, :location def location=(url) - headers['Location'] = url + headers[LOCATION] = url end def close @@ -158,10 +153,10 @@ module ActionDispatch # :nodoc: assign_default_content_type_and_charset! handle_conditional_get! - @header["Set-Cookie"] = @header["Set-Cookie"].join("\n") if @header["Set-Cookie"].respond_to?(:join) + @header[SET_COOKIE] = @header[SET_COOKIE].join("\n") if @header[SET_COOKIE].respond_to?(:join) if [204, 304].include?(@status) - @header.delete "Content-Type" + @header.delete CONTENT_TYPE [@status, @header, []] else [@status, @header, self] @@ -175,7 +170,7 @@ module ActionDispatch # :nodoc: # assert_equal 'AuthorOfNewPage', r.cookies['author'] def cookies cookies = {} - if header = self["Set-Cookie"] + if header = self[SET_COOKIE] header = header.split("\n") if header.respond_to?(:to_str) header.each do |cookie| if pair = cookie.split(';').first diff --git a/actionpack/lib/action_dispatch/http/upload.rb b/actionpack/lib/action_dispatch/http/upload.rb index 94fa747a79..5ab99d1061 100644 --- a/actionpack/lib/action_dispatch/http/upload.rb +++ b/actionpack/lib/action_dispatch/http/upload.rb @@ -22,8 +22,8 @@ module ActionDispatch 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 + # Encode the filename in the utf8 encoding, unless it is nil + if filename filename.force_encoding("UTF-8").encode! else filename diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb index c8ddd07bfa..80ffbe575b 100644 --- a/actionpack/lib/action_dispatch/http/url.rb +++ b/actionpack/lib/action_dispatch/http/url.rb @@ -1,6 +1,8 @@ module ActionDispatch module Http module URL + IP_HOST_REGEXP = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/ + mattr_accessor :tld_length self.tld_length = 1 @@ -21,7 +23,7 @@ module ActionDispatch end def url_for(options = {}) - unless options[:host].present? || options[:only_path].present? + if options[:host].blank? && options[:only_path].blank? raise ArgumentError, 'Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true' end @@ -52,7 +54,7 @@ module ActionDispatch private def named_host?(host) - !(host.nil? || /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.match(host)) + host && IP_HOST_REGEXP !~ host end def rewrite_authentication(options) @@ -64,13 +66,13 @@ module ActionDispatch end def host_or_subdomain_and_domain(options) - return options[:host] if options[:subdomain].nil? && options[:domain].nil? + return options[:host] if !named_host?(options[:host]) || (options[:subdomain].nil? && options[:domain].nil?) tld_length = options[:tld_length] || @@tld_length host = "" unless options[:subdomain] == false - host << (options[:subdomain] || extract_subdomain(options[:host], tld_length)) + host << (options[:subdomain] || extract_subdomain(options[:host], tld_length)).to_param host << "." end host << (options[:domain] || extract_domain(options[:host], tld_length)) @@ -167,7 +169,7 @@ module ActionDispatch # 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(".") + ActionDispatch::Http::URL.extract_subdomain(host, tld_length) end end end diff --git a/actionpack/lib/action_dispatch/middleware/closed_error.rb b/actionpack/lib/action_dispatch/middleware/closed_error.rb deleted file mode 100644 index 0a4db47f4b..0000000000 --- a/actionpack/lib/action_dispatch/middleware/closed_error.rb +++ /dev/null @@ -1,7 +0,0 @@ -module ActionDispatch - class ClosedError < StandardError #:nodoc: - def initialize(kind) - super "Cannot modify #{kind} because it was closed. This means it was already streamed back to the client or converted to HTTP headers." - end - end -end diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index a4ffd40a66..25f1db8228 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -121,10 +121,6 @@ module ActionDispatch @cookies = {} end - attr_reader :closed - alias :closed? :closed - def close!; @closed = true end - def each(&block) @cookies.each(&block) end @@ -165,7 +161,6 @@ module ActionDispatch # Sets the cookie named +name+. The second argument may be the very cookie # value, or a hash of options as documented above. def []=(key, options) - raise ClosedError, :cookies if closed? if options.is_a?(Hash) options.symbolize_keys! value = options[:value] @@ -196,6 +191,15 @@ module ActionDispatch value end + # Whether the given cookie is to be deleted by this CookieJar. + # Like <tt>[]=</tt>, you can pass in an options hash to test if a + # deletion applies to a specific <tt>:path</tt>, <tt>:domain</tt> etc. + def deleted?(key, options = {}) + options.symbolize_keys! + handle_options(options) + @delete_cookies[key.to_s] == options + 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) } @@ -243,10 +247,13 @@ module ActionDispatch @delete_cookies.clear end + mattr_accessor :always_write_cookie + self.always_write_cookie = false + private def write_cookie?(cookie) - @secure || !cookie[:secure] || defined?(Rails.env) && Rails.env.development? + @secure || !cookie[:secure] || always_write_cookie end end @@ -256,7 +263,6 @@ module ActionDispatch end def []=(key, options) - raise ClosedError, :cookies if closed? if options.is_a?(Hash) options.symbolize_keys! else @@ -295,7 +301,6 @@ module ActionDispatch end def []=(key, options) - raise ClosedError, :cookies if closed? if options.is_a?(Hash) options.symbolize_keys! options[:value] = @verifier.generate(options[:value]) @@ -349,9 +354,6 @@ module ActionDispatch end [status, headers, body] - ensure - cookie_jar = ActionDispatch::Request.new(env).cookie_jar unless cookie_jar - cookie_jar.close! end end end diff --git a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb new file mode 100644 index 0000000000..b903f98761 --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb @@ -0,0 +1,82 @@ +require 'action_dispatch/http/request' +require 'action_dispatch/middleware/exception_wrapper' + +module ActionDispatch + # This middleware is responsible for logging exceptions and + # showing a debugging page in case the request is local. + class DebugExceptions + RESCUES_TEMPLATE_PATH = File.join(File.dirname(__FILE__), 'templates') + + def initialize(app) + @app = app + end + + def call(env) + begin + response = @app.call(env) + + if response[1]['X-Cascade'] == 'pass' + body = response[2] + body.close if body.respond_to?(:close) + raise ActionController::RoutingError, "No route matches [#{env['REQUEST_METHOD']}] #{env['PATH_INFO'].inspect}" + end + rescue Exception => exception + raise exception if env['action_dispatch.show_exceptions'] == false + end + + exception ? render_exception(env, exception) : response + end + + private + + def render_exception(env, exception) + wrapper = ExceptionWrapper.new(env, exception) + log_error(env, wrapper) + + if env['action_dispatch.show_detailed_exceptions'] + template = ActionView::Base.new([RESCUES_TEMPLATE_PATH], + :request => Request.new(env), + :exception => wrapper.exception, + :application_trace => wrapper.application_trace, + :framework_trace => wrapper.framework_trace, + :full_trace => wrapper.full_trace + ) + + file = "rescues/#{wrapper.rescue_template}" + body = template.render(:template => file, :layout => 'rescues/layout') + render(wrapper.status_code, body) + else + raise exception + end + end + + def render(status, body) + [status, {'Content-Type' => "text/html; charset=#{Response.default_charset}", 'Content-Length' => body.bytesize.to_s}, [body]] + end + + def log_error(env, wrapper) + logger = logger(env) + return unless logger + + exception = wrapper.exception + + trace = wrapper.application_trace + trace = wrapper.framework_trace if trace.empty? + + ActiveSupport::Deprecation.silence do + message = "\n#{exception.class} (#{exception.message}):\n" + message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code) + message << " " << trace.join("\n ") + logger.fatal("#{message}\n\n") + end + end + + def logger(env) + env['action_dispatch.logger'] || stderr_logger + end + + def stderr_logger + @stderr_logger ||= ActiveSupport::Logger.new($stderr) + end + end +end diff --git a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb new file mode 100644 index 0000000000..c0532c80c4 --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb @@ -0,0 +1,78 @@ +require 'action_controller/metal/exceptions' +require 'active_support/core_ext/exception' + +module ActionDispatch + class ExceptionWrapper + cattr_accessor :rescue_responses + @@rescue_responses = Hash.new(:internal_server_error) + @@rescue_responses.merge!( + 'ActionController::RoutingError' => :not_found, + 'AbstractController::ActionNotFound' => :not_found, + 'ActionController::MethodNotAllowed' => :method_not_allowed, + 'ActionController::NotImplemented' => :not_implemented, + 'ActionController::InvalidAuthenticityToken' => :unprocessable_entity + ) + + cattr_accessor :rescue_templates + @@rescue_templates = Hash.new('diagnostics') + @@rescue_templates.merge!( + 'ActionView::MissingTemplate' => 'missing_template', + 'ActionController::RoutingError' => 'routing_error', + 'AbstractController::ActionNotFound' => 'unknown_action', + 'ActionView::Template::Error' => 'template_error' + ) + + attr_reader :env, :exception + + def initialize(env, exception) + @env = env + @exception = original_exception(exception) + end + + def rescue_template + @@rescue_templates[@exception.class.name] + end + + def status_code + Rack::Utils.status_code(@@rescue_responses[@exception.class.name]) + end + + def application_trace + clean_backtrace(:silent) + end + + def framework_trace + clean_backtrace(:noise) + end + + def full_trace + clean_backtrace(:all) + end + + private + + def original_exception(exception) + if registered_original_exception?(exception) + exception.original_exception + else + exception + end + end + + def registered_original_exception?(exception) + exception.respond_to?(:original_exception) && @@rescue_responses.has_key?(exception.original_exception.class.name) + end + + def clean_backtrace(*args) + if backtrace_cleaner + backtrace_cleaner.clean(@exception.backtrace, *args) + else + @exception.backtrace + end + end + + def backtrace_cleaner + @backtrace_cleaner ||= @env['action_dispatch.backtrace_cleaner'] + end + end +end
\ No newline at end of file diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb index e59404ef68..cff0877030 100644 --- a/actionpack/lib/action_dispatch/middleware/flash.rb +++ b/actionpack/lib/action_dispatch/middleware/flash.rb @@ -78,7 +78,7 @@ module ActionDispatch include Enumerable def initialize #:nodoc: - @used = Set.new + @discard = Set.new @closed = false @flashes = {} @now = nil @@ -93,8 +93,7 @@ module ActionDispatch end def []=(k, v) #:nodoc: - raise ClosedError, :flash if closed? - keep(k) + @discard.delete k @flashes[k] = v end @@ -103,7 +102,7 @@ module ActionDispatch end def update(h) #:nodoc: - h.keys.each { |k| keep(k) } + @discard.subtract h.keys @flashes.update h self end @@ -117,6 +116,7 @@ module ActionDispatch end def delete(key) + @discard.delete key @flashes.delete key self end @@ -130,6 +130,7 @@ module ActionDispatch end def clear + @discard.clear @flashes.clear end @@ -140,7 +141,7 @@ module ActionDispatch alias :merge! :update def replace(h) #:nodoc: - @used = Set.new + @discard.clear @flashes.replace h self end @@ -159,16 +160,13 @@ module ActionDispatch @now ||= FlashNow.new(self) end - attr_reader :closed - alias :closed? :closed - def close!; @closed = true; end - # Keeps either the entire current flash or a specific flash entry available for the next action: # # flash.keep # keeps the entire flash # flash.keep(:notice) # keeps only the "notice" entry, the rest of the flash is discarded def keep(k = nil) - use(k, false) + @discard.subtract Array(k || keys) + k ? self[k] : self end # Marks the entire flash or a single flash entry to be discarded by the end of the current action: @@ -176,24 +174,16 @@ module ActionDispatch # flash.discard # discard the entire flash at the end of the current action # flash.discard(:warning) # discard only the "warning" entry at the end of the current action def discard(k = nil) - use(k) + @discard.merge Array(k || keys) + k ? self[k] : self end # Mark for removal entries that were kept, and delete unkept ones. # # This method is called automatically by filters, so you generally don't need to care about it. def sweep #:nodoc: - keys.each do |k| - unless @used.include?(k) - @used << k - else - delete(k) - @used.delete(k) - end - end - - # clean up after keys that could have been left over by calling reject! or shift on the flash - (@used - keys).each{ |k| @used.delete(k) } + @discard.each { |k| @flashes.delete k } + @discard.replace @flashes.keys end # Convenience accessor for flash[:alert] @@ -217,22 +207,9 @@ module ActionDispatch end protected - - def now_is_loaded? - !!@now - end - - # Used internally by the <tt>keep</tt> and <tt>discard</tt> methods - # use() # marks the entire flash as used - # use('msg') # marks the "msg" entry as used - # use(nil, false) # marks the entire flash as unused (keeps it around for one more action) - # use('msg', false) # marks the "msg" entry as unused (keeps it around for one more action) - # Returns the single value for the key you asked to be marked (un)used or the FlashHash itself - # if no key is passed. - def use(key = nil, used = true) - Array(key || keys).each { |k| used ? @used << k : @used.delete(k) } - return key ? self[key] : self - end + def now_is_loaded? + @now + end end def initialize(app) @@ -258,7 +235,6 @@ module ActionDispatch end env[KEY] = new_hash - new_hash.close! end if session.key?('flash') && session['flash'].empty? diff --git a/actionpack/lib/action_dispatch/middleware/params_parser.rb b/actionpack/lib/action_dispatch/middleware/params_parser.rb index d4208ca96e..1cb803ffb9 100644 --- a/actionpack/lib/action_dispatch/middleware/params_parser.rb +++ b/actionpack/lib/action_dispatch/middleware/params_parser.rb @@ -52,14 +52,9 @@ module ActionDispatch false end rescue Exception => e # YAML, XML or Ruby code block errors - logger.debug "Error occurred while parsing request parameters.\nContents:\n\n#{request.raw_post}" + logger(env).debug "Error occurred while parsing request parameters.\nContents:\n\n#{request.raw_post}" - raise - { "body" => request.raw_post, - "content_type" => request.content_mime_type, - "content_length" => request.content_length, - "exception" => "#{e.message} (#{e.class})", - "backtrace" => e.backtrace } + raise e end def content_type_from_legacy_post_data_format_header(env) @@ -73,8 +68,8 @@ module ActionDispatch nil end - def logger - defined?(Rails.logger) ? Rails.logger : Logger.new($stderr) + def logger(env) + env['action_dispatch.logger'] || ActiveSupport::Logger.new($stderr) end end end diff --git a/actionpack/lib/action_dispatch/middleware/public_exceptions.rb b/actionpack/lib/action_dispatch/middleware/public_exceptions.rb new file mode 100644 index 0000000000..85b8d178bf --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/public_exceptions.rb @@ -0,0 +1,30 @@ +module ActionDispatch + # A simple Rack application that renders exceptions in the given public path. + class PublicExceptions + attr_accessor :public_path + + def initialize(public_path) + @public_path = public_path + end + + def call(env) + status = env["PATH_INFO"][1..-1] + locale_path = "#{public_path}/#{status}.#{I18n.locale}.html" if I18n.locale + path = "#{public_path}/#{status}.html" + + if locale_path && File.exist?(locale_path) + render(status, File.read(locale_path)) + elsif File.exist?(path) + render(status, File.read(path)) + else + [404, { "X-Cascade" => "pass" }, []] + end + end + + private + + def render(status, body) + [status, {'Content-Type' => "text/html; charset=#{Response.default_charset}", 'Content-Length' => body.bytesize.to_s}, [body]] + end + end +end
\ No newline at end of file diff --git a/actionpack/lib/action_dispatch/middleware/reloader.rb b/actionpack/lib/action_dispatch/middleware/reloader.rb index 29289a76b4..a0388e0e13 100644 --- a/actionpack/lib/action_dispatch/middleware/reloader.rb +++ b/actionpack/lib/action_dispatch/middleware/reloader.rb @@ -43,34 +43,47 @@ module ActionDispatch # Execute all prepare callbacks. def self.prepare! - new(nil).run_callbacks :prepare + new(nil).prepare! end # Execute all cleanup callbacks. def self.cleanup! - new(nil).run_callbacks :cleanup + new(nil).cleanup! end - def initialize(app) + def initialize(app, condition=nil) @app = app - end - - module CleanupOnClose - def close - super if defined?(super) - ensure - ActionDispatch::Reloader.cleanup! - end + @condition = condition || lambda { true } + @validated = true end def call(env) - run_callbacks :prepare + @validated = @condition.call + prepare! + response = @app.call(env) - response[2].extend(CleanupOnClose) + response[2] = ::Rack::BodyProxy.new(response[2]) { cleanup! } + response rescue Exception - run_callbacks :cleanup + cleanup! raise end + + def prepare! #:nodoc: + run_callbacks :prepare if validated? + end + + def cleanup! #:nodoc: + run_callbacks :cleanup if validated? + ensure + @validated = true + end + + private + + def validated? #:nodoc: + @validated + end end end diff --git a/actionpack/lib/action_dispatch/middleware/remote_ip.rb b/actionpack/lib/action_dispatch/middleware/remote_ip.rb index c7d710b98e..d924f21fad 100644 --- a/actionpack/lib/action_dispatch/middleware/remote_ip.rb +++ b/actionpack/lib/action_dispatch/middleware/remote_ip.rb @@ -2,50 +2,82 @@ module ActionDispatch class RemoteIp class IpSpoofAttackError < StandardError ; end - class RemoteIpGetter - def initialize(env, check_ip_spoofing, trusted_proxies) - @env = env - @check_ip_spoofing = check_ip_spoofing - @trusted_proxies = trusted_proxies + # IP addresses that are "trusted proxies" that can be stripped from + # the comma-delimited list in the X-Forwarded-For header. See also: + # http://en.wikipedia.org/wiki/Private_network#Private_IPv4_address_spaces + TRUSTED_PROXIES = %r{ + ^127\.0\.0\.1$ | # localhost + ^(10 | # private IP 10.x.x.x + 172\.(1[6-9]|2[0-9]|3[0-1]) | # private IP in the range 172.16.0.0 .. 172.31.255.255 + 192\.168 # private IP 192.168.x.x + )\. + }x + + attr_reader :check_ip, :proxies + + def initialize(app, check_ip_spoofing = true, custom_proxies = nil) + @app = app + @check_ip = check_ip_spoofing + @proxies = case custom_proxies + when Regexp + custom_proxies + when nil + TRUSTED_PROXIES + else + Regexp.union(TRUSTED_PROXIES, custom_proxies) end + end - def remote_addrs - @remote_addrs ||= begin - list = @env['REMOTE_ADDR'] ? @env['REMOTE_ADDR'].split(/[,\s]+/) : [] - list.reject { |addr| addr =~ @trusted_proxies } - end + def call(env) + env["action_dispatch.remote_ip"] = GetIp.new(env, self) + @app.call(env) + end + + class GetIp + def initialize(env, middleware) + @env = env + @middleware = middleware + @calculated_ip = false end - def to_s - return remote_addrs.first if remote_addrs.any? - - forwarded_ips = @env['HTTP_X_FORWARDED_FOR'] ? @env['HTTP_X_FORWARDED_FOR'].strip.split(/[,\s]+/) : [] - - if client_ip = @env['HTTP_CLIENT_IP'] - if @check_ip_spoofing && !forwarded_ips.include?(client_ip) - # We don't know which came from the proxy, and which from the user - raise IpSpoofAttackError, "IP spoofing attack?!" \ - "HTTP_CLIENT_IP=#{@env['HTTP_CLIENT_IP'].inspect}" \ - "HTTP_X_FORWARDED_FOR=#{@env['HTTP_X_FORWARDED_FOR'].inspect}" - end - return client_ip + # Determines originating IP address. REMOTE_ADDR is the standard + # but will be wrong if the user is behind a proxy. Proxies will set + # HTTP_CLIENT_IP and/or HTTP_X_FORWARDED_FOR, so we prioritize those. + # HTTP_X_FORWARDED_FOR may be a comma-delimited list in the case of + # multiple chained proxies. The last address which is not a known proxy + # will be the originating IP. + def calculate_ip + client_ip = @env['HTTP_CLIENT_IP'] + forwarded_ips = ips_from('HTTP_X_FORWARDED_FOR') + remote_addrs = ips_from('REMOTE_ADDR') + + check_ip = client_ip && @middleware.check_ip + if check_ip && !forwarded_ips.include?(client_ip) + # We don't know which came from the proxy, and which from the user + raise IpSpoofAttackError, "IP spoofing attack?!" \ + "HTTP_CLIENT_IP=#{@env['HTTP_CLIENT_IP'].inspect}" \ + "HTTP_X_FORWARDED_FOR=#{@env['HTTP_X_FORWARDED_FOR'].inspect}" end - return forwarded_ips.reject { |ip| ip =~ @trusted_proxies }.last || @env["REMOTE_ADDR"] + not_proxy = client_ip || forwarded_ips.first || remote_addrs.first + + # Return first REMOTE_ADDR if there are no other options + not_proxy || ips_from('REMOTE_ADDR', :allow_proxies).first end - end - def initialize(app, check_ip_spoofing = true, trusted_proxies = nil) - @app = app - @check_ip_spoofing = check_ip_spoofing - regex = '(^127\.0\.0\.1$|^(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\.)' - regex << "|(#{trusted_proxies})" if trusted_proxies - @trusted_proxies = Regexp.new(regex, "i") - end + def to_s + return @ip if @calculated_ip + @calculated_ip = true + @ip = calculate_ip + end - def call(env) - env["action_dispatch.remote_ip"] = RemoteIpGetter.new(env, @check_ip_spoofing, @trusted_proxies) - @app.call(env) + protected + + def ips_from(header, allow_proxies = false) + ips = @env[header] ? @env[header].strip.split(/[,\s]+/) : [] + allow_proxies ? ips : ips.reject{|ip| ip =~ @middleware.proxies } + end end + end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_dispatch/middleware/request_id.rb b/actionpack/lib/action_dispatch/middleware/request_id.rb index bee446c8a5..d5a0b80fd5 100644 --- a/actionpack/lib/action_dispatch/middleware/request_id.rb +++ b/actionpack/lib/action_dispatch/middleware/request_id.rb @@ -33,7 +33,7 @@ module ActionDispatch end def internal_request_id - SecureRandom.hex(16) + SecureRandom.uuid end end end diff --git a/actionpack/lib/action_dispatch/middleware/rescue.rb b/actionpack/lib/action_dispatch/middleware/rescue.rb deleted file mode 100644 index aee672112c..0000000000 --- a/actionpack/lib/action_dispatch/middleware/rescue.rb +++ /dev/null @@ -1,26 +0,0 @@ -module ActionDispatch - class Rescue - def initialize(app, rescuers = {}, &block) - @app, @rescuers = app, {} - rescuers.each { |exception, rescuer| rescue_from(exception, rescuer) } - instance_eval(&block) if block_given? - end - - def call(env) - @app.call(env) - rescue Exception => exception - if rescuer = @rescuers[exception.class.name] - env['action_dispatch.rescue.exception'] = exception - rescuer.call(env) - else - raise exception - end - end - - protected - def rescue_from(exception, rescuer) - exception = exception.class.name if exception.is_a?(Exception) - @rescuers[exception.to_s] = rescuer - end - end -end diff --git a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb index 6bcf099d2c..6a8e690d18 100644 --- a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb @@ -30,7 +30,7 @@ module ActionDispatch def generate_sid sid = SecureRandom.hex(16) - sid.encode!('UTF-8') if sid.respond_to?(:encode!) + sid.encode!('UTF-8') sid end @@ -74,10 +74,6 @@ module ActionDispatch class AbstractStore < Rack::Session::Abstract::ID include Compatibility include StaleSessionCheck - - def destroy_session(env, sid, options) - raise '#destroy_session needs to be implemented.' - end end end end diff --git a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb index 2fa68c64c5..836136eb95 100644 --- a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb @@ -1,172 +1,57 @@ -require 'active_support/core_ext/exception' -require 'action_controller/metal/exceptions' -require 'active_support/notifications' require 'action_dispatch/http/request' +require 'action_dispatch/middleware/exception_wrapper' module ActionDispatch - # This middleware rescues any exception returned by the application and renders - # nice exception pages if it's being rescued locally. + # This middleware rescues any exception returned by the application + # and calls an exceptions app that will wrap it in a format for the end user. + # + # The exceptions app should be passed as parameter on initialization + # of ShowExceptions. Everytime there is an exception, ShowExceptions will + # store the exception in env["action_dispatch.exception"], rewrite the + # PATH_INFO to the exception status code and call the rack app. + # + # If the application returns a "X-Cascade" pass response, this middleware + # will send an empty response as result with the correct status code. + # If any exception happens inside the exceptions app, this middleware + # catches the exceptions and returns a FAILSAFE_RESPONSE. class ShowExceptions - RESCUES_TEMPLATE_PATH = File.join(File.dirname(__FILE__), 'templates') - - cattr_accessor :rescue_responses - @@rescue_responses = Hash.new(:internal_server_error) - @@rescue_responses.update({ - 'ActionController::RoutingError' => :not_found, - 'AbstractController::ActionNotFound' => :not_found, - 'ActiveRecord::RecordNotFound' => :not_found, - 'ActiveRecord::StaleObjectError' => :conflict, - 'ActiveRecord::RecordInvalid' => :unprocessable_entity, - 'ActiveRecord::RecordNotSaved' => :unprocessable_entity, - 'ActionController::MethodNotAllowed' => :method_not_allowed, - 'ActionController::NotImplemented' => :not_implemented, - 'ActionController::InvalidAuthenticityToken' => :unprocessable_entity - }) - - cattr_accessor :rescue_templates - @@rescue_templates = Hash.new('diagnostics') - @@rescue_templates.update({ - 'ActionView::MissingTemplate' => 'missing_template', - 'ActionController::RoutingError' => 'routing_error', - 'AbstractController::ActionNotFound' => 'unknown_action', - 'ActionView::Template::Error' => 'template_error' - }) - FAILSAFE_RESPONSE = [500, {'Content-Type' => 'text/html'}, ["<html><body><h1>500 Internal Server Error</h1>" << "If you are the administrator of this website, then please read this web " << "application's log file and/or the web server's log file to find out what " << "went wrong.</body></html>"]] - def initialize(app, consider_all_requests_local = false) + def initialize(app, exceptions_app) @app = app - @consider_all_requests_local = consider_all_requests_local + @exceptions_app = exceptions_app end def call(env) begin - status, headers, body = @app.call(env) - exception = nil - - # Only this middleware cares about RoutingError. So, let's just raise - # it here. - if headers['X-Cascade'] == 'pass' - raise ActionController::RoutingError, "No route matches [#{env['REQUEST_METHOD']}] #{env['PATH_INFO'].inspect}" - end + response = @app.call(env) rescue Exception => exception raise exception if env['action_dispatch.show_exceptions'] == false end - exception ? render_exception(env, exception) : [status, headers, body] + response || render_exception(env, exception) end private - def render_exception(env, exception) - log_error(exception) - exception = original_exception(exception) - - request = Request.new(env) - if @consider_all_requests_local || request.local? - rescue_action_locally(request, exception) - else - rescue_action_in_public(exception) - end - rescue Exception => failsafe_error - $stderr.puts "Error during failsafe response: #{failsafe_error}\n #{failsafe_error.backtrace * "\n "}" - FAILSAFE_RESPONSE - end - - # Render detailed diagnostics for unhandled exceptions rescued from - # a controller action. - def rescue_action_locally(request, exception) - template = ActionView::Base.new([RESCUES_TEMPLATE_PATH], - :request => request, - :exception => exception, - :application_trace => application_trace(exception), - :framework_trace => framework_trace(exception), - :full_trace => full_trace(exception) - ) - file = "rescues/#{@@rescue_templates[exception.class.name]}" - body = template.render(:template => file, :layout => 'rescues/layout') - render(status_code(exception), body) - end - # Attempts to render a static error page based on the - # <tt>status_code</tt> thrown, or just return headers if no such file - # exists. At first, it will try to render a localized static page. - # For example, if a 500 error is being handled Rails and locale is :da, - # it will first attempt to render the file at <tt>public/500.da.html</tt> - # then attempt to render <tt>public/500.html</tt>. If none of them exist, - # the body of the response will be left empty. - def rescue_action_in_public(exception) - status = status_code(exception) - locale_path = "#{public_path}/#{status}.#{I18n.locale}.html" if I18n.locale - path = "#{public_path}/#{status}.html" - - if locale_path && File.exist?(locale_path) - render(status, File.read(locale_path)) - elsif File.exist?(path) - render(status, File.read(path)) - else - render(status, '') - end - end - - def status_code(exception) - Rack::Utils.status_code(@@rescue_responses[exception.class.name]) - end - - def render(status, body) - [status, {'Content-Type' => "text/html; charset=#{Response.default_charset}", 'Content-Length' => body.bytesize.to_s}, [body]] - end - - def public_path - defined?(Rails.public_path) ? Rails.public_path : 'public_path' - end - - def log_error(exception) - return unless logger - - ActiveSupport::Deprecation.silence do - message = "\n#{exception.class} (#{exception.message}):\n" - message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code) - message << " " << application_trace(exception).join("\n ") - logger.fatal("#{message}\n\n") - end - end - - def application_trace(exception) - clean_backtrace(exception, :silent) - end - - def framework_trace(exception) - clean_backtrace(exception, :noise) - end - - def full_trace(exception) - clean_backtrace(exception, :all) - end - - def clean_backtrace(exception, *args) - defined?(Rails) && Rails.respond_to?(:backtrace_cleaner) ? - Rails.backtrace_cleaner.clean(exception.backtrace, *args) : - exception.backtrace - end - - def logger - defined?(Rails.logger) ? Rails.logger : Logger.new($stderr) - end - - def original_exception(exception) - if registered_original_exception?(exception) - exception.original_exception - else - exception - end + def render_exception(env, exception) + wrapper = ExceptionWrapper.new(env, exception) + status = wrapper.status_code + env["action_dispatch.exception"] = wrapper.exception + env["PATH_INFO"] = "/#{status}" + response = @exceptions_app.call(env) + response[1]['X-Cascade'] == 'pass' ? pass_response(status) : response + rescue Exception => failsafe_error + $stderr.puts "Error during failsafe response: #{failsafe_error}\n #{failsafe_error.backtrace * "\n "}" + FAILSAFE_RESPONSE end - def registered_original_exception?(exception) - exception.respond_to?(:original_exception) && @@rescue_responses.has_key?(exception.original_exception.class.name) + def pass_response(status) + [status, {"Content-Type" => "text/html; charset=#{Response.default_charset}", "Content-Length" => "0"}, []] end end end diff --git a/actionpack/lib/action_dispatch/middleware/stack.rb b/actionpack/lib/action_dispatch/middleware/stack.rb index a4308f528c..28e8fbdab8 100644 --- a/actionpack/lib/action_dispatch/middleware/stack.rb +++ b/actionpack/lib/action_dispatch/middleware/stack.rb @@ -93,8 +93,9 @@ module ActionDispatch end def swap(target, *args, &block) - insert_before(target, *args, &block) - delete(target) + index = assert_index(target, :before) + insert(index, *args, &block) + middlewares.delete_at(index + 1) end def delete(target) diff --git a/actionpack/lib/action_dispatch/middleware/static.rb b/actionpack/lib/action_dispatch/middleware/static.rb index 404943d720..63b7422287 100644 --- a/actionpack/lib/action_dispatch/middleware/static.rb +++ b/actionpack/lib/action_dispatch/middleware/static.rb @@ -1,4 +1,5 @@ require 'rack/utils' +require 'active_support/core_ext/uri' module ActionDispatch class FileHandler @@ -11,14 +12,14 @@ module ActionDispatch def match?(path) path = path.dup - full_path = path.empty? ? @root : File.join(@root, ::Rack::Utils.unescape(path)) + full_path = path.empty? ? @root : File.join(@root, escape_glob_chars(unescape_path(path))) paths = "#{full_path}#{ext}" matches = Dir[paths] match = matches.detect { |m| File.file?(m) } if match match.sub!(@compiled_root, '') - match + ::Rack::Utils.escape(match) end end @@ -32,6 +33,14 @@ module ActionDispatch "{,#{ext},/index#{ext}}" end end + + def unescape_path(path) + URI.parser.unescape(path) + end + + def escape_glob_chars(path) + path.gsub(/[*?{}\[\]]/, "\\\\\\&") + end end class Static diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb index 6e71fd7ddc..1a308707d1 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb @@ -16,6 +16,7 @@ background-color: #eee; padding: 10px; font-size: 11px; + white-space: pre-wrap; } a { color: #000; } diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.erb index ccfa858cce..f06c07daa5 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.erb @@ -1,10 +1,15 @@ <h1>Routing Error</h1> <p><pre><%=h @exception.message %></pre></p> -<% unless @exception.failures.empty? %><p> - <h2>Failure reasons:</h2> - <ol> - <% @exception.failures.each do |route, reason| %> - <li><code><%=h route.inspect.gsub('\\', '') %></code> failed because <%=h reason.downcase %></li> - <% end %> - </ol> -</p><% end %> +<% unless @exception.failures.empty? %> + <p> + <h2>Failure reasons:</h2> + <ol> + <% @exception.failures.each do |route, reason| %> + <li><code><%=h route.inspect.gsub('\\', '') %></code> failed because <%=h reason.downcase %></li> + <% end %> + </ol> + </p> +<% end %> +<p> + Try running <code>rake routes</code> for more information on available routes. +</p>
\ No newline at end of file diff --git a/actionpack/lib/action_dispatch/railtie.rb b/actionpack/lib/action_dispatch/railtie.rb index 1af89858d1..35f901c575 100644 --- a/actionpack/lib/action_dispatch/railtie.rb +++ b/actionpack/lib/action_dispatch/railtie.rb @@ -9,11 +9,28 @@ module ActionDispatch config.action_dispatch.best_standards_support = true config.action_dispatch.tld_length = 1 config.action_dispatch.ignore_accept_header = false - config.action_dispatch.rack_cache = {:metastore => "rails:/", :entitystore => "rails:/", :verbose => true} + config.action_dispatch.rescue_templates = { } + config.action_dispatch.rescue_responses = { } + config.action_dispatch.default_charset = nil + + config.action_dispatch.rack_cache = { + :metastore => "rails:/", + :entitystore => "rails:/", + :verbose => true + } initializer "action_dispatch.configure" do |app| ActionDispatch::Http::URL.tld_length = app.config.action_dispatch.tld_length ActionDispatch::Request.ignore_accept_header = app.config.action_dispatch.ignore_accept_header + ActionDispatch::Response.default_charset = app.config.action_dispatch.default_charset || app.config.encoding + + ActionDispatch::ExceptionWrapper.rescue_responses.merge!(config.action_dispatch.rescue_responses) + ActionDispatch::ExceptionWrapper.rescue_templates.merge!(config.action_dispatch.rescue_templates) + + config.action_dispatch.always_write_cookie = Rails.env.development? if config.action_dispatch.always_write_cookie.nil? + ActionDispatch::Cookies::CookieJar.always_write_cookie = config.action_dispatch.always_write_cookie + + ActionDispatch.test_app = app end end end diff --git a/actionpack/lib/action_dispatch/routing.rb b/actionpack/lib/action_dispatch/routing.rb index 1dcd83ceb5..107fe80d1f 100644 --- a/actionpack/lib/action_dispatch/routing.rb +++ b/actionpack/lib/action_dispatch/routing.rb @@ -190,7 +190,7 @@ module ActionDispatch # Examples: # # match 'post/:id' => 'posts#show', :via => :get - # match 'post/:id' => "posts#create_comment', :via => :post + # match 'post/:id' => 'posts#create_comment', :via => :post # # Now, if you POST to <tt>/posts/:id</tt>, it will route to the <tt>create_comment</tt> action. A GET on the same # URL will route to the <tt>show</tt> action. @@ -203,7 +203,7 @@ module ActionDispatch # Examples: # # get 'post/:id' => 'posts#show' - # post 'post/:id' => "posts#create_comment' + # post 'post/:id' => 'posts#create_comment' # # This syntax is less verbose and the intention is more apparent to someone else reading your code, # however if your route needs to respond to more than one HTTP method (or all methods) then using the @@ -277,7 +277,6 @@ module ActionDispatch # module Routing autoload :Mapper, 'action_dispatch/routing/mapper' - autoload :Route, 'action_dispatch/routing/route' autoload :RouteSet, 'action_dispatch/routing/route_set' autoload :RoutesProxy, 'action_dispatch/routing/routes_proxy' autoload :UrlFor, 'action_dispatch/routing/url_for' @@ -285,10 +284,5 @@ module ActionDispatch SEPARATORS = %w( / . ? ) #:nodoc: HTTP_METHODS = [:get, :head, :post, :put, :delete, :options] #:nodoc: - - # A helper module to hold URL related helpers. - module Helpers #:nodoc: - include PolymorphicRoutes - end end end diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 09a8c10043..cf9c0d7b6a 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -16,7 +16,7 @@ module ActionDispatch end end - attr_reader :app + attr_reader :app, :constraints def initialize(app, constraints, request) @app, @constraints, @request = app, constraints, request @@ -54,6 +54,7 @@ module ActionDispatch def initialize(set, scope, path, options) @set, @scope = set, scope + @segment_keys = nil @options = (@scope[:options] || {}).merge(options) @path = normalize_path(path) normalize_options! @@ -213,7 +214,9 @@ module ActionDispatch end def segment_keys - @segment_keys ||= Journey::Path::Pattern.new( + return @segment_keys if @segment_keys + + @segment_keys = Journey::Path::Pattern.new( Journey::Router::Strexp.compile(@path, requirements, SEPARATORS) ).names end @@ -285,7 +288,7 @@ module ActionDispatch # A pattern can also point to a +Rack+ endpoint i.e. anything that # responds to +call+: # - # match 'photos/:id' => lambda {|hash| [200, {}, "Coming soon" } + # match 'photos/:id' => lambda {|hash| [200, {}, "Coming soon"] } # match 'photos/:id' => PhotoRackApp # # Yes, controller actions are just rack endpoints # match 'photos/:id' => PhotosController.action(:show) @@ -374,10 +377,6 @@ module ActionDispatch # # Matches any request starting with 'path' # match 'path' => 'c#a', :anchor => false def match(path, options=nil) - mapping = Mapping.new(@set, @scope, path, options || {}) - app, conditions, requirements, defaults, as, anchor = mapping.to_route - @set.add_route(app, conditions, requirements, defaults, as, anchor) - self end # Mount a Rack-based application to be used within the application. @@ -468,7 +467,7 @@ module ActionDispatch # # get 'bacon', :to => 'food#bacon' def get(*args, &block) - map_method(:get, *args, &block) + map_method(:get, args, &block) end # Define a route that only recognizes HTTP POST. @@ -478,7 +477,7 @@ module ActionDispatch # # post 'bacon', :to => 'food#bacon' def post(*args, &block) - map_method(:post, *args, &block) + map_method(:post, args, &block) end # Define a route that only recognizes HTTP PUT. @@ -488,25 +487,24 @@ module ActionDispatch # # put 'bacon', :to => 'food#bacon' def put(*args, &block) - map_method(:put, *args, &block) + map_method(:put, args, &block) end - # Define a route that only recognizes HTTP PUT. + # Define a route that only recognizes HTTP DELETE. # For supported arguments, see <tt>Base#match</tt>. # # Example: # # delete 'broccoli', :to => 'food#broccoli' def delete(*args, &block) - map_method(:delete, *args, &block) + map_method(:delete, args, &block) end private - def map_method(method, *args, &block) + def map_method(method, args, &block) options = args.extract_options! options[:via] = method - args.push(options) - match(*args, &block) + match(*args, options, &block) self end end @@ -735,7 +733,7 @@ module ActionDispatch # if the user should be given access to that route, or +false+ if the user should not. # # class Iphone - # def self.matches(request) + # def self.matches?(request) # request.env["HTTP_USER_AGENT"] =~ /iPhone/ # end # end @@ -867,8 +865,6 @@ module ActionDispatch CANONICAL_ACTIONS = %w(index create new show update destroy) class Resource #:nodoc: - DEFAULT_ACTIONS = [:index, :create, :new, :show, :update, :destroy, :edit] - attr_reader :controller, :path, :options def initialize(entities, options = {}) @@ -880,7 +876,7 @@ module ActionDispatch end def default_actions - self.class::DEFAULT_ACTIONS + [:index, :create, :new, :show, :update, :destroy, :edit] end def actions @@ -934,16 +930,17 @@ module ActionDispatch end class SingletonResource < Resource #:nodoc: - DEFAULT_ACTIONS = [:show, :create, :update, :destroy, :new, :edit] - def initialize(entities, options) super - @as = nil @controller = (options[:controller] || plural).to_s @as = options[:as] end + def default_actions + [:show, :create, :update, :destroy, :new, :edit] + end + def plural @plural ||= name.to_s.pluralize end @@ -991,7 +988,7 @@ module ActionDispatch return self end - resource_scope(SingletonResource.new(resources.pop, options)) do + resource_scope(:resource, SingletonResource.new(resources.pop, options)) do yield if block_given? collection do @@ -1023,6 +1020,7 @@ module ActionDispatch # creates seven different routes in your application, all mapping to # the +Photos+ controller: # + # GET /photos # GET /photos/new # POST /photos # GET /photos/:id @@ -1038,6 +1036,7 @@ module ActionDispatch # # This generates the following comments routes: # + # GET /photos/:photo_id/comments # GET /photos/:photo_id/comments/new # POST /photos/:photo_id/comments # GET /photos/:photo_id/comments/:id @@ -1049,13 +1048,20 @@ module ActionDispatch # Takes same options as <tt>Base#match</tt> as well as: # # [:path_names] - # Allows you to change the paths of the seven default actions. - # Paths not specified are not changed. + # Allows you to change the segment component of the +edit+ and +new+ actions. + # Actions not specified are not changed. # # resources :posts, :path_names => { :new => "brand_new" } # # The above example will now change /posts/new to /posts/brand_new # + # [:path] + # Allows you to change the path prefix for the resource. + # + # resources :posts, :path => 'postings' + # + # The resource and all segments will now route to /postings instead of /posts + # # [:only] # Only generate routes for the given actions. # @@ -1120,7 +1126,7 @@ module ActionDispatch return self end - resource_scope(Resource.new(resources.pop, options)) do + resource_scope(:resources, Resource.new(resources.pop, options)) do yield if block_given? collection do @@ -1243,32 +1249,47 @@ module ActionDispatch parent_resource.instance_of?(Resource) && @scope[:shallow] end - def match(*args) - options = args.extract_options!.dup - options[:anchor] = true unless options.key?(:anchor) - - if args.length > 1 - args.each { |path| match(path, options.dup) } - return self + # match 'path' => 'controller#action' + # match 'path', to: 'controller#action' + # match 'path', 'otherpath', on: :member, via: :get + def match(path, *rest) + if rest.empty? && Hash === path + options = path + path, to = options.find { |name, value| name.is_a?(String) } + options[:to] = to + options.delete(path) + paths = [path] + else + options = rest.pop || {} + paths = [path] + rest end - on = options.delete(:on) - if VALID_ON_OPTIONS.include?(on) - args.push(options) - return send(on){ match(*args) } - elsif on + options[:anchor] = true unless options.key?(:anchor) + + if options[:on] && !VALID_ON_OPTIONS.include?(options[:on]) raise ArgumentError, "Unknown scope #{on.inspect} given to :on" end - if @scope[:scope_level] == :resources - args.push(options) - return nested { match(*args) } - elsif @scope[:scope_level] == :resource - args.push(options) - return member { match(*args) } + paths.each { |_path| decomposed_match(_path, options.dup) } + self + end + + def decomposed_match(path, options) # :nodoc: + if on = options.delete(:on) + send(on) { decomposed_match(path, options) } + else + case @scope[:scope_level] + when :resources + nested { decomposed_match(path, options) } + when :resource + member { decomposed_match(path, options) } + else + add_route(path, options) + end end + end - action = args.first + def add_route(action, options) # :nodoc: path = path_for_action(action, options.delete(:path)) if action.to_s =~ /^[\w\/]+$/ @@ -1277,13 +1298,15 @@ module ActionDispatch action = nil end - if options.key?(:as) && !options[:as] + if !options.fetch(:as, true) options.delete(:as) else options[:as] = name_for_action(options[:as], action) end - super(path, options) + mapping = Mapping.new(@set, @scope, path, options) + app, conditions, requirements, defaults, as, anchor = mapping.to_route + @set.add_route(app, conditions, requirements, defaults, as, anchor) end def root(options={}) @@ -1339,7 +1362,7 @@ module ActionDispatch end def scope_action_options? #:nodoc: - @scope[:options].is_a?(Hash) && (@scope[:options][:only] || @scope[:options][:except]) + @scope[:options] && (@scope[:options][:only] || @scope[:options][:except]) end def scope_action_options #:nodoc: @@ -1347,11 +1370,11 @@ module ActionDispatch end def resource_scope? #:nodoc: - @scope[:scope_level].in?([:resource, :resources]) + [:resource, :resources].include? @scope[:scope_level] end def resource_method_scope? #:nodoc: - @scope[:scope_level].in?([:collection, :member, :new]) + [:collection, :member, :new].include? @scope[:scope_level] end def with_exclusive_scope @@ -1376,8 +1399,8 @@ module ActionDispatch @scope[:scope_level_resource] = old_resource end - def resource_scope(resource) #:nodoc: - with_scope_level(resource.is_a?(SingletonResource) ? :resource : :resources, resource) do + def resource_scope(kind, resource) #:nodoc: + with_scope_level(kind, resource) do scope(parent_resource.resource_scope) do yield end @@ -1385,10 +1408,12 @@ module ActionDispatch end def nested_options #:nodoc: - {}.tap do |options| - options[:as] = parent_resource.member_name - options[:constraints] = { "#{parent_resource.singular}_id".to_sym => id_constraint } if id_constraint? - end + options = { :as => parent_resource.member_name } + options[:constraints] = { + :"#{parent_resource.singular}_id" => id_constraint + } if id_constraint? + + options end def id_constraint? #:nodoc: @@ -1419,8 +1444,7 @@ module ActionDispatch end def action_path(name, path = nil) #:nodoc: - # Ruby 1.8 can't transform empty strings to symbols - name = name.to_sym if name.is_a?(String) && !name.empty? + name = name.to_sym if name.is_a?(String) path || @scope[:path_names][name] || name.to_s end @@ -1459,22 +1483,17 @@ module ActionDispatch [name_prefix, member_name, prefix] end - candidate = name.select(&:present?).join("_").presence - candidate unless as.nil? && @set.routes.find { |r| r.name == candidate } - end - end - - module Shorthand #:nodoc: - def match(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) - else - super + if candidate = name.select(&:present?).join("_").presence + # If a name was not explicitly given, we check if it is valid + # and return nil in case it isn't. Otherwise, we pass the invalid name + # forward so the underlying router engine treats it and raises an exception. + if as.nil? + candidate unless @set.routes.find { |r| r.name == candidate } || candidate !~ /\A[_a-z]/i + else + candidate + end + end end - end end def initialize(set) #:nodoc: @@ -1487,7 +1506,6 @@ module ActionDispatch include Redirection include Scoping include Resources - include Shorthand end end end diff --git a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb index e989a38d8b..013cf93dbc 100644 --- a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb +++ b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb @@ -165,7 +165,7 @@ module ActionDispatch if parent.is_a?(Symbol) || parent.is_a?(String) parent else - ActiveModel::Naming.route_key(parent).singularize + ActiveModel::Naming.singular_route_key(parent) end end else @@ -176,9 +176,11 @@ module ActionDispatch if record.is_a?(Symbol) || record.is_a?(String) route << record elsif record - route << ActiveModel::Naming.route_key(record) - route = [route.join("_").singularize] if inflection == :singular - route << "index" if ActiveModel::Naming.uncountable?(record) && inflection == :plural + if inflection == :singular + route << ActiveModel::Naming.singular_route_key(record) + else + route << ActiveModel::Naming.route_key(record) + end else raise ArgumentError, "Nil location provided. Can't build URI." end diff --git a/actionpack/lib/action_dispatch/routing/redirection.rb b/actionpack/lib/action_dispatch/routing/redirection.rb index 804991ad5f..617b24b46a 100644 --- a/actionpack/lib/action_dispatch/routing/redirection.rb +++ b/actionpack/lib/action_dispatch/routing/redirection.rb @@ -2,6 +2,54 @@ require 'action_dispatch/http/request' module ActionDispatch module Routing + class Redirect # :nodoc: + attr_reader :status, :block + + def initialize(status, block) + @status = status + @block = block + end + + def call(env) + req = Request.new(env) + + uri = URI.parse(path(req.symbolized_path_parameters, req)) + uri.scheme ||= req.scheme + uri.host ||= req.host + uri.port ||= req.port unless req.standard_port? + + body = %(<html><body>You are being <a href="#{ERB::Util.h(uri.to_s)}">redirected</a>.</body></html>) + + headers = { + 'Location' => uri.to_s, + 'Content-Type' => 'text/html', + 'Content-Length' => body.length.to_s + } + + [ status, headers, [body] ] + end + + def path(params, request) + block.call params, request + end + end + + class OptionRedirect < Redirect # :nodoc: + alias :options :block + + def path(params, request) + url_options = { + :protocol => request.protocol, + :host => request.host, + :port => request.optional_port, + :path => request.path, + :params => request.query_parameters + }.merge options + + ActionDispatch::Http::URL.url_for url_options + end + end + module Redirection # Redirect any path to another path: @@ -40,71 +88,18 @@ module ActionDispatch options = args.last.is_a?(Hash) ? args.pop : {} status = options.delete(:status) || 301 - path = args.shift + return OptionRedirect.new(status, options) if options.any? - path_proc = if path.is_a?(String) - proc { |params| (params.empty? || !path.match(/%\{\w*\}/)) ? path : (path % params) } - elsif options.any? - options_proc(options) - elsif path.respond_to?(:call) - proc { |params, request| path.call(params, request) } - elsif block - block - else - raise ArgumentError, "redirection argument not supported" - end - - redirection_proc(status, path_proc) - end + path = args.shift - private - - def options_proc(options) - proc do |params, request| - path = if options[:path].nil? - request.path - elsif params.empty? || !options[:path].match(/%\{\w*\}/) - options.delete(:path) - else - (options.delete(:path) % params) - end - - default_options = { - :protocol => request.protocol, - :host => request.host, - :port => request.optional_port, - :path => path, - :params => request.query_parameters - } - - ActionDispatch::Http::URL.url_for(options.reverse_merge(default_options)) - end - end - - def redirection_proc(status, path_proc) - lambda do |env| - req = Request.new(env) - - params = [req.symbolized_path_parameters] - params << req if path_proc.arity > 1 - - uri = URI.parse(path_proc.call(*params)) - uri.scheme ||= req.scheme - uri.host ||= req.host - uri.port ||= req.port unless req.standard_port? - - body = %(<html><body>You are being <a href="#{ERB::Util.h(uri.to_s)}">redirected</a>.</body></html>) - - headers = { - 'Location' => uri.to_s, - 'Content-Type' => 'text/html', - 'Content-Length' => body.length.to_s - } - - [ status, headers, [body] ] - end - end + block = lambda { |params, request| + (params.empty? || !path.match(/%\{\w*\}/)) ? path : (path % params) + } if String === path + block = path if path.respond_to? :call + raise ArgumentError, "redirection argument not supported" unless block + Redirect.new status, block + end end end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index e7bc431783..6c189fdba6 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -31,13 +31,14 @@ module ActionDispatch end def prepare_params!(params) + normalize_controller!(params) merge_default_action!(params) split_glob_param!(params) if @glob_param end # If this is a default_controller (i.e. a controller specified by the user) # we should raise an error in case it's not found, because it usually means - # an user error. However, if the controller was retrieved through a dynamic + # a user error. However, if the controller was retrieved through a dynamic # segment, as in :controller(/:action), we should simply return nil and # delegate the control back to Rack cascade. Besides, if this is not a default # controller, it means we should respect the @scope[:module] parameter. @@ -66,6 +67,10 @@ module ActionDispatch controller.action(action).call(env) end + def normalize_controller!(params) + params[:controller] = params[:controller].underscore if params.key?(:controller) + end + def merge_default_action!(params) params[:action] ||= 'index' end @@ -83,7 +88,9 @@ module ActionDispatch attr_reader :routes, :helpers, :module def initialize - clear! + @routes = {} + @helpers = [] + @module = Module.new end def helper_names @@ -91,12 +98,8 @@ module ActionDispatch end def clear! - @routes = {} - @helpers = [] - - @module ||= Module.new do - instance_methods.each { |selector| remove_method(selector) } - end + @routes.clear + @helpers.clear end def add(name, route) @@ -125,21 +128,6 @@ module ActionDispatch routes.length end - def reset! - old_routes = routes.dup - clear! - old_routes.each do |name, route| - add(name, route) - end - end - - def install(destinations = [ActionController::Base, ActionView::Base], regenerate = false) - reset! if regenerate - Array(destinations).each do |dest| - dest.__send__(:include, @module) - end - end - private def url_helper_name(name, kind = :url) :"#{name}_#{kind}" @@ -160,22 +148,23 @@ module ActionDispatch def define_hash_access(route, name, kind, options) selector = hash_access_name(name, kind) - # We use module_eval to avoid leaks - @module.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1 - remove_possible_method :#{selector} - def #{selector}(*args) - options = args.extract_options! - result = #{options.inspect} + @module.module_eval do + remove_possible_method selector + + define_method(selector) do |*args| + inner_options = args.extract_options! + result = options.dup if args.any? result[:_positional_args] = args - result[:_positional_keys] = #{route.segment_keys.inspect} + result[:_positional_keys] = route.segment_keys end - result.merge(options) + result.merge(inner_options) end - protected :#{selector} - END_EVAL + + protected selector + end helpers << selector end @@ -287,14 +276,15 @@ module ActionDispatch @prepend.each { |blk| eval_block(blk) } end - def install_helpers(destinations = [ActionController::Base, ActionView::Base], regenerate_code = false) - Array(destinations).each { |d| d.module_eval { include Helpers } } - named_routes.install(destinations, regenerate_code) - end - - module MountedHelpers + module MountedHelpers #:nodoc: + extend ActiveSupport::Concern + include UrlFor end + # Contains all the mounted helpers accross different + # engines and the `main_app` helper for the application. + # You can include this in your classes if you want to + # access routes for other engines. def mounted_helpers MountedHelpers end @@ -305,7 +295,7 @@ module ActionDispatch routes = self MountedHelpers.class_eval do define_method "_#{name}" do - RoutesProxy.new(routes, self._routes_context) + RoutesProxy.new(routes, _routes_context) end end @@ -320,28 +310,36 @@ module ActionDispatch @url_helpers ||= begin routes = self - helpers = Module.new do + Module.new do extend ActiveSupport::Concern include UrlFor + # Define url_for in the singleton level so one can do: + # Rails.application.routes.url_helpers.url_for(args) @_routes = routes class << self delegate :url_for, :to => '@_routes' end + + # Make named_routes available in the module singleton + # as well, so one can do: + # Rails.application.routes.url_helpers.posts_path extend routes.named_routes.module - # ROUTES TODO: install_helpers isn't great... can we make a module with the stuff that - # we can include? - # Yes plz - JP + # Any class that includes this module will get all + # named routes... + include routes.named_routes.module + + # plus a singleton class method called _routes ... included do - routes.install_helpers(self) singleton_class.send(:redefine_method, :_routes) { routes } end + # And an instance method _routes. Note that + # UrlFor (included in this module) add extra + # conveniences for working with @_routes. define_method(:_routes) { @_routes || routes } end - - helpers end end @@ -356,7 +354,7 @@ module ActionDispatch 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 + named_routes[name] = route if name && !named_routes[name] route end @@ -367,7 +365,26 @@ module ActionDispatch SEPARATORS, anchor) - Journey::Path::Pattern.new(strexp) + pattern = Journey::Path::Pattern.new(strexp) + + builder = Journey::GTG::Builder.new pattern.spec + + # Get all the symbol nodes followed by literals that are not the + # dummy node. + symbols = pattern.spec.grep(Journey::Nodes::Symbol).find_all { |n| + builder.followpos(n).first.literal? + } + + # Get all the symbol nodes preceded by literals. + symbols.concat pattern.spec.find_all(&:literal?).map { |n| + builder.followpos(n).first + }.find_all(&:symbol?) + + symbols.each { |x| + x.regexp = /(?:#{Regexp.union(x.regexp, '-')})+/ + } + + pattern end private :build_path @@ -470,7 +487,7 @@ module ActionDispatch # if the current controller is "foo/bar/baz" and :controller => "baz/bat" # is specified, the controller becomes "foo/baz/bat" def use_relative_controller! - if !named_route && different_controller? + if !named_route && different_controller? && !controller.start_with?("/") old_parts = current_controller.split('/') size = controller.count("/") + 1 parts = old_parts[0...-size] << controller @@ -540,7 +557,6 @@ module ActionDispatch end def url_for(options) - finalize! options = (options || {}).reverse_merge!(default_url_options) handle_positional_args(options) @@ -556,8 +572,9 @@ module ActionDispatch path_addition, params = generate(path_options, path_segments || {}) path << path_addition + params.merge!(options[:params] || {}) - ActionDispatch::Http::URL.url_for(options.merge({ + ActionDispatch::Http::URL.url_for(options.merge!({ :path => path, :params => params, :user => user, @@ -566,7 +583,6 @@ module ActionDispatch end def call(env) - finalize! @router.call(env) end @@ -584,11 +600,12 @@ module ActionDispatch @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? + value = value.dup.force_encoding(Encoding::BINARY) params[key] = URI.parser.unescape(value) end end - + old_params = env[::ActionDispatch::Routing::RouteSet::PARAMETERS_KEY] + env[::ActionDispatch::Routing::RouteSet::PARAMETERS_KEY] = (old_params || {}).merge(params) dispatcher = route.app while dispatcher.is_a?(Mapper::Constraints) && dispatcher.matches?(env) do dispatcher = dispatcher.app diff --git a/actionpack/lib/action_dispatch/routing/routes_proxy.rb b/actionpack/lib/action_dispatch/routing/routes_proxy.rb index f7d5f6397d..73af5920ed 100644 --- a/actionpack/lib/action_dispatch/routing/routes_proxy.rb +++ b/actionpack/lib/action_dispatch/routing/routes_proxy.rb @@ -16,6 +16,10 @@ module ActionDispatch end end + def respond_to?(method, include_private = false) + super || routes.url_helpers.respond_to?(method) + end + def method_missing(method, *args) if routes.url_helpers.respond_to?(method) self.class.class_eval <<-RUBY, __FILE__, __LINE__ + 1 diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb index 8fc8dc191b..ee6616c5d3 100644 --- a/actionpack/lib/action_dispatch/routing/url_for.rb +++ b/actionpack/lib/action_dispatch/routing/url_for.rb @@ -8,7 +8,8 @@ module ActionDispatch # # <b>Tip:</b> If you need to generate URLs from your models or some other place, # then ActionController::UrlFor is what you're looking for. Read on for - # an introduction. + # an introduction. In general, this module should not be included on its own, + # as it is usually included by url_helpers (as in Rails.application.routes.url_helpers). # # == URL generation from parameters # @@ -42,7 +43,7 @@ module ActionDispatch # url_for(:controller => 'users', # :action => 'new', # :message => 'Welcome!', - # :host => 'www.example.com') # Changed this. + # :host => 'www.example.com') # # => "http://www.example.com/users/new?message=Welcome%21" # # By default, all controllers and views have access to a special version of url_for, @@ -52,7 +53,7 @@ module ActionDispatch # # For convenience reasons, mailers provide a shortcut for ActionController::UrlFor#url_for. # So within mailers, you only have to type 'url_for' instead of 'ActionController::UrlFor#url_for' - # in full. However, mailers don't have hostname information, and what's why you'll still + # in full. However, mailers don't have hostname information, and that's why you'll still # have to specify the <tt>:host</tt> argument when generating URLs in mailers. # # @@ -84,14 +85,12 @@ module ActionDispatch include PolymorphicRoutes included do - # TODO: with_routing extends @controller with url_helpers, trickling down to including this module which overrides its default_url_options unless method_defined?(:default_url_options) # Including in a class uses an inheritable hash. Modules get a plain hash. if respond_to?(:class_attribute) class_attribute :default_url_options else - mattr_accessor :default_url_options - remove_method :default_url_options + mattr_writer :default_url_options end self.default_url_options = {} @@ -145,23 +144,24 @@ module ActionDispatch when String options when nil, Hash - _routes.url_for((options || {}).reverse_merge(url_options).symbolize_keys) + _routes.url_for((options || {}).symbolize_keys.reverse_merge!(url_options)) else polymorphic_url(options) end end protected - def _with_routes(routes) - old_routes, @_routes = @_routes, routes - yield - ensure - @_routes = old_routes - end - def _routes_context - self - end + def _with_routes(routes) + old_routes, @_routes = @_routes, routes + yield + ensure + @_routes = old_routes + end + + def _routes_context + self + end end end end diff --git a/actionpack/lib/action_dispatch/testing/assertions/dom.rb b/actionpack/lib/action_dispatch/testing/assertions/dom.rb index 47c84742aa..edea6dab39 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/dom.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/dom.rb @@ -13,9 +13,7 @@ module ActionDispatch def assert_dom_equal(expected, actual, message = "") expected_dom = HTML::Document.new(expected).root actual_dom = HTML::Document.new(actual).root - full_message = build_message(message, "<?> expected to be == to\n<?>.", expected_dom.to_s, actual_dom.to_s) - - assert_block(full_message) { expected_dom == actual_dom } + assert_equal expected_dom, actual_dom end # The negated form of +assert_dom_equivalent+. @@ -28,9 +26,7 @@ module ActionDispatch def assert_dom_not_equal(expected, actual, message = "") expected_dom = HTML::Document.new(expected).root actual_dom = HTML::Document.new(actual).root - full_message = build_message(message, "<?> expected to be != to\n<?>.", expected_dom.to_s, actual_dom.to_s) - - assert_block(full_message) { expected_dom != actual_dom } + refute_equal expected_dom, actual_dom end end end diff --git a/actionpack/lib/action_dispatch/testing/assertions/response.rb b/actionpack/lib/action_dispatch/testing/assertions/response.rb index 7381617dd7..094cfbfc76 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/response.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/response.rb @@ -26,16 +26,17 @@ module ActionDispatch # assert_response 401 # def assert_response(type, message = nil) - validate_request! + message ||= "Expected response to be a <#{type}>, but was <#{@response.response_code}>" - if type.in?([:success, :missing, :redirect, :error]) && @response.send("#{type}?") - assert_block("") { true } # to count the assertion - elsif type.is_a?(Fixnum) && @response.response_code == type - assert_block("") { true } # to count the assertion - elsif type.is_a?(Symbol) && @response.response_code == Rack::Utils::SYMBOL_TO_STATUS_CODE[type] - assert_block("") { true } # to count the assertion + if Symbol === type + if [:success, :missing, :redirect, :error].include?(type) + assert @response.send("#{type}?"), message + else + code = Rack::Utils::SYMBOL_TO_STATUS_CODE[type] + assert_equal @response.response_code, code, message + end else - flunk(build_message(message, "Expected response to be a <?>, but was <?>", type, @response.response_code)) + assert_equal type, @response.response_code, message end end @@ -61,9 +62,8 @@ module ActionDispatch redirect_is = normalize_argument_to_redirection(@response.location) redirect_expected = normalize_argument_to_redirection(options) - if redirect_is != redirect_expected - flunk "Expected response to be a redirect to <#{redirect_expected}> but was a redirect to <#{redirect_is}>" - end + message ||= "Expected response to be a redirect to <#{redirect_expected}> but was a redirect to <#{redirect_is}>" + assert_equal redirect_expected, redirect_is, message end private @@ -85,12 +85,6 @@ module ActionDispatch @controller.url_for(fragment) end.gsub(/[\r\n]/, '') end - - def validate_request! - unless @request.is_a?(ActionDispatch::Request) - raise ArgumentError, "@request must be an ActionDispatch::Request" - end - end end end end diff --git a/actionpack/lib/action_dispatch/testing/assertions/routing.rb b/actionpack/lib/action_dispatch/testing/assertions/routing.rb index b10aab9029..1552676fbb 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/routing.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/routing.rb @@ -45,9 +45,11 @@ module ActionDispatch extras.each_key { |key| expected_options.delete key } unless extras.nil? expected_options.stringify_keys! - msg = build_message(message, "The recognized options <?> did not match <?>, difference: <?>", + + # FIXME: minitest does object diffs, do we need to have our own? + message ||= sprintf("The recognized options <%s> did not match <%s>, difference: <%s>", request.path_parameters, expected_options, expected_options.diff(request.path_parameters)) - assert_equal(expected_options, request.path_parameters, msg) + assert_equal(expected_options, request.path_parameters, message) end # Asserts that the provided options can be used to generate the provided path. This is the inverse of +assert_recognizes+. @@ -84,10 +86,10 @@ module ActionDispatch generated_path, extra_keys = @routes.generate_extras(options, defaults) found_extras = options.reject {|k, v| ! extra_keys.include? k} - msg = build_message(message, "found extras <?>, not <?>", found_extras, extras) + msg = message || sprintf("found extras <%s>, not <%s>", found_extras, extras) assert_equal(extras, found_extras, msg) - msg = build_message(message, "The generated path <?> did not match <?>", generated_path, + msg = message || sprintf("The generated path <%s> did not match <%s>", generated_path, expected_path) assert_equal(expected_path, generated_path, msg) end diff --git a/actionpack/lib/action_dispatch/testing/assertions/selector.rb b/actionpack/lib/action_dispatch/testing/assertions/selector.rb index b4555f4f59..8eed85bce2 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/selector.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/selector.rb @@ -270,7 +270,7 @@ module ActionDispatch end text.strip! unless NO_STRIP.include?(match.name) unless match_with.is_a?(Regexp) ? (text =~ match_with) : (text == match_with.to_s) - content_mismatch ||= build_message(message, "<?> expected but was\n<?>.", match_with, text) + content_mismatch ||= sprintf("<%s> expected but was\n<%s>.", match_with, text) true end end @@ -279,7 +279,7 @@ module ActionDispatch html = match.children.map(&:to_s).join html.strip! unless NO_STRIP.include?(match.name) unless match_with.is_a?(Regexp) ? (html =~ match_with) : (html == match_with.to_s) - content_mismatch ||= build_message(message, "<?> expected but was\n<?>.", match_with, html) + content_mismatch ||= sprintf("<%s> expected but was\n<%s>.", match_with, html) true end end @@ -289,12 +289,15 @@ module ActionDispatch message ||= content_mismatch if matches.empty? # Test minimum/maximum occurrence. min, max, count = equals[:minimum], equals[:maximum], equals[:count] + + # FIXME: minitest provides messaging when we use assert_operator, + # so is this custom message really needed? message = message || %(Expected #{count_description(min, max, count)} matching "#{selector.to_s}", found #{matches.size}.) if count - assert matches.size == count, message + assert_equal matches.size, count, message else - assert matches.size >= min, message if min - assert matches.size <= max, message if max + assert_operator matches.size, :>=, min, message if min + assert_operator matches.size, :<=, max, message if max end # If a block is given call that block. Set @selected to allow @@ -337,8 +340,8 @@ module ActionDispatch # element +encoded+. It then calls the block with all un-encoded elements. # # ==== Examples - # # Selects all bold tags from within the title of an ATOM feed's entries (perhaps to nab a section name prefix) - # assert_select_feed :atom, 1.0 do + # # Selects all bold tags from within the title of an Atom feed's entries (perhaps to nab a section name prefix) + # assert_select "feed[xmlns='http://www.w3.org/2005/Atom']" do # # Select each entry item and then the title item # assert_select "entry>title" do # # Run assertions on the encoded title elements @@ -350,7 +353,7 @@ module ActionDispatch # # # # Selects all paragraph tags from within the description of an RSS feed - # assert_select_feed :rss, 2.0 do + # assert_select "rss[version=2.0]" do # # Select description element of each feed item. # assert_select "channel>item>description" do # # Run assertions on the encoded elements. @@ -414,8 +417,8 @@ module ActionDispatch deliveries = ActionMailer::Base.deliveries assert !deliveries.empty?, "No e-mail in delivery list" - for delivery in deliveries - for part in (delivery.parts.empty? ? [delivery] : delivery.parts) + deliveries.each do |delivery| + (delivery.parts.empty? ? [delivery] : delivery.parts).each do |part| if part["Content-Type"].to_s =~ /^text\/html\W/ root = HTML::Document.new(part.body.to_s).root assert_select root, ":root", &block diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index 0f1bb9f260..08b7ff49c2 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -4,7 +4,6 @@ require 'active_support/core_ext/kernel/singleton_class' require 'active_support/core_ext/object/inclusion' require 'active_support/core_ext/object/try' require 'rack/test' -require 'test/unit/assertions' module ActionDispatch module Integration #:nodoc: @@ -127,7 +126,7 @@ module ActionDispatch class Session DEFAULT_HOST = "www.example.com" - include Test::Unit::Assertions + include MiniTest::Assertions include TestProcess, RequestHelpers, Assertions %w( status status_message headers body redirect? ).each do |method| @@ -463,9 +462,12 @@ module ActionDispatch @@app = nil def self.app - # DEPRECATE Rails application fallback - # This should be set by the initializer - @@app || (defined?(Rails.application) && Rails.application) || nil + if !@@app && !ActionDispatch.test_app + ActiveSupport::Deprecation.warn "Rails application fallback is deprecated " \ + "and no longer works, please set ActionDispatch.test_app", caller + end + + @@app || ActionDispatch.test_app end def self.app=(app) diff --git a/actionpack/lib/action_pack.rb b/actionpack/lib/action_pack.rb index 914b13dbfb..39c7faf740 100644 --- a/actionpack/lib/action_pack.rb +++ b/actionpack/lib/action_pack.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2011 David Heinemeier Hansson +# Copyright (c) 2004-2012 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/actionpack/lib/action_pack/version.rb b/actionpack/lib/action_pack/version.rb index add6b56425..94cbc8e478 100644 --- a/actionpack/lib/action_pack/version.rb +++ b/actionpack/lib/action_pack/version.rb @@ -1,7 +1,7 @@ module ActionPack module VERSION #:nodoc: - MAJOR = 3 - MINOR = 2 + MAJOR = 4 + MINOR = 0 TINY = 0 PRE = "beta" diff --git a/actionpack/lib/action_view.rb b/actionpack/lib/action_view.rb index d7229419a9..349a3fcc6e 100644 --- a/actionpack/lib/action_view.rb +++ b/actionpack/lib/action_view.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2011 David Heinemeier Hansson +# Copyright (c) 2004-2012 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -33,6 +33,7 @@ module ActionView autoload :AssetPaths autoload :Base autoload :Context + autoload :CompiledTemplates, "action_view/context" autoload :Helpers autoload :LookupContext autoload :PathSet diff --git a/actionpack/lib/action_view/asset_paths.rb b/actionpack/lib/action_view/asset_paths.rb index 1d16e34df6..2a28e780bf 100644 --- a/actionpack/lib/action_view/asset_paths.rb +++ b/actionpack/lib/action_view/asset_paths.rb @@ -4,6 +4,8 @@ require 'action_controller/metal/exceptions' module ActionView class AssetPaths #:nodoc: + URI_REGEXP = %r{^[-a-z]+://|^cid:|^//} + attr_reader :config, :controller def initialize(config, controller = nil) @@ -37,7 +39,7 @@ module ActionView end def is_uri?(path) - path =~ %r{^[-a-z]+://|^cid:|^//} + path =~ URI_REGEXP end private @@ -103,8 +105,8 @@ module ActionView 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.") + if (arity > 1 || arity < -2) && !has_request? + invalid_asset_host!("Remove the second argument to your asset_host Proc if you do not need the request, or make it optional.") end args << current_request if (arity > 1 || arity < 0) && has_request? host.call(*args) diff --git a/actionpack/lib/action_view/base.rb b/actionpack/lib/action_view/base.rb index 36c49d9c91..23329d7f35 100644 --- a/actionpack/lib/action_view/base.rb +++ b/actionpack/lib/action_view/base.rb @@ -1,16 +1,14 @@ require 'active_support/core_ext/module/attr_internal' require 'active_support/core_ext/module/delegation' 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 # - # Action View templates can be written in several ways. If the template file has a <tt>.erb</tt> (or <tt>.rhtml</tt>) extension then it uses a mixture of ERb - # (included in Ruby) and HTML. If the template file has a <tt>.builder</tt> (or <tt>.rxml</tt>) extension then Jim Weirich's Builder::XmlMarkup library is used. + # Action View templates can be written in several ways. If the template file has a <tt>.erb</tt> extension then it uses a mixture of ERb + # (included in Ruby) and HTML. If the template file has a <tt>.builder</tt> extension then Jim Weirich's Builder::XmlMarkup library is used. # # == ERB # @@ -94,10 +92,10 @@ module ActionView #:nodoc: # # Any method with a block will be treated as an XML markup tag with nested markup in the block. For example, the following: # - # xml.div { + # xml.div do # xml.h1(@person.name) # xml.p(@person.bio) - # } + # end # # would produce something like: # @@ -145,10 +143,10 @@ module ActionView #:nodoc: class_attribute :helpers class_attribute :_routes + class_attribute :logger class << self delegate :erb_trim_mode=, :to => 'ActionView::Template::Handlers::ERB' - delegate :logger, :to => 'ActionController::Base', :allow_nil => true def cache_template_loading ActionView::Resolver.caching? @@ -158,12 +156,6 @@ module ActionView #:nodoc: ActionView::Resolver.caching = value end - def process_view_paths(value) - value.is_a?(PathSet) ? - value.dup : ActionView::PathSet.new(Array.wrap(value)) - end - deprecate :process_view_paths - def xss_safe? #:nodoc: true end diff --git a/actionpack/lib/action_view/buffers.rb b/actionpack/lib/action_view/buffers.rb index be7f65c2ce..2372d3c433 100644 --- a/actionpack/lib/action_view/buffers.rb +++ b/actionpack/lib/action_view/buffers.rb @@ -4,7 +4,7 @@ module ActionView class OutputBuffer < ActiveSupport::SafeBuffer #:nodoc: def initialize(*) super - encode! if encoding_aware? + encode! end def <<(value) diff --git a/actionpack/lib/action_view/flows.rb b/actionpack/lib/action_view/flows.rb index a8f740713f..c0e458cd41 100644 --- a/actionpack/lib/action_view/flows.rb +++ b/actionpack/lib/action_view/flows.rb @@ -22,11 +22,8 @@ module ActionView def append(key, value) @content[key] << value end + alias_method :append!, :append - # Called by provide - def append!(key, value) - @content[key] << value - end end class StreamingFlow < OutputFlow #:nodoc: diff --git a/actionpack/lib/action_view/helpers.rb b/actionpack/lib/action_view/helpers.rb index 262e0f1010..f2a3a494bc 100644 --- a/actionpack/lib/action_view/helpers.rb +++ b/actionpack/lib/action_view/helpers.rb @@ -1,5 +1,3 @@ -require 'active_support/benchmarkable' - module ActionView #:nodoc: module Helpers #:nodoc: extend ActiveSupport::Autoload @@ -7,6 +5,7 @@ module ActionView #:nodoc: autoload :ActiveModelHelper autoload :AssetTagHelper autoload :AtomFeedHelper + autoload :BenchmarkHelper autoload :CacheHelper autoload :CaptureHelper autoload :ControllerHelper @@ -33,10 +32,10 @@ module ActionView #:nodoc: extend SanitizeHelper::ClassMethods end - include ActiveSupport::Benchmarkable include ActiveModelHelper include AssetTagHelper include AtomFeedHelper + include BenchmarkHelper include CacheHelper include CaptureHelper include ControllerHelper diff --git a/actionpack/lib/action_view/helpers/active_model_helper.rb b/actionpack/lib/action_view/helpers/active_model_helper.rb index 96c3eec337..e27111012d 100644 --- a/actionpack/lib/action_view/helpers/active_model_helper.rb +++ b/actionpack/lib/action_view/helpers/active_model_helper.rb @@ -1,4 +1,3 @@ -require 'action_view/helpers/form_helper' require 'active_support/core_ext/class/attribute_accessors' require 'active_support/core_ext/enumerable' require 'active_support/core_ext/object/blank' @@ -17,8 +16,8 @@ module ActionView end end - %w(content_tag to_date_select_tag to_datetime_select_tag to_time_select_tag).each do |meth| - module_eval "def #{meth}(*) error_wrapping(super) end", __FILE__, __LINE__ + def content_tag(*) + error_wrapping(super) end def tag(type, options, *) @@ -40,16 +39,12 @@ module ActionView private def object_has_errors? - object.respond_to?(:errors) && object.errors.respond_to?(:full_messages) && error_message.any? + object.respond_to?(:errors) && object.errors.respond_to?(:[]) && error_message.present? end def tag_generate_errors?(options) options['type'] != 'hidden' end end - - class InstanceTag - include ActiveModelInstanceTag - end end end diff --git a/actionpack/lib/action_view/helpers/asset_paths.rb b/actionpack/lib/action_view/helpers/asset_paths.rb deleted file mode 100644 index fae2e4fc1c..0000000000 --- a/actionpack/lib/action_view/helpers/asset_paths.rb +++ /dev/null @@ -1,7 +0,0 @@ -ActiveSupport::Deprecation.warn "ActionView::Helpers::AssetPaths is deprecated. Please use ActionView::AssetPaths instead." - -module ActionView - module Helpers - 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 7d01e5ddb8..662adbe183 100644 --- a/actionpack/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/asset_tag_helper.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/array/extract_options' +require 'active_support/core_ext/hash/keys' 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' @@ -196,7 +198,7 @@ module ActionView include JavascriptTagHelpers include StylesheetTagHelpers # Returns a link tag that browsers and news readers can use to auto-detect - # an RSS or ATOM feed. The +type+ can either be <tt>:rss</tt> (default) or + # an RSS or Atom feed. The +type+ can either be <tt>:rss</tt> (default) or # <tt>:atom</tt>. Control the link options in url_for format using the # +url_options+. You can modify the LINK tag itself in +tag_options+. # @@ -228,23 +230,19 @@ module ActionView ) end - # Web browsers cache favicons. If you just throw a <tt>favicon.ico</tt> into the document - # root of your application and it changes later, clients that have it in their cache - # won't see the update. Using this helper prevents that because it appends an asset ID: - # # <%= favicon_link_tag %> # # generates # - # <link href="/favicon.ico?4649789979" rel="shortcut icon" type="image/vnd.microsoft.icon" /> + # <link href="/assets/favicon.ico" rel="shortcut icon" type="image/vnd.microsoft.icon" /> # # You may specify a different file in the first argument: # - # <%= favicon_link_tag 'favicon.ico' %> + # <%= favicon_link_tag '/myicon.ico' %> # # That's passed to +path_to_image+ as is, so it gives # - # <link href="/images/favicon.ico?4649789979" rel="shortcut icon" type="image/vnd.microsoft.icon" /> + # <link href="/myicon.ico" rel="shortcut icon" type="image/vnd.microsoft.icon" /> # # The helper accepts an additional options hash where you can override "rel" and "type". # @@ -254,7 +252,7 @@ module ActionView # # <%= favicon_link_tag 'mb-icon.png', :rel => 'apple-touch-icon', :type => 'image/png' %> # - def favicon_link_tag(source='/favicon.ico', options={}) + def favicon_link_tag(source='favicon.ico', options={}) tag('link', { :rel => 'shortcut icon', :type => 'image/vnd.microsoft.icon', @@ -280,6 +278,13 @@ module ActionView end alias_method :path_to_image, :image_path # aliased to avoid conflicts with an image_path named route + # Computes the full URL to an image asset in the public images directory. + # This will use +image_path+ internally, so most of their behaviors will be the same. + def image_url(source) + URI.join(current_host, path_to_image(source)).to_s + end + alias_method :url_to_image, :image_url # aliased to avoid conflicts with an image_url named route + # Computes the path to a video asset in the public videos directory. # Full paths from the document root will be passed through. # Used internally by +video_tag+ to build the video path. @@ -295,6 +300,13 @@ module ActionView end alias_method :path_to_video, :video_path # aliased to avoid conflicts with a video_path named route + # Computes the full URL to a video asset in the public videos directory. + # This will use +video_path+ internally, so most of their behaviors will be the same. + def video_url(source) + URI.join(current_host, path_to_video(source)).to_s + end + alias_method :url_to_video, :video_url # aliased to avoid conflicts with an video_url named route + # Computes the path to an audio asset in the public audios directory. # Full paths from the document root will be passed through. # Used internally by +audio_tag+ to build the audio path. @@ -310,6 +322,34 @@ module ActionView end alias_method :path_to_audio, :audio_path # aliased to avoid conflicts with an audio_path named route + # Computes the full URL to a audio asset in the public audios directory. + # This will use +audio_path+ internally, so most of their behaviors will be the same. + def audio_url(source) + URI.join(current_host, path_to_audio(source)).to_s + end + alias_method :url_to_audio, :audio_url # aliased to avoid conflicts with an audio_url named route + + # Computes the path to a font asset in the public fonts directory. + # Full paths from the document root will be passed through. + # + # ==== Examples + # font_path("font") # => /fonts/font + # font_path("font.ttf") # => /fonts/font.ttf + # font_path("dir/font.ttf") # => /fonts/dir/font.ttf + # font_path("/dir/font.ttf") # => /dir/font.ttf + # font_path("http://www.example.com/dir/font.ttf") # => http://www.example.com/dir/font.ttf + def font_path(source) + asset_paths.compute_public_path(source, 'fonts') + end + alias_method :path_to_font, :font_path # aliased to avoid conflicts with an font_path named route + + # Computes the full URL to a font asset in the public fonts directory. + # This will use +font_path+ internally, so most of their behaviors will be the same. + def font_url(source) + URI.join(current_host, path_to_font(source)).to_s + end + alias_method :url_to_font, :font_url # aliased to avoid conflicts with an font_url named route + # Returns an html image tag for the +source+. The +source+ can be a full # path or a file that exists in your public images directory. # @@ -343,8 +383,8 @@ module ActionView # <img src="/images/mouse.png" onmouseover="this.src='/images/mouse_over.png'" onmouseout="this.src='/images/mouse.png'" alt="Mouse" /> # image_tag("mouse.png", :mouseover => image_path("mouse_over.png")) # => # <img src="/images/mouse.png" onmouseover="this.src='/images/mouse_over.png'" onmouseout="this.src='/images/mouse.png'" alt="Mouse" /> - def image_tag(source, options = {}) - options.symbolize_keys! + def image_tag(source, options={}) + options = options.dup.symbolize_keys! src = options[:src] = path_to_image(source) @@ -397,26 +437,19 @@ module ActionView # <video src="/trailers/hd.avi" width="16" height="16" /> # video_tag("/trailers/hd.avi", :height => '32', :width => '32') # => # <video height="32" src="/trailers/hd.avi" width="32" /> + # video_tag("trailer.ogg", "trailer.flv") # => + # <video><source src="/videos/trailer.ogg" /><source src="/videos/trailer.flv" /></video> # video_tag(["trailer.ogg", "trailer.flv"]) # => - # <video><source src="trailer.ogg" /><source src="trailer.ogg" /><source src="trailer.flv" /></video> + # <video><source src="/videos/trailer.ogg" /><source src="/videos/trailer.flv" /></video> # video_tag(["trailer.ogg", "trailer.flv"] :size => "160x120") # => - # <video height="120" width="160"><source src="trailer.ogg" /><source src="trailer.flv" /></video> - def video_tag(sources, options = {}) - options.symbolize_keys! - - options[:poster] = path_to_image(options[:poster]) if options[:poster] + # <video height="120" width="160"><source src="/videos/trailer.ogg" /><source src="/videos/trailer.flv" /></video> + def video_tag(*sources) + multiple_sources_tag('video', sources) do |options| + options[:poster] = path_to_image(options[:poster]) if options[:poster] - if size = options.delete(:size) - options[:width], options[:height] = size.split("x") if size =~ %r{^\d+x\d+$} - end - - if sources.is_a?(Array) - content_tag("video", options) do - sources.map { |source| tag("source", :src => source) }.join.html_safe + if size = options.delete(:size) + options[:width], options[:height] = size.split("x") if size =~ %r{^\d+x\d+$} end - else - options[:src] = path_to_video(sources) - tag("video", options) end end @@ -431,10 +464,10 @@ module ActionView # <audio src="/audios/sound.wav" /> # audio_tag("sound.wav", :autoplay => true, :controls => true) # => # <audio autoplay="autoplay" controls="controls" src="/audios/sound.wav" /> - def audio_tag(source, options = {}) - options.symbolize_keys! - options[:src] = path_to_audio(source) - tag("audio", options) + # audio_tag("sound.wav", "sound.mid") # => + # <audio><source src="/audios/sound.wav" /><source src="/audios/sound.mid" /></audio> + def audio_tag(*sources) + multiple_sources_tag('audio', sources) end private @@ -442,6 +475,26 @@ module ActionView def asset_paths @asset_paths ||= AssetTagHelper::AssetPaths.new(config, controller) end + + def multiple_sources_tag(type, sources) + options = sources.extract_options!.dup.symbolize_keys! + sources.flatten! + + yield options if block_given? + + if sources.size > 1 + content_tag(type, options) do + safe_join sources.map { |source| tag("source", :src => send("path_to_#{type}", source)) } + end + else + options[:src] = send("path_to_#{type}", sources.first) + content_tag(type, nil, options) + end + end + + def current_host + url_for(:only_path => false) + end end end 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 09700bd0c5..c67f81dcf4 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 @@ -87,6 +87,13 @@ module ActionView end alias_method :path_to_javascript, :javascript_path # aliased to avoid conflicts with a javascript_path named route + # Computes the full URL to a javascript asset in the public javascripts directory. + # This will use +javascript_path+ internally, so most of their behaviors will be the same. + def javascript_url(source) + URI.join(current_host, path_to_javascript(source)).to_s + end + alias_method :url_to_javascript, :javascript_url # aliased to avoid conflicts with a javascript_url named route + # Returns an HTML script tag for each of the +sources+ provided. # # Sources may be paths to JavaScript files. Relative paths are assumed to be relative @@ -119,10 +126,10 @@ module ActionView # # <script type="text/javascript" src="/elsewhere/cools.js?1423139606"></script> # # javascript_include_tag "http://www.example.com/xmlhr" - # # => <script type="text/javascript" src="http://www.example.com/xmlhr.js?1284139606"></script> + # # => <script type="text/javascript" src="http://www.example.com/xmlhr"></script> # # javascript_include_tag "http://www.example.com/xmlhr.js" - # # => <script type="text/javascript" src="http://www.example.com/xmlhr.js?1284139606"></script> + # # => <script type="text/javascript" src="http://www.example.com/xmlhr.js"></script> # # javascript_include_tag :defaults # # => <script type="text/javascript" src="/javascripts/jquery.js?1284139606"></script> 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 343153c8c5..2584b67548 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 @@ -65,9 +65,19 @@ module ActionView end alias_method :path_to_stylesheet, :stylesheet_path # aliased to avoid conflicts with a stylesheet_path named route + # Computes the full URL to a stylesheet asset in the public stylesheets directory. + # This will use +stylesheet_path+ internally, so most of their behaviors will be the same. + def stylesheet_url(source) + URI.join(current_host, path_to_stylesheet(source)).to_s + end + alias_method :url_to_stylesheet, :stylesheet_url # aliased to avoid conflicts with a stylesheet_url named route + # Returns a stylesheet link tag for the sources specified as arguments. If # you don't specify an extension, <tt>.css</tt> will be appended automatically. # You can modify the link attributes by passing a hash as the last argument. + # For historical reasons, the 'media' attribute will always be present and defaults + # to "screen", so you must explicitely set it to "all" for the stylesheet(s) to + # apply to all media types. # # ==== Examples # stylesheet_link_tag "style" # => diff --git a/actionpack/lib/action_view/helpers/atom_feed_helper.rb b/actionpack/lib/action_view/helpers/atom_feed_helper.rb index 39c37b25dc..73824dc1f8 100644 --- a/actionpack/lib/action_view/helpers/atom_feed_helper.rb +++ b/actionpack/lib/action_view/helpers/atom_feed_helper.rb @@ -32,7 +32,7 @@ module ActionView # app/views/posts/index.atom.builder: # atom_feed do |feed| # feed.title("My great blog!") - # feed.updated(@posts.first.created_at) + # feed.updated(@posts[0].created_at) if @posts.length > 0 # # @posts.each do |post| # feed.entry(post) do |entry| diff --git a/actionpack/lib/action_view/helpers/benchmark_helper.rb b/actionpack/lib/action_view/helpers/benchmark_helper.rb new file mode 100644 index 0000000000..dfdd5a786d --- /dev/null +++ b/actionpack/lib/action_view/helpers/benchmark_helper.rb @@ -0,0 +1,13 @@ +require 'active_support/benchmarkable' + +module ActionView + module Helpers + module BenchmarkHelper + include ActiveSupport::Benchmarkable + + def benchmark(*) + capture { super } + end + end + end +end diff --git a/actionpack/lib/action_view/helpers/capture_helper.rb b/actionpack/lib/action_view/helpers/capture_helper.rb index 8abd85c3a3..17bbfe2efd 100644 --- a/actionpack/lib/action_view/helpers/capture_helper.rb +++ b/actionpack/lib/action_view/helpers/capture_helper.rb @@ -81,8 +81,8 @@ module ActionView # <%# This is the layout %> # <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> # <head> - # <title>My Website</title> - # <%= yield :script %> + # <title>My Website</title> + # <%= yield :script %> # </head> # <body> # <%= yield %> @@ -110,7 +110,7 @@ module ActionView # That will place +script+ tags for your default set of JavaScript files on the page; # this technique is useful if you'll only be using these scripts in a few views. # - # Note that content_for concatenates the blocks it is given for a particular + # Note that content_for concatenates (default) the blocks it is given for a particular # identifier in order. For example: # # <% content_for :navigation do %> @@ -127,16 +127,37 @@ module ActionView # # <ul><%= content_for :navigation %></ul> # + # If the flush parameter is true content_for replaces the blocks it is given. For example: + # + # <% content_for :navigation do %> + # <li><%= link_to 'Home', :action => 'index' %></li> + # <% end %> + # + # <%# Add some other content, or use a different template: %> + # + # <% content_for :navigation, true do %> + # <li><%= link_to 'Login', :action => 'login' %></li> + # <% end %> + # + # Then, in another template or layout, this code would render only the last link: + # + # <ul><%= content_for :navigation %></ul> + # # Lastly, simple content can be passed as a parameter: # # <% content_for :script, javascript_include_tag(:defaults) %> # # 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) + def content_for(name, content = nil, flush = false, &block) if content || block_given? - content = capture(&block) if block_given? - @view_flow.append(name, content) if content + if block_given? + flush = content if content + content = capture(&block) + end + if content + flush ? @view_flow.set(name, content) : @view_flow.append(name, content) + end nil else @view_flow.get(name) @@ -164,8 +185,8 @@ module ActionView # <%# This is the layout %> # <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> # <head> - # <title>My Website</title> - # <%= yield :script %> + # <title>My Website</title> + # <%= yield :script %> # </head> # <body class="<%= content_for?(:right_col) ? 'one-column' : 'two-column' %>"> # <%= yield %> @@ -181,7 +202,7 @@ module ActionView def with_output_buffer(buf = nil) #:nodoc: unless buf buf = ActionView::OutputBuffer.new - buf.force_encoding(output_buffer.encoding) if output_buffer.respond_to?(:encoding) && buf.respond_to?(:force_encoding) + buf.force_encoding(output_buffer.encoding) if output_buffer end self.output_buffer, old_buffer = buf, output_buffer yield diff --git a/actionpack/lib/action_view/helpers/csrf_helper.rb b/actionpack/lib/action_view/helpers/csrf_helper.rb index 1f2bc28cac..eeb0ed94b9 100644 --- a/actionpack/lib/action_view/helpers/csrf_helper.rb +++ b/actionpack/lib/action_view/helpers/csrf_helper.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/string/strip' - module ActionView # = Action View CSRF Helper module Helpers diff --git a/actionpack/lib/action_view/helpers/date_helper.rb b/actionpack/lib/action_view/helpers/date_helper.rb index 4deb87180c..2d37923825 100644 --- a/actionpack/lib/action_view/helpers/date_helper.rb +++ b/actionpack/lib/action_view/helpers/date_helper.rb @@ -142,6 +142,8 @@ module ActionView # ==== Options # * <tt>:use_month_numbers</tt> - Set to true if you want to use month numbers rather than month names (e.g. # "2" instead of "February"). + # * <tt>:use_two_digit_numbers</tt> - Set to true if you want to display two digit month and day numbers (e.g. + # "02" instead of "February" and "08" instead of "8"). # * <tt>:use_short_month</tt> - Set to true if you want to use abbreviated month names instead of full # month names (e.g. "Feb" instead of "February"). # * <tt>:add_month_numbers</tt> - Set to true if you want to use both month numbers and month names (e.g. @@ -189,6 +191,10 @@ module ActionView # 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 article variable, in the written_on attribute, + # # with two digit numbers used for months and days. + # date_select("article", "written_on", :use_two_digit_numbers => true) + # # # 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("article", "written_on", :order => [:day, :month, :year]) @@ -213,7 +219,7 @@ module ActionView # Note: If the day is not included as an option but the month is, the day will be set to the 1st to ensure that # all month choices are valid. def date_select(object_name, method, options = {}, html_options = {}) - InstanceTag.new(object_name, method, self, options.delete(:object)).to_date_select_tag(options, html_options) + Tags::DateSelect.new(object_name, method, self, options, html_options).render end # Returns a set of select tags (one for hour, minute and optionally second) pre-selected for accessing a @@ -251,7 +257,7 @@ module ActionView # Note: If the day is not included as an option but the month is, the day will be set to the 1st to ensure that # all month choices are valid. def time_select(object_name, method, options = {}, html_options = {}) - InstanceTag.new(object_name, method, self, options.delete(:object)).to_time_select_tag(options, html_options) + Tags::TimeSelect.new(object_name, method, self, options, html_options).render end # Returns a set of select tags (one for year, month, day, hour, and minute) pre-selected for accessing a @@ -287,7 +293,7 @@ module ActionView # # 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) + Tags::DatetimeSelect.new(object_name, method, self, options, html_options).render end # Returns a set of html select-tags (one for year, month, day, hour, minute, and second) pre-selected with the @@ -422,7 +428,7 @@ module ActionView end # Returns a select tag with options for each of the seconds 0 through 59 with the current second selected. - # The <tt>datetime</tt> can be either a +Time+ or +DateTime+ object or an integer. + # 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 @@ -448,7 +454,7 @@ 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>datetime</tt> can be either a +Time+ or +DateTime+ object or an integer. + # 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 @@ -473,7 +479,7 @@ module ActionView end # Returns a select tag with options for each of the hours 0 through 23 with the current hour selected. - # The <tt>datetime</tt> can be either a +Time+ or +DateTime+ object or an integer. + # 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 @@ -502,6 +508,7 @@ module ActionView # 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 day number. + # If you want to display days with a leading zero set the <tt>:use_two_digit_numbers</tt> key in +options+ to true. # Override the field name using the <tt>:field_name</tt> option, 'day' by default. # # ==== Examples @@ -513,6 +520,9 @@ module ActionView # # 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 number given, but displays it with two digits. + # select_day(5, :use_two_digit_numbers => true) + # # # Generates a select field for days that defaults to the day for the date in my_date # # that is named 'due' rather than 'day'. # select_day(my_time, :field_name => 'due') @@ -532,6 +542,7 @@ module ActionView # want both numbers and names, set the <tt>:add_month_numbers</tt> key in +options+ to true. If you would prefer # to show month names as abbreviations, set the <tt>:use_short_month</tt> key in +options+ to true. If you want # to use your own month names, set the <tt>:use_month_names</tt> key in +options+ to an array of 12 month names. + # If you want to display months with a leading zero set the <tt>:use_two_digit_numbers</tt> key in +options+ to true. # Override the field name using the <tt>:field_name</tt> option, 'month' by default. # # ==== Examples @@ -559,6 +570,10 @@ 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 that defaults to the current month that + # # will use keys with two digit numbers like "01", "03". + # select_month(Date.today, :use_two_digit_numbers => true) + # # # 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') @@ -734,15 +749,15 @@ module ActionView def select_day if @options[:use_hidden] || @options[:discard_day] - build_hidden(:day, day) + build_hidden(:day, day || 1) else - build_options_and_select(:day, day, :start => 1, :end => 31, :leading_zeros => false) + build_options_and_select(:day, day, :start => 1, :end => 31, :leading_zeros => false, :use_two_digit_numbers => @options[:use_two_digit_numbers]) end end def select_month if @options[:use_hidden] || @options[:discard_month] - build_hidden(:month, month) + build_hidden(:month, month || 1) else month_options = [] 1.upto(12) do |month_number| @@ -756,7 +771,7 @@ module ActionView def select_year if !@datetime || @datetime == 0 - val = '' + val = '1' middle_year = Date.today.year else val = middle_year = year @@ -817,11 +832,16 @@ module ActionView # If <tt>:use_month_numbers</tt> option is passed # month_name(1) => 1 # + # If <tt>:use_two_month_numbers</tt> option is passed + # month_name(1) => '01' + # # If <tt>:add_month_numbers</tt> option is passed # month_name(1) => "1 - January" def month_name(number) if @options[:use_month_numbers] number + elsif @options[:use_two_digit_numbers] + sprintf "%02d", number elsif @options[:add_month_numbers] "#{number} - #{month_names[number]}" else @@ -834,7 +854,15 @@ module ActionView end def translated_date_order - I18n.translate(:'date.order', :locale => @options[:locale]) || [] + date_order = I18n.translate(:'date.order', :locale => @options[:locale], :default => []) + + forbidden_elements = date_order - [:year, :month, :day] + if forbidden_elements.any? + raise StandardError, + "#{@options[:locale]}.date.order only accepts :year, :month and :day" + end + + date_order end # Build full select tag from date type and options. @@ -848,6 +876,12 @@ module ActionView # <option value="2">2</option> # <option value="3">3</option>..." # + # If <tt>:use_two_digit_numbers => true</tt> option is passed + # build_options(15, :start => 1, :end => 31, :use_two_digit_numbers => true) + # => "<option value="1">01</option> + # <option value="2">02</option> + # <option value="3">03</option>..." + # # If <tt>:step</tt> options is passed # build_options(15, :start => 1, :end => 31, :step => 2) # => "<option value="1">1</option> @@ -857,7 +891,7 @@ module ActionView start = options.delete(:start) || 0 stop = options.delete(:end) || 59 step = options.delete(:step) || 1 - options.reverse_merge!({:leading_zeros => true, :ampm => false}) + options.reverse_merge!({:leading_zeros => true, :ampm => false, :use_two_digit_numbers => false}) leading_zeros = options.delete(:leading_zeros) select_options = [] @@ -865,7 +899,8 @@ module ActionView value = leading_zeros ? sprintf("%02d", i) : i tag_options = { :value => value } tag_options[:selected] = "selected" if selected == i - text = options[:ampm] ? AMPM_TRANSLATION[i] : value + text = options[:use_two_digit_numbers] ? sprintf("%02d", i) : value + text = options[:ampm] ? AMPM_TRANSLATION[i] : text select_options << content_tag(:option, text, tag_options) end (select_options.join("\n") + "\n").html_safe @@ -944,8 +979,9 @@ module ActionView # and join them with their appropriate separators. def build_selects_from_types(order) select = '' + first_visible = order.find { |type| !@options[:"discard_#{type}"] } order.reverse.each do |type| - separator = separator(type) unless type == order.first # don't add on last field + separator = separator(type) unless type == first_visible # don't add before first visible field select.insert(0, separator.to_s + send("select_#{type}").to_s) end select.html_safe @@ -954,74 +990,12 @@ module ActionView # Returns the separator for a given datetime component. def separator(type) case type - when :year - @options[:discard_year] ? "" : @options[:date_separator] - when :month - @options[:discard_month] ? "" : @options[:date_separator] - when :day - @options[:discard_day] ? "" : @options[:date_separator] + when :year, :month, :day + @options[:"discard_#{type}"] ? "" : @options[:date_separator] when :hour (@options[:discard_year] && @options[:discard_day]) ? "" : @options[:datetime_separator] - when :minute - @options[:discard_minute] ? "" : @options[:time_separator] - when :second - @options[:include_seconds] ? @options[:time_separator] : "" - end - end - end - - class InstanceTag #:nodoc: - def to_date_select_tag(options = {}, html_options = {}) - datetime_selector(options, html_options).select_date.html_safe - end - - def to_time_select_tag(options = {}, html_options = {}) - datetime_selector(options, html_options).select_time.html_safe - end - - def to_datetime_select_tag(options = {}, html_options = {}) - datetime_selector(options, html_options).select_datetime.html_safe - end - - private - def datetime_selector(options, html_options) - datetime = value(object) || default_datetime(options) - @auto_index ||= nil - - options = options.dup - options[:field_name] = @method_name - options[:include_position] = true - options[:prefix] ||= @object_name - options[:index] = @auto_index if @auto_index && !options.has_key?(:index) - - DateTimeSelector.new(datetime, options, html_options) - end - - def default_datetime(options) - return if options[:include_blank] || options[:prompt] - - case options[:default] - when nil - Time.current - when Date, Time - options[:default] - else - default = options[:default].dup - - # Rename :minute and :second to :min and :sec - default[:min] ||= default[:minute] - default[:sec] ||= default[:second] - - time = Time.current - - [:year, :month, :day, :hour, :min, :sec].each do |key| - default[key] ||= time.send(key) - end - - Time.utc_time( - default[:year], default[:month], default[:day], - default[:hour], default[:min], default[:sec] - ) + when :minute, :second + @options[:"discard_#{type}"] ? "" : @options[:time_separator] end end end diff --git a/actionpack/lib/action_view/helpers/form_helper.rb b/actionpack/lib/action_view/helpers/form_helper.rb index f22c466666..44e24fecd1 100644 --- a/actionpack/lib/action_view/helpers/form_helper.rb +++ b/actionpack/lib/action_view/helpers/form_helper.rb @@ -2,9 +2,10 @@ require 'cgi' require 'action_view/helpers/date_helper' require 'action_view/helpers/tag_helper' require 'action_view/helpers/form_tag_helper' +require 'action_view/helpers/active_model_helper' +require 'action_view/helpers/tags' 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' @@ -158,6 +159,9 @@ module ActionView # * <tt>:url</tt> - The URL the form is submitted to. It takes the same # fields you pass to +url_for+ or +link_to+. In particular you may pass # here a named route directly as well. Defaults to the current action. + # * <tt>:namespace</tt> - A namespace for your form to ensure uniqueness of + # id attributes on form elements. The namespace attribute will be prefixed + # with underscore on the generated HTML id. # * <tt>:html</tt> - Optional HTML attributes for the form tag. # # Also note that +form_for+ doesn't create an exclusive scope. It's still @@ -362,34 +366,33 @@ module ActionView else object = record.is_a?(Array) ? record.last : record object_name = options[:as] || ActiveModel::Naming.param_key(object) - apply_form_for_options!(record, options) + apply_form_for_options!(record, object, options) end 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) - builder = options[:parent_builder] = instantiate_builder(object_name, object, options, &proc) + builder = options[:parent_builder] = instantiate_builder(object_name, object, options) fields_for = fields_for(object_name, object, options, &proc) default_options = builder.multipart? ? { :multipart => true } : {} - output = form_tag(options.delete(:url) || {}, default_options.merge!(options.delete(:html))) - output << fields_for - output.safe_concat('</form>') + default_options.merge!(options.delete(:html)) + + form_tag(options.delete(:url) || {}, default_options) { fields_for } end - def apply_form_for_options!(object_or_array, options) #:nodoc: - object = object_or_array.is_a?(Array) ? object_or_array.last : object_or_array + def apply_form_for_options!(record, object, options) #:nodoc: object = convert_to_model(object) as = options[:as] action, method = object.respond_to?(:persisted?) && object.persisted? ? [:edit, :put] : [:new, :post] options[:html].reverse_merge!( - :class => as ? "#{as}_#{action}" : dom_class(object, action), - :id => as ? "#{as}_#{action}" : dom_id(object, action), + :class => as ? "#{action}_#{as}" : dom_class(object, action), + :id => as ? "#{action}_#{as}" : [options[:namespace], dom_id(object, action)].compact.join("_").presence, :method => method ) - options[:url] ||= polymorphic_path(object_or_array, :format => options.delete(:format)) + options[:url] ||= polymorphic_path(record, :format => options.delete(:format)) end private :apply_form_for_options! @@ -598,7 +601,7 @@ module ActionView # ... # <% end %> def fields_for(record_name, record_object = nil, options = {}, &block) - builder = instantiate_builder(record_name, record_object, options, &block) + builder = instantiate_builder(record_name, record_object, options) output = capture(builder, &block) output.concat builder.hidden_field(:id) if output && options[:hidden_field_id] && !builder.emitted_hidden_id? output @@ -649,19 +652,10 @@ module ActionView # # => <label for="post_privacy_public">Public Post</label> # # label(:post, :terms) do - # 'Accept <a href="/terms">Terms</a>.' + # 'Accept <a href="/terms">Terms</a>.'.html_safe # end def label(object_name, method, content_or_options = nil, options = nil, &block) - content_is_options = content_or_options.is_a?(Hash) - if content_is_options || block_given? - options = content_or_options if content_is_options - text = nil - else - text = content_or_options - end - - options ||= {} - InstanceTag.new(object_name, method, self, options.delete(:object)).to_label_tag(text, options, &block) + Tags::Label.new(object_name, method, self, content_or_options, options).render(&block) end # Returns an input tag of the "text" type tailored for accessing a specified attribute (identified by +method+) on an object @@ -683,7 +677,7 @@ module ActionView # # => <input type="text" id="snippet_code" name="snippet[code]" size="20" value="#{@snippet.code}" class="code_input" /> # def text_field(object_name, method, options = {}) - InstanceTag.new(object_name, method, self, options.delete(:object)).to_input_field_tag("text", options) + Tags::TextField.new(object_name, method, self, options).render end # Returns an input tag of the "password" type tailored for accessing a specified attribute (identified by +method+) on an object @@ -705,7 +699,7 @@ module ActionView # # => <input type="password" id="account_pin" name="account[pin]" size="20" class="form_input" /> # def password_field(object_name, method, options = {}) - InstanceTag.new(object_name, method, self, options.delete(:object)).to_input_field_tag("password", { :value => nil }.merge!(options)) + Tags::PasswordField.new(object_name, method, self, options).render end # Returns a hidden input tag tailored for accessing a specified attribute (identified by +method+) on an object @@ -723,7 +717,7 @@ module ActionView # hidden_field(:user, :token) # # => <input type="hidden" id="user_token" name="user[token]" value="#{@user.token}" /> def hidden_field(object_name, method, options = {}) - InstanceTag.new(object_name, method, self, options.delete(:object)).to_input_field_tag("hidden", options) + Tags::HiddenField.new(object_name, method, self, options).render end # Returns a file upload input tag tailored for accessing a specified attribute (identified by +method+) on an object @@ -738,13 +732,13 @@ module ActionView # # => <input type="file" id="user_avatar" name="user[avatar]" /> # # file_field(:post, :attached, :accept => 'text/html') - # # => <input type="file" id="post_attached" name="post[attached]" /> + # # => <input accept="text/html" type="file" id="post_attached" name="post[attached]" /> # # file_field(:attachment, :file, :class => 'file_input') # # => <input type="file" id="attachment_file" name="attachment[file]" class="file_input" /> # def file_field(object_name, method, options = {}) - InstanceTag.new(object_name, method, self, options.delete(:object)).to_input_field_tag("file", options.update({:size => nil})) + Tags::FileField.new(object_name, method, self, options).render end # Returns a textarea opening and closing tag set tailored for accessing a specified attribute (identified by +method+) @@ -772,7 +766,7 @@ module ActionView # # #{@entry.body} # # </textarea> def text_area(object_name, method, options = {}) - InstanceTag.new(object_name, method, self, options.delete(:object)).to_text_area_tag(options) + Tags::TextArea.new(object_name, method, self, options).render end # Returns a checkbox tag tailored for accessing a specified attribute (identified by +method+) on an object @@ -834,7 +828,7 @@ module ActionView # # <input type="checkbox" class="eula_check" id="eula_accepted" name="eula[accepted]" value="yes" /> # def check_box(object_name, method, options = {}, checked_value = "1", unchecked_value = "0") - InstanceTag.new(object_name, method, self, options.delete(:object)).to_check_box_tag(options, checked_value, unchecked_value) + Tags::CheckBox.new(object_name, method, self, checked_value, unchecked_value, options).render end # Returns a radio button tag for accessing a specified attribute (identified by +method+) on an object @@ -856,7 +850,7 @@ module ActionView # # => <input type="radio" id="user_receive_newsletter_yes" name="user[receive_newsletter]" value="yes" /> # # <input type="radio" id="user_receive_newsletter_no" name="user[receive_newsletter]" value="no" checked="checked" /> def radio_button(object_name, method, tag_value, options = {}) - InstanceTag.new(object_name, method, self, options.delete(:object)).to_radio_button_tag(tag_value, options) + Tags::RadioButton.new(object_name, method, self, tag_value, options).render end # Returns an input of type "search" for accessing a specified attribute (identified by +method+) on an object @@ -882,20 +876,7 @@ module ActionView # # => <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 - - if options["autosave"] - if options["autosave"] == true - options["autosave"] = request.host.split(".").reverse.join(".") - end - options["results"] ||= 10 - end - - if options["onsearch"] - options["incremental"] = true unless options.has_key?("incremental") - end - - InstanceTag.new(object_name, method, self, options.delete("object")).to_input_field_tag("search", options) + Tags::SearchField.new(object_name, method, self, options).render end # Returns a text_field of type "tel". @@ -904,17 +885,35 @@ module ActionView # # => <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) + Tags::TelField.new(object_name, method, self, options).render end alias phone_field telephone_field + # Returns a text_field of type "date". + # + # date_field("user", "born_on") + # # => <input id="user_born_on" name="user[born_on]" type="date" /> + # + # The default value is generated by trying to call "to_date" + # on the object's value, which makes it behave as expected for instances + # of DateTime and ActiveSupport::TimeWithZone. You can still override that + # by passing the "value" option explicitly, e.g. + # + # @user.born_on = Date.new(1984, 1, 27) + # date_field("user", "born_on", value: "1984-05-12") + # # => <input id="user_born_on" name="user[born_on]" type="date" value="1984-05-12" /> + # + def date_field(object_name, method, options = {}) + Tags::DateField.new(object_name, method, self, options).render + end + # 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) + Tags::UrlField.new(object_name, method, self, options).render end # Returns a text_field of type "email". @@ -923,7 +922,7 @@ module ActionView # # => <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) + Tags::EmailField.new(object_name, method, self, options).render end # Returns an input tag of type "number". @@ -931,7 +930,7 @@ module ActionView # ==== Options # * Accepts same options as number_field_tag def number_field(object_name, method, options = {}) - InstanceTag.new(object_name, method, self, options.delete(:object)).to_number_field_tag("number", options) + Tags::NumberField.new(object_name, method, self, options).render end # Returns an input tag of type "range". @@ -939,12 +938,12 @@ module ActionView # ==== Options # * Accepts same options as range_field_tag def range_field(object_name, method, options = {}) - InstanceTag.new(object_name, method, self, options.delete(:object)).to_number_field_tag("range", options) + Tags::RangeField.new(object_name, method, self, options).render end private - def instantiate_builder(record_name, record_object, options, &block) + def instantiate_builder(record_name, record_object, options) case record_name when String, Symbol object = record_object @@ -955,267 +954,14 @@ module ActionView end builder = options[:builder] || ActionView::Base.default_form_builder - builder.new(object_name, object, self, options, block) - end - end - - class InstanceTag - include Helpers::CaptureHelper, Context, Helpers::TagHelper, Helpers::FormTagHelper - - attr_reader :object, :method_name, :object_name - - DEFAULT_FIELD_OPTIONS = { "size" => 30 } - DEFAULT_RADIO_OPTIONS = { } - DEFAULT_TEXT_AREA_OPTIONS = { "cols" => 40, "rows" => 20 } - - def initialize(object_name, method_name, template_object, object = nil) - @object_name, @method_name = object_name.to_s.dup, method_name.to_s.dup - @template_object = template_object - @object_name.sub!(/\[\]$/,"") || @object_name.sub!(/\[\]\]$/,"]") - @object = retrieve_object(object) - @auto_index = retrieve_autoindex(Regexp.last_match.pre_match) if Regexp.last_match - end - - def to_label_tag(text = nil, options = {}, &block) - options = options.stringify_keys - tag_value = options.delete("value") - name_and_id = options.dup - - if name_and_id["for"] - name_and_id["id"] = name_and_id["for"] - else - name_and_id.delete("id") - end - - add_default_name_and_id_for_value(tag_value, name_and_id) - options.delete("index") - options["for"] ||= name_and_id["id"] - - if block_given? - label_tag(name_and_id["id"], options, &block) - else - content = if text.blank? - method_and_value = tag_value.present? ? "#{method_name}.#{tag_value}" : method_name - I18n.t("helpers.label.#{object_name}.#{method_and_value}", :default => "").presence - else - text.to_s - end - - content ||= if object && object.class.respond_to?(:human_attribute_name) - object.class.human_attribute_name(method_name) - end - - content ||= method_name.humanize - - label_tag(name_and_id["id"], content, options) - end - end - - def to_input_field_tag(field_type, options = {}) - options = options.stringify_keys - options["size"] = options["maxlength"] || DEFAULT_FIELD_OPTIONS["size"] unless options.key?("size") - options = DEFAULT_FIELD_OPTIONS.merge(options) - if field_type == "hidden" - options.delete("size") - end - options["type"] ||= field_type - options["value"] = options.fetch("value"){ value_before_type_cast(object) } unless field_type == "file" - options["value"] &&= ERB::Util.html_escape(options["value"]) - add_default_name_and_id(options) - tag("input", options) - end - - def to_number_field_tag(field_type, options = {}) - options = options.stringify_keys - if range = options.delete("in") || options.delete("within") - options.update("min" => range.min, "max" => range.max) - end - to_input_field_tag(field_type, options) - end - - def to_radio_button_tag(tag_value, options = {}) - options = DEFAULT_RADIO_OPTIONS.merge(options.stringify_keys) - options["type"] = "radio" - options["value"] = tag_value - if options.has_key?("checked") - cv = options.delete "checked" - checked = cv == true || cv == "checked" - else - checked = self.class.radio_button_checked?(value(object), tag_value) - end - options["checked"] = "checked" if checked - add_default_name_and_id_for_value(tag_value, options) - tag("input", options) - end - - def to_text_area_tag(options = {}) - options = DEFAULT_TEXT_AREA_OPTIONS.merge(options.stringify_keys) - add_default_name_and_id(options) - - if size = options.delete("size") - options["cols"], options["rows"] = size.split("x") if size.respond_to?(:split) - end - - content_tag("textarea", ERB::Util.html_escape(options.delete('value') || value_before_type_cast(object)), options) - end - - def to_check_box_tag(options = {}, checked_value = "1", unchecked_value = "0") - options = options.stringify_keys - options["type"] = "checkbox" - options["value"] = checked_value - if options.has_key?("checked") - cv = options.delete "checked" - checked = cv == true || cv == "checked" - else - checked = self.class.check_box_checked?(value(object), checked_value) - end - options["checked"] = "checked" if checked - if options["multiple"] - add_default_name_and_id_for_value(checked_value, options) - options.delete("multiple") - else - add_default_name_and_id(options) - end - hidden = tag("input", "name" => options["name"], "type" => "hidden", "value" => options['disabled'] && checked ? checked_value : unchecked_value) - checkbox = tag("input", options) - (hidden + checkbox).html_safe - end - - def to_boolean_select_tag(options = {}) - options = options.stringify_keys - add_default_name_and_id(options) - value = value(object) - tag_text = "<select" - tag_text << tag_options(options) - tag_text << "><option value=\"false\"" - tag_text << " selected" if value == false - tag_text << ">False</option><option value=\"true\"" - tag_text << " selected" if value - tag_text << ">True</option></select>" - end - - def to_content_tag(tag_name, options = {}) - content_tag(tag_name, value(object), options) - end - - def retrieve_object(object) - if object - object - elsif @template_object.instance_variable_defined?("@#{@object_name}") - @template_object.instance_variable_get("@#{@object_name}") - end - rescue NameError - # As @object_name may contain the nested syntax (item[subobject]) we need to fallback to nil. - nil - end - - def retrieve_autoindex(pre_match) - object = self.object || @template_object.instance_variable_get("@#{pre_match}") - if object && object.respond_to?(:to_param) - object.to_param - else - raise ArgumentError, "object[] naming but object param and @object var don't exist or don't respond to to_param: #{object.inspect}" - end - end - - def value(object) - self.class.value(object, @method_name) - end - - def value_before_type_cast(object) - self.class.value_before_type_cast(object, @method_name) - end - - class << self - def value(object, method_name) - object.send method_name if object - end - - def value_before_type_cast(object, method_name) - unless object.nil? - object.respond_to?(method_name + "_before_type_cast") ? - object.send(method_name + "_before_type_cast") : - object.send(method_name) - end - end - - def check_box_checked?(value, checked_value) - case value - when TrueClass, FalseClass - value - when NilClass - false - when Integer - value != 0 - when String - value == checked_value - when Array - value.include?(checked_value) - else - value.to_i != 0 - end - end - - def radio_button_checked?(value, checked_value) - value.to_s == checked_value.to_s - end - end - - private - def add_default_name_and_id_for_value(tag_value, options) - unless tag_value.nil? - pretty_tag_value = tag_value.to_s.gsub(/\s/, "_").gsub(/[^-\w]/, "").downcase - specified_id = options["id"] - add_default_name_and_id(options) - options["id"] += "_#{pretty_tag_value}" if specified_id.blank? && options["id"].present? - else - add_default_name_and_id(options) - end - end - - def add_default_name_and_id(options) - if options.has_key?("index") - options["name"] ||= tag_name_with_index(options["index"]) - options["id"] = options.fetch("id"){ tag_id_with_index(options["index"]) } - options.delete("index") - elsif defined?(@auto_index) - 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['multiple'] ? '[]' : '') - options["id"] = options.fetch("id"){ tag_id } - end - end - - def tag_name - "#{@object_name}[#{sanitized_method_name}]" - end - - def tag_name_with_index(index) - "#{@object_name}[#{index}][#{sanitized_method_name}]" - end - - def tag_id - "#{sanitized_object_name}_#{sanitized_method_name}" - end - - def tag_id_with_index(index) - "#{sanitized_object_name}_#{index}_#{sanitized_method_name}" - end - - def sanitized_object_name - @sanitized_object_name ||= @object_name.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "") - end - - def sanitized_method_name - @sanitized_method_name ||= @method_name.sub(/\?$/,"") + builder.new(object_name, object, self, options) end end class FormBuilder # The methods which wrap a form helper call. class_attribute :field_helpers - self.field_helpers = FormHelper.instance_method_names - %w(form_for convert_to_model) + self.field_helpers = FormHelper.instance_methods - [:form_for, :convert_to_model] attr_accessor :object_name, :object, :options @@ -1239,11 +985,11 @@ module ActionView self end - def initialize(object_name, object, template, options, proc) + def initialize(object_name, object, template, options) @nested_child_index = {} - @object_name, @object, @template, @options, @proc = object_name, object, template, options, proc + @object_name, @object, @template, @options = object_name, object, template, options @parent_builder = options[:parent_builder] - @default_options = @options ? @options.slice(:index) : {} + @default_options = @options ? @options.slice(:index, :namespace) : {} if @object_name.to_s.match(/\[\]$/) if object ||= @template.instance_variable_get("@#{Regexp.last_match.pre_match}") and object.respond_to?(:to_param) @auto_index = object.to_param @@ -1254,7 +1000,7 @@ module ActionView @multipart = nil end - (field_helpers - %w(label check_box radio_button fields_for hidden_field file_field)).each do |selector| + (field_helpers - [:label, :check_box, :radio_button, :fields_for, :hidden_field, :file_field]).each do |selector| class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 def #{selector}(method, options = {}) # def text_field(method, options = {}) @template.send( # @template.send( @@ -1270,6 +1016,7 @@ module ActionView 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 + fields_options[:namespace] = fields_options[:parent_builder].options[:namespace] case record_name when String, Symbol @@ -1347,6 +1094,39 @@ module ActionView @template.submit_tag(value, options) end + # Add the submit button for the given form. When no value is given, it checks + # if the object is a new resource or not to create the proper label: + # + # <%= form_for @post do |f| %> + # <%= f.button %> + # <% end %> + # + # In the example above, if @post is a new record, it will use "Create Post" as + # submit button label, otherwise, it uses "Update Post". + # + # Those labels can be customized using I18n, under the helpers.submit key and accept + # the %{model} as translation interpolation: + # + # en: + # helpers: + # button: + # create: "Create a %{model}" + # update: "Confirm changes to %{model}" + # + # It also searches for a key specific for the given object: + # + # en: + # helpers: + # button: + # post: + # create: "Add %{model}" + # + def button(value=nil, options={}) + value, options = nil, value if value.is_a?(Hash) + value ||= submit_default_value + @template.button_tag(value, options) + end + def emitted_hidden_id? @emitted_hidden_id ||= nil end @@ -1421,9 +1201,6 @@ module ActionView end ActiveSupport.on_load(:action_view) do - class ActionView::Base - cattr_accessor :default_form_builder - @@default_form_builder = ::ActionView::Helpers::FormBuilder - end + cattr_accessor(:default_form_builder) { ::ActionView::Helpers::FormBuilder } end end diff --git a/actionpack/lib/action_view/helpers/form_options_helper.rb b/actionpack/lib/action_view/helpers/form_options_helper.rb index f895cad058..bc03a1cf83 100644 --- a/actionpack/lib/action_view/helpers/form_options_helper.rb +++ b/actionpack/lib/action_view/helpers/form_options_helper.rb @@ -134,7 +134,7 @@ module ActionView # # ==== Gotcha # - # The HTML specification says when +multiple+ parameter passed to select and all options got deselected + # 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, @@ -154,7 +154,7 @@ module ActionView # 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) + Tags::Select.new(object, method, self, choices, options, html_options).render end # Returns <tt><select></tt> and <tt><option></tt> tags for the collection of existing return values of @@ -164,7 +164,9 @@ module ActionView # # The <tt>:value_method</tt> and <tt>:text_method</tt> parameters are methods to be called on each member # of +collection+. The return values are used as the +value+ attribute and contents of each - # <tt><option></tt> tag, respectively. + # <tt><option></tt> tag, respectively. They can also be any object that responds to +call+, such + # as a +proc+, that will be called for each member of the +collection+ to + # retrieve the value/text. # # Example object structure for use with this method: # class Post < ActiveRecord::Base @@ -188,10 +190,9 @@ module ActionView # <option value="3">M. Clark</option> # </select> def collection_select(object, method, collection, value_method, text_method, options = {}, html_options = {}) - InstanceTag.new(object, method, self, options.delete(:object)).to_collection_select_tag(collection, value_method, text_method, options, html_options) + Tags::CollectionSelect.new(object, method, self, collection, value_method, text_method, options, html_options).render end - # Returns <tt><select></tt>, <tt><optgroup></tt> and <tt><option></tt> tags for the collection of existing return values of # +method+ for +object+'s class. The value returned from calling +method+ on the instance +object+ will # be selected. If calling +method+ returns +nil+, no selection is made without including <tt>:prompt</tt> @@ -240,7 +241,7 @@ module ActionView # </select> # def grouped_collection_select(object, method, collection, group_method, group_label_method, option_key_method, option_value_method, options = {}, html_options = {}) - InstanceTag.new(object, method, self, options.delete(:object)).to_grouped_collection_select_tag(collection, group_method, group_label_method, option_key_method, option_value_method, options, html_options) + Tags::GroupedCollectionSelect.new(object, method, self, collection, group_method, group_label_method, option_key_method, option_value_method, options, html_options).render end # Return select and option tags for the given object and method, using @@ -274,7 +275,7 @@ module ActionView # # time_zone_select( "user", "time_zone", ActiveSupport::TimeZone.all.sort, :model => ActiveSupport::TimeZone) def time_zone_select(object, method, priority_zones = nil, options = {}, html_options = {}) - InstanceTag.new(object, method, self, options.delete(:object)).to_time_zone_select_tag(priority_zones, options, html_options) + Tags::TimeZoneSelect.new(object, method, self, priority_zones, options, html_options).render end # Accepts a container (hash, array, enumerable, your type) and returns a string of option tags. Given a container @@ -322,8 +323,8 @@ module ActionView def options_for_select(container, selected = nil) return container if String === container - selected, disabled = extract_selected_and_disabled(selected).map do | r | - Array.wrap(r).map { |item| item.to_s } + selected, disabled = extract_selected_and_disabled(selected).map do |r| + Array(r).map { |item| item.to_s } end container.map do |element| @@ -333,10 +334,9 @@ module ActionView 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>) end.join("\n").html_safe - end - # Returns a string of option tags that have been compiled by iterating over the +collection+ and assigning the + # Returns a string of option tags that have been compiled by iterating over the +collection+ and assigning # the result of a call to the +value_method+ as the option value and the +text_method+ as the option text. # Example: # options_from_collection_for_select(@people, 'id', 'name') @@ -362,12 +362,13 @@ module ActionView # should produce the desired results. def options_from_collection_for_select(collection, value_method, text_method, selected = nil) options = collection.map do |element| - [element.send(text_method), element.send(value_method)] + [value_for_collection(element, text_method), value_for_collection(element, value_method)] end selected, disabled = extract_selected_and_disabled(selected) - select_deselect = {} - select_deselect[:selected] = extract_values_from_collection(collection, value_method, selected) - select_deselect[:disabled] = extract_values_from_collection(collection, value_method, disabled) + select_deselect = { + :selected => extract_values_from_collection(collection, value_method, selected), + :disabled => extract_values_from_collection(collection, value_method, disabled) + } options_for_select(options, select_deselect) end @@ -420,10 +421,10 @@ module ActionView # wrap the output in an appropriate <tt><select></tt> tag. def option_groups_from_collection_for_select(collection, group_method, group_label_method, option_key_method, option_value_method, selected_key = nil) collection.map do |group| - group_label_string = eval("group.#{group_label_method}") - "<optgroup label=\"#{ERB::Util.html_escape(group_label_string)}\">" + - options_from_collection_for_select(eval("group.#{group_method}"), option_key_method, option_value_method, selected_key) + - '</optgroup>' + option_tags = options_from_collection_for_select( + group.send(group_method), option_key_method, option_value_method, selected_key) + + content_tag(:optgroup, option_tags, :label => group.send(group_label_method)) end.join.html_safe end @@ -508,9 +509,9 @@ module ActionView convert_zones = lambda { |list| list.map { |z| [ z.to_s, z.name ] } } if priority_zones - if priority_zones.is_a?(Regexp) + if priority_zones.is_a?(Regexp) priority_zones = model.all.find_all {|z| z =~ priority_zones} - end + end zone_options += options_for_select(convert_zones[priority_zones], selected) zone_options += "<option value=\"\" disabled=\"disabled\">-------------</option>\n" @@ -521,14 +522,138 @@ module ActionView zone_options.html_safe end + # Returns radio button tags for the collection of existing return values + # of +method+ for +object+'s class. The value returned from calling + # +method+ on the instance +object+ will be selected. If calling +method+ + # returns +nil+, no selection is made. + # + # The <tt>:value_method</tt> and <tt>:text_method</tt> parameters are + # methods to be called on each member of +collection+. The return values + # are used as the +value+ attribute and contents of each radio button tag, + # respectively. They can also be any object that responds to +call+, such + # as a +proc+, that will be called for each member of the +collection+ to + # retrieve the value/text. + # + # Example object structure for use with this method: + # class Post < ActiveRecord::Base + # belongs_to :author + # end + # class Author < ActiveRecord::Base + # has_many :posts + # def name_with_initial + # "#{first_name.first}. #{last_name}" + # end + # end + # + # Sample usage (selecting the associated Author for an instance of Post, <tt>@post</tt>): + # collection_radio_buttons(:post, :author_id, Author.all, :id, :name_with_initial) + # + # If <tt>@post.author_id</tt> is already <tt>1</tt>, this would return: + # <input id="post_author_id_1" name="post[author_id]" type="radio" value="1" checked="checked" /> + # <label for="post_author_id_1">D. Heinemeier Hansson</label> + # <input id="post_author_id_2" name="post[author_id]" type="radio" value="2" /> + # <label for="post_author_id_2">D. Thomas</label> + # <input id="post_author_id_3" name="post[author_id]" type="radio" value="3" /> + # <label for="post_author_id_3">M. Clark</label> + # + # It is also possible to customize the way the elements will be shown by + # giving a block to the method: + # collection_radio_buttons(:post, :author_id, Author.all, :id, :name_with_initial) do |b| + # b.label { b.radio_button } + # end + # + # The argument passed to the block is a special kind of builder for this + # collection, which has the ability to generate the label and radio button + # for the current item in the collection, with proper text and value. + # Using it, you can change the label and radio button display order or + # even use the label as wrapper, as in the example above. + # + # The builder methods <tt>label</tt> and <tt>radio_button</tt> also accept + # extra html options: + # collection_radio_buttons(:post, :author_id, Author.all, :id, :name_with_initial) do |b| + # b.label(:class => "radio_button") { b.radio_button(:class => "radio_button") } + # end + # + # There are also two special methods available: <tt>text</tt> and + # <tt>value</tt>, which are the current text and value methods for the + # item being rendered, respectively. You can use them like this: + # collection_radio_buttons(:post, :author_id, Author.all, :id, :name_with_initial) do |b| + # b.label(:"data-value" => b.value) { b.radio_button + b.text } + # end + def collection_radio_buttons(object, method, collection, value_method, text_method, options = {}, html_options = {}, &block) + Tags::CollectionRadioButtons.new(object, method, self, collection, value_method, text_method, options, html_options).render(&block) + end + + # Returns check box tags for the collection of existing return values of + # +method+ for +object+'s class. The value returned from calling +method+ + # on the instance +object+ will be selected. If calling +method+ returns + # +nil+, no selection is made. + # + # The <tt>:value_method</tt> and <tt>:text_method</tt> parameters are + # methods to be called on each member of +collection+. The return values + # are used as the +value+ attribute and contents of each check box tag, + # respectively. They can also be any object that responds to +call+, such + # as a +proc+, that will be called for each member of the +collection+ to + # retrieve the value/text. + # + # Example object structure for use with this method: + # class Post < ActiveRecord::Base + # has_and_belongs_to_many :author + # end + # class Author < ActiveRecord::Base + # has_and_belongs_to_many :posts + # def name_with_initial + # "#{first_name.first}. #{last_name}" + # end + # end + # + # Sample usage (selecting the associated Author for an instance of Post, <tt>@post</tt>): + # collection_check_boxes(:post, :author_ids, Author.all, :id, :name_with_initial) + # + # If <tt>@post.author_ids</tt> is already <tt>[1]</tt>, this would return: + # <input id="post_author_ids_1" name="post[author_ids][]" type="checkbox" value="1" checked="checked" /> + # <label for="post_author_ids_1">D. Heinemeier Hansson</label> + # <input id="post_author_ids_2" name="post[author_ids][]" type="checkbox" value="2" /> + # <label for="post_author_ids_2">D. Thomas</label> + # <input id="post_author_ids_3" name="post[author_ids][]" type="checkbox" value="3" /> + # <label for="post_author_ids_3">M. Clark</label> + # <input name="post[author_ids][]" type="hidden" value="" /> + # + # It is also possible to customize the way the elements will be shown by + # giving a block to the method: + # collection_check_boxes(:post, :author_ids, Author.all, :id, :name_with_initial) do |b| + # b.label { b.check_box } + # end + # + # The argument passed to the block is a special kind of builder for this + # collection, which has the ability to generate the label and check box + # for the current item in the collection, with proper text and value. + # Using it, you can change the label and check box display order or even + # use the label as wrapper, as in the example above. + # + # The builder methods <tt>label</tt> and <tt>check_box</tt> also accept + # extra html options: + # collection_check_boxes(:post, :author_ids, Author.all, :id, :name_with_initial) do |b| + # b.label(:class => "check_box") { b.check_box(:class => "check_box") } + # end + # + # There are also two special methods available: <tt>text</tt> and + # <tt>value</tt>, which are the current text and value methods for the + # item being rendered, respectively. You can use them like this: + # collection_check_boxes(:post, :author_ids, Author.all, :id, :name_with_initial) do |b| + # b.label(:"data-value" => b.value) { b.check_box + b.text } + # end + def collection_check_boxes(object, method, collection, value_method, text_method, options = {}, html_options = {}, &block) + Tags::CollectionCheckBoxes.new(object, method, self, collection, value_method, text_method, options, html_options).render(&block) + end + private def option_html_attributes(element) return "" unless Array === element - html_attributes = [] - element.select { |e| Hash === e }.reduce({}, :merge).each do |k, v| - html_attributes << " #{k}=\"#{ERB::Util.html_escape(v.to_s)}\"" - end - html_attributes.join + + element.select { |e| Hash === e }.reduce({}, :merge).map do |k, v| + " #{k}=\"#{ERB::Util.html_escape(v.to_s)}\"" + end.join end def option_text_and_value(option) @@ -554,11 +679,12 @@ module ActionView def extract_selected_and_disabled(selected) if selected.is_a?(Proc) - [ selected, nil ] + [selected, nil] else selected = Array.wrap(selected) options = selected.extract_options!.symbolize_keys - [ options.include?(:selected) ? options[:selected] : selected, options[:disabled] ] + selected_items = options.fetch(:selected, selected) + [selected_items, options[:disabled]] end end @@ -571,68 +697,9 @@ module ActionView selected end end - end - - class InstanceTag #:nodoc: - include FormOptionsHelper - - def to_select_tag(choices, options, html_options) - selected_value = options.has_key?(:selected) ? options[:selected] : value(object) - - # Grouped choices look like this: - # - # [nil, []] - # { nil => [] } - # - if !choices.empty? && Array === choices.first.last - 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) - 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) - 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) - select_content_tag( - time_zone_options_for_select(value(object) || options[:default], priority_zones, options[:model] || ActiveSupport::TimeZone), options, html_options - ) - end - - private - def add_options(option_tags, options, value = nil) - if options[:include_blank] - option_tags = "<option value=\"\">#{ERB::Util.html_escape(options[:include_blank]) if options[:include_blank].kind_of?(String)}</option>\n" + option_tags - end - if value.blank? && options[:prompt] - prompt = options[:prompt].kind_of?(String) ? options[:prompt] : I18n.translate('helpers.select.prompt', :default => 'Please select') - option_tags = "<option value=\"\">#{ERB::Util.html_escape(prompt)}</option>\n" + option_tags - 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 + def value_for_collection(item, value) + value.respond_to?(:call) ? value.call(item) : item.send(value) end end @@ -652,6 +719,14 @@ module ActionView def time_zone_select(method, priority_zones = nil, options = {}, html_options = {}) @template.time_zone_select(@object_name, method, priority_zones, objectify_options(options), @default_options.merge(html_options)) end + + def collection_check_boxes(method, collection, value_method, text_method, options = {}, html_options = {}) + @template.collection_check_boxes(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_options.merge(html_options)) + end + + def collection_radio_buttons(method, collection, value_method, text_method, options = {}, html_options = {}) + @template.collection_radio_buttons(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_options.merge(html_options)) + end end end end diff --git a/actionpack/lib/action_view/helpers/form_tag_helper.rb b/actionpack/lib/action_view/helpers/form_tag_helper.rb index 1424a3584d..53fd189c39 100644 --- a/actionpack/lib/action_view/helpers/form_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/form_tag_helper.rb @@ -47,7 +47,7 @@ module ActionView # <% end -%> # # => <form action="/posts" method="post"><div><input type="submit" name="submit" value="Save" /></div></form> # - # <%= form_tag('/posts', :remote => true) %> + # <%= form_tag('/posts', :remote => true) %> # # => <form action="/posts" method="post" data-remote="true"> # # form_tag('http://far.away.com/form', :authenticity_token => false) @@ -393,21 +393,17 @@ module ActionView # submit_tag "Save edits", :disabled => true # # => <input disabled="disabled" name="commit" type="submit" value="Save edits" /> # - # # submit_tag "Complete sale", :disable_with => "Please wait..." - # # => <input name="commit" data-disable-with="Please wait..." - # # type="submit" value="Complete sale" /> + # # => <input name="commit" data-disable-with="Please wait..." type="submit" value="Complete sale" /> # # submit_tag nil, :class => "form_submit" # # => <input class="form_submit" name="commit" type="submit" /> # # submit_tag "Edit", :disable_with => "Editing...", :class => "edit_button" - # # => <input class="edit_button" data-disable_with="Editing..." - # # name="commit" type="submit" value="Edit" /> + # # => <input class="edit_button" data-disable_with="Editing..." name="commit" type="submit" value="Edit" /> # # submit_tag "Save", :confirm => "Are you sure?" - # # => <input name='commit' type='submit' value='Save' - # data-confirm="Are you sure?" /> + # # => <input name='commit' type='submit' value='Save' data-confirm="Are you sure?" /> # def submit_tag(value = "Save changes", options = {}) options = options.stringify_keys @@ -451,12 +447,11 @@ module ActionView # content_tag(:strong, 'Ask me!') # end # # => <button name="button" type="button"> - # <strong>Ask me!</strong> - # </button> + # # <strong>Ask me!</strong> + # # </button> # # button_tag "Checkout", :disable_with => "Please wait..." - # # => <button data-disable-with="Please wait..." name="button" - # type="submit">Checkout</button> + # # => <button data-disable-with="Please wait..." name="button" type="submit">Checkout</button> # def button_tag(content_or_options = nil, options = nil, &block) options = content_or_options if block_given? && content_or_options.is_a?(Hash) @@ -554,6 +549,14 @@ module ActionView end alias phone_field_tag telephone_field_tag + # Creates a text field of type "date". + # + # ==== Options + # * Accepts the same options as text_field_tag. + def date_field_tag(name, value = nil, options = {}) + text_field_tag(name, value, options.stringify_keys.update("type" => "date")) + end + # Creates a text field of type "url". # # ==== Options @@ -582,7 +585,7 @@ module ActionView # # ==== Examples # number_field_tag 'quantity', nil, :in => 1...10 - # => <input id="quantity" name="quantity" min="1" max="9" type="number" /> + # # => <input id="quantity" name="quantity" min="1" max="9" type="number" /> def number_field_tag(name, value = nil, options = {}) options = options.stringify_keys options["type"] ||= "number" @@ -632,7 +635,7 @@ module ActionView token_tag(authenticity_token) else html_options["method"] = "post" - tag(:input, :type => "hidden", :name => "_method", :value => method) + token_tag(authenticity_token) + method_tag(method) + token_tag(authenticity_token) end tags = utf8_enforcer_tag << method_tag @@ -641,26 +644,16 @@ module ActionView def form_tag_html(html_options) extra_tags = extra_tags_for_form(html_options) - (tag(:form, html_options, true) + extra_tags).html_safe + tag(:form, html_options, true) + extra_tags end def form_tag_in_block(html_options, &block) content = capture(&block) - output = ActiveSupport::SafeBuffer.new - output.safe_concat(form_tag_html(html_options)) + output = form_tag_html(html_options) output << content output.safe_concat("</form>") end - def token_tag(token) - if token == false || !protect_against_forgery? - '' - else - token ||= form_authenticity_token - tag(:input, :type => "hidden", :name => request_forgery_protection_token.to_s, :value => token) - end - end - # see http://www.w3.org/TR/html4/types.html#type-name def sanitize_to_id(name) name.to_s.gsub(']','').gsub(/[^-a-zA-Z0-9:.]/, "_") diff --git a/actionpack/lib/action_view/helpers/javascript_helper.rb b/actionpack/lib/action_view/helpers/javascript_helper.rb index 1adcd716f8..ac9e530f01 100644 --- a/actionpack/lib/action_view/helpers/javascript_helper.rb +++ b/actionpack/lib/action_view/helpers/javascript_helper.rb @@ -1,5 +1,4 @@ require 'action_view/helpers/tag_helper' -require 'active_support/core_ext/string/encoding' module ActionView module Helpers @@ -14,11 +13,7 @@ module ActionView "'" => "\\'" } - if "ruby".encoding_aware? - JS_ESCAPE_MAP["\342\200\250".force_encoding('UTF-8').encode!] = '
' - else - JS_ESCAPE_MAP["\342\200\250"] = '
' - end + JS_ESCAPE_MAP["\342\200\250".force_encoding('UTF-8').encode!] = '
' # Escapes carriage returns and single and double quotes for JavaScript segments. # @@ -94,7 +89,7 @@ module ActionView # If +html_options+ has an <tt>:onclick</tt>, that one is put before +function+. Once all # the JavaScript is set, the helper appends "; return false;". # - # The +href+ attribute of the tag is set to "#" unles +html_options+ has one. + # The +href+ attribute of the tag is set to "#" unless +html_options+ has one. # # link_to_function "Greeting", "alert('Hello world!')", :class => "nav_link" # # => <a class="nav_link" href="#" onclick="alert('Hello world!'); return false;">Greeting</a> diff --git a/actionpack/lib/action_view/helpers/number_helper.rb b/actionpack/lib/action_view/helpers/number_helper.rb index 7031694af4..b0860f87c4 100644 --- a/actionpack/lib/action_view/helpers/number_helper.rb +++ b/actionpack/lib/action_view/helpers/number_helper.rb @@ -1,7 +1,6 @@ # 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' require 'active_support/core_ext/string/output_safety' @@ -33,33 +32,36 @@ module ActionView # in the +options+ hash. # # ==== Options - # * <tt>:area_code</tt> - Adds parentheses around the area code. - # * <tt>:delimiter</tt> - Specifies the delimiter to use (defaults to "-"). - # * <tt>:extension</tt> - Specifies an extension to add to the end of the - # generated number. + # + # * <tt>:area_code</tt> - Adds parentheses around the area code. + # * <tt>:delimiter</tt> - Specifies the delimiter to use (defaults to "-"). + # * <tt>:extension</tt> - Specifies an extension to add to the end of the + # generated number. # * <tt>:country_code</tt> - Sets the country code for the phone number. + # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when the argument is invalid. # # ==== Examples + # # number_to_phone(5551234) # => 555-1234 + # number_to_phone("5551234") # => 555-1234 # number_to_phone(1235551234) # => 123-555-1234 # number_to_phone(1235551234, :area_code => true) # => (123) 555-1234 # number_to_phone(1235551234, :delimiter => " ") # => 123 555 1234 # number_to_phone(1235551234, :area_code => true, :extension => 555) # => (123) 555-1234 x 555 # number_to_phone(1235551234, :country_code => 1) # => +1-123-555-1234 + # number_to_phone("123a456") # => 123a456 + # + # number_to_phone("1234a567", :raise => true) # => InvalidNumberError # # number_to_phone(1235551234, :country_code => 1, :extension => 1343, :delimiter => ".") - # => +1.123.555.1234 x 1343 + # # => +1.123.555.1234 x 1343 def number_to_phone(number, options = {}) return unless number + options = options.symbolize_keys - begin - Float(number) - rescue ArgumentError, TypeError - raise InvalidNumberError, number - end if options[:raise] + parse_float(number, true) if options[:raise] number = number.to_s.strip - options = options.symbolize_keys area_code = options[:area_code] delimiter = options[:delimiter] || "-" extension = options[:extension] @@ -69,7 +71,7 @@ module ActionView number.gsub!(/(\d{1,3})(\d{3})(\d{4}$)/,"(\\1) \\2#{delimiter}\\3") else number.gsub!(/(\d{0,3})(\d{3})(\d{4})$/,"\\1#{delimiter}\\2#{delimiter}\\3") - number.slice!(0, 1) if number.starts_with?(delimiter) && !delimiter.blank? + number.slice!(0, 1) if number.start_with?(delimiter) && !delimiter.blank? end str = [] @@ -83,6 +85,7 @@ module ActionView # in the +options+ hash. # # ==== Options + # # * <tt>:locale</tt> - Sets the locale to be used for formatting (defaults to current locale). # * <tt>:precision</tt> - Sets the level of precision (defaults to 2). # * <tt>:unit</tt> - Sets the denomination of the currency (defaults to "$"). @@ -95,28 +98,32 @@ module ActionView # an hyphen to the formatted number given by <tt>:format</tt>). # Accepts the same fields than <tt>:format</tt>, except # <tt>%n</tt> is here the absolute value of the number. + # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when the argument is invalid. # # ==== Examples + # # number_to_currency(1234567890.50) # => $1,234,567,890.50 # number_to_currency(1234567890.506) # => $1,234,567,890.51 # number_to_currency(1234567890.506, :precision => 3) # => $1,234,567,890.506 - # number_to_currency(1234567890.506, :locale => :fr) # => 1 234 567 890,506 € + # number_to_currency(1234567890.506, :locale => :fr) # => 1 234 567 890,51 € + # number_to_currency("123a456") # => $123a456 + # + # number_to_currency("123a456", :raise => true) # => InvalidNumberError # # number_to_currency(-1234567890.50, :negative_format => "(%u%n)") - # # => ($1,234,567,890.51) + # # => ($1,234,567,890.50) # number_to_currency(1234567890.50, :unit => "£", :separator => ",", :delimiter => "") # # => £1234567890,50 # number_to_currency(1234567890.50, :unit => "£", :separator => ",", :delimiter => "", :format => "%n %u") # # => 1234567890,50 £ def number_to_currency(number, options = {}) return unless number + options = options.symbolize_keys - options.symbolize_keys! + currency = translations_for('currency', options[:locale]) + currency[:negative_format] ||= "-" + currency[:format] if currency[:format] - defaults = I18n.translate(:'number.format', :locale => options[:locale], :default => {}) - currency = I18n.translate(:'number.currency.format', :locale => options[:locale], :default => {}) - - defaults = DEFAULT_CURRENCY_VALUES.merge(defaults).merge!(currency) + defaults = DEFAULT_CURRENCY_VALUES.merge(defaults_translations(options[:locale])).merge!(currency) defaults[:negative_format] = "-" + options[:format] if options[:format] options = defaults.merge!(options) @@ -139,44 +146,56 @@ module ActionView e.number.to_s.html_safe? ? formatted_number.html_safe : formatted_number end end - end - # Formats a +number+ as a percentage string (e.g., 65%). You can customize the - # format in the +options+ hash. + # Formats a +number+ as a percentage string (e.g., 65%). You can customize the format in the +options+ hash. # # ==== Options - # * <tt>:locale</tt> - Sets the locale to be used for formatting (defaults to current locale). - # * <tt>:precision</tt> - Sets the precision of the number (defaults to 3). - # * <tt>:significant</tt> - If +true+, precision will be the # of significant_digits. If +false+, the # of fractional digits (defaults to +false+) - # * <tt>:separator</tt> - Sets the separator between the fractional and integer digits (defaults to "."). - # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults to ""). - # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes insignificant zeros after the decimal separator (defaults to +false+) + # + # * <tt>:locale</tt> - Sets the locale to be used for formatting (defaults to current + # locale). + # * <tt>:precision</tt> - Sets the precision of the number (defaults to 3). + # * <tt>:significant</tt> - If +true+, precision will be the # of significant_digits. If +false+, + # the # of fractional digits (defaults to +false+). + # * <tt>:separator</tt> - Sets the separator between the fractional and integer digits (defaults + # to "."). + # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults to ""). + # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes insignificant zeros after the decimal separator + # (defaults to +false+). + # * <tt>:format</tt> - Specifies the format of the percentage string + # The number field is <tt>%n</tt> (defaults to "%n%"). + # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when the argument is invalid. # # ==== Examples + # # number_to_percentage(100) # => 100.000% + # number_to_percentage("98") # => 98.000% # number_to_percentage(100, :precision => 0) # => 100% # number_to_percentage(1000, :delimiter => '.', :separator => ',') # => 1.000,000% # number_to_percentage(302.24398923423, :precision => 5) # => 302.24399% # number_to_percentage(1000, :locale => :fr) # => 1 000,000% + # number_to_percentage("98a") # => 98a% + # number_to_percentage(100, :format => "%n %") # => 100 % + # + # number_to_percentage("98a", :raise => true) # => InvalidNumberError def number_to_percentage(number, options = {}) return unless number + options = options.symbolize_keys - options.symbolize_keys! - - defaults = I18n.translate(:'number.format', :locale => options[:locale], :default => {}) - percentage = I18n.translate(:'number.percentage.format', :locale => options[:locale], :default => {}) - defaults = defaults.merge(percentage) + defaults = format_translations('percentage', options[:locale]) + options = defaults.merge!(options) - options = options.reverse_merge(defaults) + format = options[:format] || "%n%" begin - "#{number_with_precision(number, options.merge(:raise => true))}%".html_safe + value = number_with_precision(number, options.merge(:raise => true)) + format.gsub(/%n/, value).html_safe rescue InvalidNumberError => e if options[:raise] raise else - e.number.to_s.html_safe? ? "#{e.number}%".html_safe : "#{e.number}%" + formatted_number = format.gsub(/%n/, e.number) + e.number.to_s.html_safe? ? formatted_number.html_safe : formatted_number end end end @@ -185,38 +204,36 @@ module ActionView # customize the format in the +options+ hash. # # ==== Options + # # * <tt>:locale</tt> - Sets the locale to be used for formatting (defaults to current locale). # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults to ","). # * <tt>:separator</tt> - Sets the separator between the fractional and integer digits (defaults to "."). + # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when the argument is invalid. # # ==== Examples + # # number_with_delimiter(12345678) # => 12,345,678 + # number_with_delimiter("123456") # => 123,456 # number_with_delimiter(12345678.05) # => 12,345,678.05 # number_with_delimiter(12345678, :delimiter => ".") # => 12.345.678 - # number_with_delimiter(12345678, :separator => ",") # => 12,345,678 + # number_with_delimiter(12345678, :delimiter => ",") # => 12,345,678 + # number_with_delimiter(12345678.05, :separator => " ") # => 12,345,678 05 # number_with_delimiter(12345678.05, :locale => :fr) # => 12 345 678,05 + # number_with_delimiter("112a") # => 112a # number_with_delimiter(98765432.98, :delimiter => " ", :separator => ",") # # => 98 765 432,98 + # + # number_with_delimiter("112a", :raise => true) # => raise InvalidNumberError def number_with_delimiter(number, options = {}) - options.symbolize_keys! + options = options.symbolize_keys - begin - Float(number) - rescue ArgumentError, TypeError - if options[:raise] - raise InvalidNumberError, number - else - return number - end - end + parse_float(number, options[:raise]) or return number - defaults = I18n.translate(:'number.format', :locale => options[:locale], :default => {}) - options = options.reverse_merge(defaults) + options = defaults_translations(options[:locale]).merge(options) parts = number.to_s.to_str.split('.') parts[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{options[:delimiter]}") parts.join(options[:separator]).html_safe - end # Formats a +number+ with the specified level of <tt>:precision</tt> (e.g., 112.32 has a precision @@ -224,12 +241,16 @@ module ActionView # You can customize the format in the +options+ hash. # # ==== Options - # * <tt>:locale</tt> - Sets the locale to be used for formatting (defaults to current locale). - # * <tt>:precision</tt> - Sets the precision of the number (defaults to 3). - # * <tt>:significant</tt> - If +true+, precision will be the # of significant_digits. If +false+, the # of fractional digits (defaults to +false+) - # * <tt>:separator</tt> - Sets the separator between the fractional and integer digits (defaults to "."). - # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults to ""). - # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes insignificant zeros after the decimal separator (defaults to +false+) + # * <tt>:locale</tt> - Sets the locale to be used for formatting (defaults to current locale). + # * <tt>:precision</tt> - Sets the precision of the number (defaults to 3). + # * <tt>:significant</tt> - If +true+, precision will be the # of significant_digits. If +false+, + # the # of fractional digits (defaults to +false+). + # * <tt>:separator</tt> - Sets the separator between the fractional and integer digits (defaults + # to "."). + # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults to ""). + # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes insignificant zeros after the decimal separator + # (defaults to +false+). + # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when the argument is invalid. # # ==== Examples # number_with_precision(111.2345) # => 111.235 @@ -240,29 +261,21 @@ module ActionView # number_with_precision(111.2345, :precision => 1, :significant => true) # => 100 # number_with_precision(13, :precision => 5, :significant => true) # => 13.000 # number_with_precision(111.234, :locale => :fr) # => 111,234 + # # number_with_precision(13, :precision => 5, :significant => true, :strip_insignificant_zeros => true) # # => 13 + # # number_with_precision(389.32314, :precision => 4, :significant => true) # => 389.3 # number_with_precision(1111.2345, :precision => 2, :separator => ',', :delimiter => '.') # # => 1.111,23 def number_with_precision(number, options = {}) - options.symbolize_keys! + options = options.symbolize_keys - number = begin - Float(number) - rescue ArgumentError, TypeError - if options[:raise] - raise InvalidNumberError, number - else - return number - end - end + number = (parse_float(number, options[:raise]) or return number) - defaults = I18n.translate(:'number.format', :locale => options[:locale], :default => {}) - precision_defaults = I18n.translate(:'number.precision.format', :locale => options[:locale], :default => {}) - defaults = defaults.merge(precision_defaults) + defaults = format_translations('precision', options[:locale]) + options = defaults.merge!(options) - options = options.reverse_merge(defaults) # Allow the user to unset default values: Eg.: :significant => false precision = options.delete :precision significant = options.delete :significant strip_insignificant_zeros = options.delete :strip_insignificant_zeros @@ -287,7 +300,6 @@ module ActionView else formatted_number end - end STORAGE_UNITS = [:byte, :kb, :mb, :gb, :tb].freeze @@ -307,6 +319,7 @@ module ActionView # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults to ""). # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes insignificant zeros after the decimal separator (defaults to +true+) # * <tt>:prefix</tt> - If +:si+ formats the number using the SI prefix (defaults to :binary) + # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when the argument is invalid. # ==== Examples # number_to_human_size(123) # => 123 Bytes # number_to_human_size(1234) # => 1.21 KB @@ -323,23 +336,13 @@ module ActionView # number_to_human_size(1234567890123, :precision => 5) # => "1.1229 TB" # number_to_human_size(524288000, :precision => 5) # => "500 MB" def number_to_human_size(number, options = {}) - options.symbolize_keys! + options = options.symbolize_keys - number = begin - Float(number) - rescue ArgumentError, TypeError - if options[:raise] - raise InvalidNumberError, number - else - return number - end - end + number = (parse_float(number, options[:raise]) or return number) - defaults = I18n.translate(:'number.format', :locale => options[:locale], :default => {}) - human = I18n.translate(:'number.human.format', :locale => options[:locale], :default => {}) - defaults = defaults.merge(human) + defaults = format_translations('human', options[:locale]) + options = defaults.merge!(options) - options = options.reverse_merge(defaults) #for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files options[:strip_insignificant_zeros] = true if not options.key?(:strip_insignificant_zeros) @@ -388,6 +391,7 @@ module ActionView # * *integers*: <tt>:unit</tt>, <tt>:ten</tt>, <tt>:hundred</tt>, <tt>:thousand</tt>, <tt>:million</tt>, <tt>:billion</tt>, <tt>:trillion</tt>, <tt>:quadrillion</tt> # * *fractionals*: <tt>:deci</tt>, <tt>:centi</tt>, <tt>:mili</tt>, <tt>:micro</tt>, <tt>:nano</tt>, <tt>:pico</tt>, <tt>:femto</tt> # * <tt>:format</tt> - Sets the format of the output string (defaults to "%n %u"). The field types are: + # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when the argument is invalid. # # %u The quantifier (ex.: 'thousand') # %n The number @@ -442,23 +446,13 @@ module ActionView # number_to_human(0.34, :units => :distance) # => "34 centimeters" # def number_to_human(number, options = {}) - options.symbolize_keys! + options = options.symbolize_keys - number = begin - Float(number) - rescue ArgumentError, TypeError - if options[:raise] - raise InvalidNumberError, number - else - return number - end - end + number = (parse_float(number, options[:raise]) or return number) - defaults = I18n.translate(:'number.format', :locale => options[:locale], :default => {}) - human = I18n.translate(:'number.human.format', :locale => options[:locale], :default => {}) - defaults = defaults.merge(human) + defaults = format_translations('human', options[:locale]) + options = defaults.merge!(options) - options = options.reverse_merge(defaults) #for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files options[:strip_insignificant_zeros] = true if not options.key?(:strip_insignificant_zeros) @@ -494,6 +488,25 @@ module ActionView decimal_format.gsub(/%n/, formatted_number).gsub(/%u/, unit).strip.html_safe end + private + + def format_translations(namespace, locale) + defaults_translations(locale).merge(translations_for(namespace, locale)) + end + + def defaults_translations(locale) + I18n.translate(:'number.format', :locale => locale, :default => {}) + end + + def translations_for(namespace, locale) + I18n.translate(:"number.#{namespace}.format", :locale => locale, :default => {}) + end + + def parse_float(number, raise_error) + Float(number) + rescue ArgumentError, TypeError + raise InvalidNumberError, number if raise_error + end end end end diff --git a/actionpack/lib/action_view/helpers/output_safety_helper.rb b/actionpack/lib/action_view/helpers/output_safety_helper.rb index a035dd70ad..2e7e9dc50c 100644 --- a/actionpack/lib/action_view/helpers/output_safety_helper.rb +++ b/actionpack/lib/action_view/helpers/output_safety_helper.rb @@ -28,11 +28,10 @@ module ActionView #:nodoc: # # => "<p>foo</p><br /><p>bar</p>" # def safe_join(array, sep=$,) - sep ||= "".html_safe sep = ERB::Util.html_escape(sep) array.map { |i| ERB::Util.html_escape(i) }.join(sep).html_safe end end end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_view/helpers/record_tag_helper.rb b/actionpack/lib/action_view/helpers/record_tag_helper.rb index b351302d01..1a15459406 100644 --- a/actionpack/lib/action_view/helpers/record_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/record_tag_helper.rb @@ -81,13 +81,11 @@ module ActionView # <li id="person_123" class="person bar">... # 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 + options, prefix = prefix, nil if prefix.is_a?(Hash) + + Array(single_or_multiple_records).map do |single_record| + content_tag_for_single_record(tag_name, single_record, prefix, options, &block) + end.join("\n").html_safe end private @@ -95,14 +93,11 @@ module ActionView # 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 + options = options ? options.dup : {} + options.merge!(:class => "#{dom_class(record, prefix)} #{options[:class]}".rstrip, :id => dom_id(record, prefix)) + + content = block.arity == 0 ? capture(&block) : capture(record, &block) + content_tag(tag_name, content, options) end end end diff --git a/actionpack/lib/action_view/helpers/sanitize_helper.rb b/actionpack/lib/action_view/helpers/sanitize_helper.rb index bcc8f6fbcb..7768c8c151 100644 --- a/actionpack/lib/action_view/helpers/sanitize_helper.rb +++ b/actionpack/lib/action_view/helpers/sanitize_helper.rb @@ -1,6 +1,5 @@ require 'active_support/core_ext/object/try' require 'action_controller/vendor/html-scanner' -require 'action_view/helpers/tag_helper' module ActionView # = Action View Sanitize Helpers diff --git a/actionpack/lib/action_view/helpers/tag_helper.rb b/actionpack/lib/action_view/helpers/tag_helper.rb index 8c33ef09fa..ecd26891d6 100644 --- a/actionpack/lib/action_view/helpers/tag_helper.rb +++ b/actionpack/lib/action_view/helpers/tag_helper.rb @@ -118,38 +118,49 @@ module ActionView # escape_once("<< Accept & Checkout") # # => "<< Accept & Checkout" def escape_once(html) - ActiveSupport::Multibyte.clean(html.to_s).gsub(/[\"><]|&(?!([a-zA-Z]+|(#\d+));)/) { |special| ERB::Util::HTML_ESCAPE[special] } + ERB::Util.html_escape_once(html) end private def content_tag_string(name, content, options, escape = true) tag_options = tag_options(options, escape) if options - "<#{name}#{tag_options}>#{escape ? ERB::Util.h(content) : content}</#{name}>".html_safe + content = ERB::Util.h(content) if escape + "<#{name}#{tag_options}>#{content}</#{name}>".html_safe end def tag_options(options, escape = true) - unless options.blank? - attrs = [] - options.each_pair do |key, value| - if key.to_s == 'data' && value.is_a?(Hash) - value.each do |k, v| - if !v.is_a?(String) && !v.is_a?(Symbol) - v = v.to_json - end - v = ERB::Util.html_escape(v) if escape - attrs << %(data-#{k.to_s.dasherize}="#{v}") - end - elsif BOOLEAN_ATTRIBUTES.include?(key) - attrs << %(#{key}="#{key}") if value - elsif !value.nil? - final_value = value.is_a?(Array) ? value.join(" ") : value - final_value = ERB::Util.html_escape(final_value) if escape - attrs << %(#{key}="#{final_value}") + return if options.blank? + attrs = [] + options.each_pair do |key, value| + if key.to_s == 'data' && value.is_a?(Hash) + value.each_pair do |k, v| + attrs << data_tag_option(k, v, escape) end + elsif BOOLEAN_ATTRIBUTES.include?(key) + attrs << boolean_tag_option(key) if value + elsif !value.nil? + attrs << tag_option(key, value, escape) end - " #{attrs.sort * ' '}".html_safe unless attrs.empty? end + " #{attrs.sort * ' '}".html_safe unless attrs.empty? + end + + def data_tag_option(key, value, escape) + key = "data-#{key.to_s.dasherize}" + value = value.to_json if !value.is_a?(String) && !value.is_a?(Symbol) + + tag_option(key, value, escape) + end + + def boolean_tag_option(key) + %(#{key}="#{key}") + end + + def tag_option(key, value, escape) + value = value.join(" ") if value.is_a?(Array) + value = ERB::Util.h(value) if escape + %(#{key}="#{value}") end end end diff --git a/actionpack/lib/action_view/helpers/tags.rb b/actionpack/lib/action_view/helpers/tags.rb new file mode 100644 index 0000000000..3cf762877f --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags.rb @@ -0,0 +1,33 @@ +module ActionView + module Helpers + module Tags #:nodoc: + extend ActiveSupport::Autoload + + autoload :Base + autoload :CheckBox + autoload :CollectionCheckBoxes + autoload :CollectionRadioButtons + autoload :CollectionSelect + autoload :DateField + autoload :DateSelect + autoload :DatetimeSelect + autoload :EmailField + autoload :FileField + autoload :GroupedCollectionSelect + autoload :HiddenField + autoload :Label + autoload :NumberField + autoload :PasswordField + autoload :RadioButton + autoload :RangeField + autoload :SearchField + autoload :Select + autoload :TelField + autoload :TextArea + autoload :TextField + autoload :TimeSelect + autoload :TimeZoneSelect + autoload :UrlField + end + end +end diff --git a/actionpack/lib/action_view/helpers/tags/base.rb b/actionpack/lib/action_view/helpers/tags/base.rb new file mode 100644 index 0000000000..1ece0ad2fc --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags/base.rb @@ -0,0 +1,147 @@ +module ActionView + module Helpers + module Tags + class Base #:nodoc: + include Helpers::ActiveModelInstanceTag, Helpers::TagHelper, Helpers::FormTagHelper + include FormOptionsHelper + + DEFAULT_FIELD_OPTIONS = { "size" => 30 } + + attr_reader :object + + def initialize(object_name, method_name, template_object, options = {}) + @object_name, @method_name = object_name.to_s.dup, method_name.to_s.dup + @template_object = template_object + + @object_name.sub!(/\[\]$/,"") || @object_name.sub!(/\[\]\]$/,"]") + @object = retrieve_object(options.delete(:object)) + @options = options + @auto_index = retrieve_autoindex(Regexp.last_match.pre_match) if Regexp.last_match + end + + # This is what child classes implement. + def render + raise NotImplementedError, "Subclasses must implement a render method" + end + + private + + def value(object) + object.send @method_name if object + end + + def value_before_type_cast(object) + unless object.nil? + method_before_type_cast = @method_name + "_before_type_cast" + + object.respond_to?(method_before_type_cast) ? + object.send(method_before_type_cast) : + value(object) + end + end + + def retrieve_object(object) + if object + object + elsif @template_object.instance_variable_defined?("@#{@object_name}") + @template_object.instance_variable_get("@#{@object_name}") + end + rescue NameError + # As @object_name may contain the nested syntax (item[subobject]) we need to fallback to nil. + nil + end + + def retrieve_autoindex(pre_match) + object = self.object || @template_object.instance_variable_get("@#{pre_match}") + if object && object.respond_to?(:to_param) + object.to_param + else + raise ArgumentError, "object[] naming but object param and @object var don't exist or don't respond to to_param: #{object.inspect}" + end + end + + def add_default_name_and_id_for_value(tag_value, options) + if tag_value.nil? + add_default_name_and_id(options) + else + specified_id = options["id"] + add_default_name_and_id(options) + + if specified_id.blank? && options["id"].present? + options["id"] += "_#{sanitized_value(tag_value)}" + end + end + end + + def add_default_name_and_id(options) + if options.has_key?("index") + options["name"] ||= tag_name_with_index(options["index"]) + options["id"] = options.fetch("id"){ tag_id_with_index(options["index"]) } + options.delete("index") + elsif defined?(@auto_index) + options["name"] ||= tag_name_with_index(@auto_index) + options["id"] = options.fetch("id"){ tag_id_with_index(@auto_index) } + else + options["name"] ||= options['multiple'] ? tag_name_multiple : tag_name + options["id"] = options.fetch("id"){ tag_id } + end + options["id"] = [options.delete('namespace'), options["id"]].compact.join("_").presence + end + + def tag_name + "#{@object_name}[#{sanitized_method_name}]" + end + + def tag_name_multiple + "#{tag_name}[]" + end + + def tag_name_with_index(index) + "#{@object_name}[#{index}][#{sanitized_method_name}]" + end + + def tag_id + "#{sanitized_object_name}_#{sanitized_method_name}" + end + + def tag_id_with_index(index) + "#{sanitized_object_name}_#{index}_#{sanitized_method_name}" + end + + def sanitized_object_name + @sanitized_object_name ||= @object_name.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "") + end + + def sanitized_method_name + @sanitized_method_name ||= @method_name.sub(/\?$/,"") + end + + def sanitized_value(value) + value.to_s.gsub(/\s/, "_").gsub(/[^-\w]/, "").downcase + 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 + + def add_options(option_tags, options, value = nil) + if options[:include_blank] + option_tags = "<option value=\"\">#{ERB::Util.html_escape(options[:include_blank]) if options[:include_blank].kind_of?(String)}</option>\n" + option_tags + end + if value.blank? && options[:prompt] + prompt = options[:prompt].kind_of?(String) ? options[:prompt] : I18n.translate('helpers.select.prompt', :default => 'Please select') + option_tags = "<option value=\"\">#{ERB::Util.html_escape(prompt)}</option>\n" + option_tags + end + option_tags.html_safe + end + end + end + end +end diff --git a/actionpack/lib/action_view/helpers/tags/check_box.rb b/actionpack/lib/action_view/helpers/tags/check_box.rb new file mode 100644 index 0000000000..579cdb9fc9 --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags/check_box.rb @@ -0,0 +1,58 @@ +require 'action_view/helpers/tags/checkable' + +module ActionView + module Helpers + module Tags + class CheckBox < Base #:nodoc: + include Checkable + + def initialize(object_name, method_name, template_object, checked_value, unchecked_value, options) + @checked_value = checked_value + @unchecked_value = unchecked_value + super(object_name, method_name, template_object, options) + end + + def render + options = @options.stringify_keys + options["type"] = "checkbox" + options["value"] = @checked_value + options["checked"] = "checked" if input_checked?(object, options) + + if options["multiple"] + add_default_name_and_id_for_value(@checked_value, options) + options.delete("multiple") + else + add_default_name_and_id(options) + end + + hidden = hidden_field_for_checkbox(options) + checkbox = tag("input", options) + hidden + checkbox + end + + private + + def checked?(value) + case value + when TrueClass, FalseClass + value + when NilClass + false + when Integer + value != 0 + when String + value == @checked_value + when Array + value.include?(@checked_value) + else + value.to_i != 0 + end + end + + def hidden_field_for_checkbox(options) + @unchecked_value ? tag("input", options.slice("name", "disabled", "form").merge!("type" => "hidden", "value" => @unchecked_value)) : "".html_safe + end + end + end + end +end diff --git a/actionpack/lib/action_view/helpers/tags/checkable.rb b/actionpack/lib/action_view/helpers/tags/checkable.rb new file mode 100644 index 0000000000..b97c0c68d7 --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags/checkable.rb @@ -0,0 +1,16 @@ +module ActionView + module Helpers + module Tags + module Checkable + def input_checked?(object, options) + if options.has_key?("checked") + checked = options.delete "checked" + checked == true || checked == "checked" + else + checked?(value(object)) + end + end + end + end + end +end diff --git a/actionpack/lib/action_view/helpers/tags/collection_check_boxes.rb b/actionpack/lib/action_view/helpers/tags/collection_check_boxes.rb new file mode 100644 index 0000000000..e23f5113fb --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags/collection_check_boxes.rb @@ -0,0 +1,37 @@ +require 'action_view/helpers/tags/collection_helpers' + +module ActionView + module Helpers + module Tags + class CollectionCheckBoxes < Base + include CollectionHelpers + + class CheckBoxBuilder < Builder + def check_box(extra_html_options={}) + html_options = extra_html_options.merge(@input_html_options) + @template_object.check_box(@object_name, @method_name, html_options, @value, nil) + end + end + + def render + rendered_collection = render_collection do |item, value, text, default_html_options| + default_html_options[:multiple] = true + builder = instantiate_builder(CheckBoxBuilder, item, value, text, default_html_options) + + if block_given? + yield builder + else + builder.check_box + builder.label + end + end + + # Append a hidden field to make sure something will be sent back to the + # server if all check boxes are unchecked. + hidden = @template_object.hidden_field_tag(tag_name_multiple, "", :id => nil) + + rendered_collection + hidden + end + end + end + end +end diff --git a/actionpack/lib/action_view/helpers/tags/collection_helpers.rb b/actionpack/lib/action_view/helpers/tags/collection_helpers.rb new file mode 100644 index 0000000000..6f950e552a --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags/collection_helpers.rb @@ -0,0 +1,81 @@ +module ActionView + module Helpers + module Tags + module CollectionHelpers + class Builder + attr_reader :object, :text, :value + + def initialize(template_object, object_name, method_name, object, + sanitized_attribute_name, text, value, input_html_options) + @template_object = template_object + @object_name = object_name + @method_name = method_name + @object = object + @sanitized_attribute_name = sanitized_attribute_name + @text = text + @value = value + @input_html_options = input_html_options + end + + def label(label_html_options={}, &block) + @template_object.label(@object_name, @sanitized_attribute_name, @text, label_html_options, &block) + end + end + + def initialize(object_name, method_name, template_object, collection, value_method, text_method, options, html_options) + @collection = collection + @value_method = value_method + @text_method = text_method + @html_options = html_options + + super(object_name, method_name, template_object, options) + end + + private + + def instantiate_builder(builder_class, item, value, text, html_options) + builder_class.new(@template_object, @object_name, @method_name, item, + sanitize_attribute_name(value), text, value, html_options) + end + + # Generate default options for collection helpers, such as :checked and + # :disabled. + def default_html_options_for_collection(item, value) #:nodoc: + html_options = @html_options.dup + + [:checked, :selected, :disabled].each do |option| + next unless current_value = @options[option] + + accept = if current_value.respond_to?(:call) + current_value.call(item) + else + Array(current_value).include?(value) + end + + if accept + html_options[option] = true + elsif option == :checked + html_options[option] = false + end + end + + html_options + end + + def sanitize_attribute_name(value) #:nodoc: + "#{sanitized_method_name}_#{sanitized_value(value)}" + end + + def render_collection #:nodoc: + @collection.map do |item| + value = value_for_collection(item, @value_method) + text = value_for_collection(item, @text_method) + default_html_options = default_html_options_for_collection(item, value) + + yield item, value, text, default_html_options + end.join.html_safe + end + end + end + end +end diff --git a/actionpack/lib/action_view/helpers/tags/collection_radio_buttons.rb b/actionpack/lib/action_view/helpers/tags/collection_radio_buttons.rb new file mode 100644 index 0000000000..ba2035f074 --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags/collection_radio_buttons.rb @@ -0,0 +1,30 @@ +require 'action_view/helpers/tags/collection_helpers' + +module ActionView + module Helpers + module Tags + class CollectionRadioButtons < Base + include CollectionHelpers + + class RadioButtonBuilder < Builder + def radio_button(extra_html_options={}) + html_options = extra_html_options.merge(@input_html_options) + @template_object.radio_button(@object_name, @method_name, @value, html_options) + end + end + + def render + render_collection do |item, value, text, default_html_options| + builder = instantiate_builder(RadioButtonBuilder, item, value, text, default_html_options) + + if block_given? + yield builder + else + builder.radio_button + builder.label + end + end + end + end + end + end +end diff --git a/actionpack/lib/action_view/helpers/tags/collection_select.rb b/actionpack/lib/action_view/helpers/tags/collection_select.rb new file mode 100644 index 0000000000..ec78e6e5f9 --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags/collection_select.rb @@ -0,0 +1,28 @@ +module ActionView + module Helpers + module Tags + class CollectionSelect < Base #:nodoc: + def initialize(object_name, method_name, template_object, collection, value_method, text_method, options, html_options) + @collection = collection + @value_method = value_method + @text_method = text_method + @html_options = html_options + + super(object_name, method_name, template_object, options) + end + + def render + option_tags_options = { + :selected => @options.fetch(:selected) { value(@object) }, + :disabled => @options[:disabled] + } + + select_content_tag( + options_from_collection_for_select(@collection, @value_method, @text_method, option_tags_options), + @options, @html_options + ) + end + end + end + end +end diff --git a/actionpack/lib/action_view/helpers/tags/date_field.rb b/actionpack/lib/action_view/helpers/tags/date_field.rb new file mode 100644 index 0000000000..bb968e9f39 --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags/date_field.rb @@ -0,0 +1,15 @@ +module ActionView + module Helpers + module Tags + class DateField < TextField #:nodoc: + def render + options = @options.stringify_keys + options["value"] = @options.fetch("value") { value(object).try(:to_date) } + options["size"] = nil + @options = options + super + end + end + end + end +end diff --git a/actionpack/lib/action_view/helpers/tags/date_select.rb b/actionpack/lib/action_view/helpers/tags/date_select.rb new file mode 100644 index 0000000000..5d706087b0 --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags/date_select.rb @@ -0,0 +1,70 @@ +module ActionView + module Helpers + module Tags + class DateSelect < Base #:nodoc: + def initialize(object_name, method_name, template_object, options, html_options) + @html_options = html_options + + super(object_name, method_name, template_object, options) + end + + def render + error_wrapping(datetime_selector(@options, @html_options).send("select_#{select_type}").html_safe) + end + + class << self + def select_type + @select_type ||= self.name.split("::").last.sub("Select", "").downcase + end + end + + private + + def select_type + self.class.select_type + end + + def datetime_selector(options, html_options) + datetime = value(object) || default_datetime(options) + @auto_index ||= nil + + options = options.dup + options[:field_name] = @method_name + options[:include_position] = true + options[:prefix] ||= @object_name + options[:index] = @auto_index if @auto_index && !options.has_key?(:index) + + DateTimeSelector.new(datetime, options, html_options) + end + + def default_datetime(options) + return if options[:include_blank] || options[:prompt] + + case options[:default] + when nil + Time.current + when Date, Time + options[:default] + else + default = options[:default].dup + + # Rename :minute and :second to :min and :sec + default[:min] ||= default[:minute] + default[:sec] ||= default[:second] + + time = Time.current + + [:year, :month, :day, :hour, :min, :sec].each do |key| + default[key] ||= time.send(key) + end + + Time.utc_time( + default[:year], default[:month], default[:day], + default[:hour], default[:min], default[:sec] + ) + end + end + end + end + end +end diff --git a/actionpack/lib/action_view/helpers/tags/datetime_select.rb b/actionpack/lib/action_view/helpers/tags/datetime_select.rb new file mode 100644 index 0000000000..a32c840bce --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags/datetime_select.rb @@ -0,0 +1,8 @@ +module ActionView + module Helpers + module Tags + class DatetimeSelect < DateSelect #:nodoc: + end + end + end +end diff --git a/actionpack/lib/action_view/helpers/tags/email_field.rb b/actionpack/lib/action_view/helpers/tags/email_field.rb new file mode 100644 index 0000000000..45cde507d7 --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags/email_field.rb @@ -0,0 +1,8 @@ +module ActionView + module Helpers + module Tags + class EmailField < TextField #:nodoc: + end + end + end +end diff --git a/actionpack/lib/action_view/helpers/tags/file_field.rb b/actionpack/lib/action_view/helpers/tags/file_field.rb new file mode 100644 index 0000000000..56442e1c14 --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags/file_field.rb @@ -0,0 +1,12 @@ +module ActionView + module Helpers + module Tags + class FileField < TextField #:nodoc: + def render + @options.update(:size => nil) + super + end + end + end + end +end diff --git a/actionpack/lib/action_view/helpers/tags/grouped_collection_select.rb b/actionpack/lib/action_view/helpers/tags/grouped_collection_select.rb new file mode 100644 index 0000000000..507466a57a --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags/grouped_collection_select.rb @@ -0,0 +1,24 @@ +module ActionView + module Helpers + module Tags + class GroupedCollectionSelect < Base #:nodoc: + def initialize(object_name, method_name, template_object, collection, group_method, group_label_method, option_key_method, option_value_method, options, html_options) + @collection = collection + @group_method = group_method + @group_label_method = group_label_method + @option_key_method = option_key_method + @option_value_method = option_value_method + @html_options = html_options + + super(object_name, method_name, template_object, options) + end + + def render + 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 + end + end + end +end diff --git a/actionpack/lib/action_view/helpers/tags/hidden_field.rb b/actionpack/lib/action_view/helpers/tags/hidden_field.rb new file mode 100644 index 0000000000..ea86596e0b --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags/hidden_field.rb @@ -0,0 +1,12 @@ +module ActionView + module Helpers + module Tags + class HiddenField < TextField #:nodoc: + def render + @options.update(:size => nil) + super + end + end + end + end +end diff --git a/actionpack/lib/action_view/helpers/tags/label.rb b/actionpack/lib/action_view/helpers/tags/label.rb new file mode 100644 index 0000000000..1bd71c2778 --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags/label.rb @@ -0,0 +1,65 @@ +module ActionView + module Helpers + module Tags + class Label < Base #:nodoc: + def initialize(object_name, method_name, template_object, content_or_options = nil, options = nil) + content_is_options = content_or_options.is_a?(Hash) + if content_is_options + options = content_or_options + @content = nil + else + @content = content_or_options + end + + options ||= {} + + super(object_name, method_name, template_object, options) + end + + def render(&block) + options = @options.stringify_keys + tag_value = options.delete("value") + name_and_id = options.dup + + if name_and_id["for"] + name_and_id["id"] = name_and_id["for"] + else + name_and_id.delete("id") + end + + add_default_name_and_id_for_value(tag_value, name_and_id) + options.delete("index") + options.delete("namespace") + options["for"] = name_and_id["id"] unless options.key?("for") + + if block_given? + @template_object.label_tag(name_and_id["id"], options, &block) + else + content = if @content.blank? + @object_name.gsub!(/\[(.*)_attributes\]\[\d\]/, '.\1') + method_and_value = tag_value.present? ? "#{@method_name}.#{tag_value}" : @method_name + + if object.respond_to?(:to_model) + key = object.class.model_name.i18n_key + i18n_default = ["#{key}.#{method_and_value}".to_sym, ""] + end + + i18n_default ||= "" + I18n.t("#{@object_name}.#{method_and_value}", :default => i18n_default, :scope => "helpers.label").presence + else + @content.to_s + end + + content ||= if object && object.class.respond_to?(:human_attribute_name) + object.class.human_attribute_name(@method_name) + end + + content ||= @method_name.humanize + + label_tag(name_and_id["id"], content, options) + end + end + end + end + end +end diff --git a/actionpack/lib/action_view/helpers/tags/number_field.rb b/actionpack/lib/action_view/helpers/tags/number_field.rb new file mode 100644 index 0000000000..e89fdbec46 --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags/number_field.rb @@ -0,0 +1,19 @@ +module ActionView + module Helpers + module Tags + class NumberField < TextField #:nodoc: + def render + options = @options.stringify_keys + options['size'] ||= nil + + if range = options.delete("in") || options.delete("within") + options.update("min" => range.min, "max" => range.max) + end + + @options = options + super + end + end + end + end +end diff --git a/actionpack/lib/action_view/helpers/tags/password_field.rb b/actionpack/lib/action_view/helpers/tags/password_field.rb new file mode 100644 index 0000000000..6e7a4d3c36 --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags/password_field.rb @@ -0,0 +1,12 @@ +module ActionView + module Helpers + module Tags + class PasswordField < TextField #:nodoc: + def render + @options = {:value => nil}.merge!(@options) + super + end + end + end + end +end diff --git a/actionpack/lib/action_view/helpers/tags/radio_button.rb b/actionpack/lib/action_view/helpers/tags/radio_button.rb new file mode 100644 index 0000000000..8a0421f061 --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags/radio_button.rb @@ -0,0 +1,31 @@ +require 'action_view/helpers/tags/checkable' + +module ActionView + module Helpers + module Tags + class RadioButton < Base #:nodoc: + include Checkable + + def initialize(object_name, method_name, template_object, tag_value, options) + @tag_value = tag_value + super(object_name, method_name, template_object, options) + end + + def render + options = @options.stringify_keys + options["type"] = "radio" + options["value"] = @tag_value + options["checked"] = "checked" if input_checked?(object, options) + add_default_name_and_id_for_value(@tag_value, options) + tag("input", options) + end + + private + + def checked?(value) + value.to_s == @tag_value.to_s + end + end + end + end +end diff --git a/actionpack/lib/action_view/helpers/tags/range_field.rb b/actionpack/lib/action_view/helpers/tags/range_field.rb new file mode 100644 index 0000000000..47db4680e7 --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags/range_field.rb @@ -0,0 +1,8 @@ +module ActionView + module Helpers + module Tags + class RangeField < NumberField #:nodoc: + end + end + end +end diff --git a/actionpack/lib/action_view/helpers/tags/search_field.rb b/actionpack/lib/action_view/helpers/tags/search_field.rb new file mode 100644 index 0000000000..818fd4b887 --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags/search_field.rb @@ -0,0 +1,24 @@ +module ActionView + module Helpers + module Tags + class SearchField < TextField #:nodoc: + def render + options = @options.stringify_keys + + if options["autosave"] + if options["autosave"] == true + options["autosave"] = request.host.split(".").reverse.join(".") + end + options["results"] ||= 10 + end + + if options["onsearch"] + options["incremental"] = true unless options.has_key?("incremental") + end + + super + end + end + end + end +end diff --git a/actionpack/lib/action_view/helpers/tags/select.rb b/actionpack/lib/action_view/helpers/tags/select.rb new file mode 100644 index 0000000000..53a108b7e6 --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags/select.rb @@ -0,0 +1,41 @@ +module ActionView + module Helpers + module Tags + class Select < Base #:nodoc: + def initialize(object_name, method_name, template_object, choices, options, html_options) + @choices = choices + @choices = @choices.to_a if @choices.is_a?(Range) + @html_options = html_options + + super(object_name, method_name, template_object, options) + end + + def render + option_tags_options = { + :selected => @options.fetch(:selected) { value(@object) }, + :disabled => @options[:disabled] + } + + option_tags = if grouped_choices? + grouped_options_for_select(@choices, option_tags_options) + else + options_for_select(@choices, option_tags_options) + end + + select_content_tag(option_tags, @options, @html_options) + end + + private + + # Grouped choices look like this: + # + # [nil, []] + # { nil => [] } + # + def grouped_choices? + !@choices.empty? && @choices.first.respond_to?(:last) && Array === @choices.first.last + end + end + end + end +end diff --git a/actionpack/lib/action_view/helpers/tags/tel_field.rb b/actionpack/lib/action_view/helpers/tags/tel_field.rb new file mode 100644 index 0000000000..87c1f6b6b6 --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags/tel_field.rb @@ -0,0 +1,8 @@ +module ActionView + module Helpers + module Tags + class TelField < TextField #:nodoc: + end + end + end +end diff --git a/actionpack/lib/action_view/helpers/tags/text_area.rb b/actionpack/lib/action_view/helpers/tags/text_area.rb new file mode 100644 index 0000000000..a7db8eb437 --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags/text_area.rb @@ -0,0 +1,20 @@ +module ActionView + module Helpers + module Tags + class TextArea < Base #:nodoc: + DEFAULT_TEXT_AREA_OPTIONS = { "cols" => 40, "rows" => 20 } + + def render + options = DEFAULT_TEXT_AREA_OPTIONS.merge(@options.stringify_keys) + add_default_name_and_id(options) + + if size = options.delete("size") + options["cols"], options["rows"] = size.split("x") if size.respond_to?(:split) + end + + content_tag("textarea", ERB::Util.html_escape(options.delete('value') || value_before_type_cast(object)), options) + end + end + end + end +end diff --git a/actionpack/lib/action_view/helpers/tags/text_field.rb b/actionpack/lib/action_view/helpers/tags/text_field.rb new file mode 100644 index 0000000000..ce5182d20f --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags/text_field.rb @@ -0,0 +1,30 @@ +module ActionView + module Helpers + module Tags + class TextField < Base #:nodoc: + def render + options = @options.stringify_keys + options["size"] = options["maxlength"] || DEFAULT_FIELD_OPTIONS["size"] unless options.key?("size") + options = DEFAULT_FIELD_OPTIONS.merge(options) + options["type"] ||= field_type + options["value"] = options.fetch("value"){ value_before_type_cast(object) } unless field_type == "file" + options["value"] &&= ERB::Util.html_escape(options["value"]) + add_default_name_and_id(options) + tag("input", options) + end + + class << self + def field_type + @field_type ||= self.name.split("::").last.sub("Field", "").downcase + end + end + + private + + def field_type + self.class.field_type + end + end + end + end +end diff --git a/actionpack/lib/action_view/helpers/tags/time_select.rb b/actionpack/lib/action_view/helpers/tags/time_select.rb new file mode 100644 index 0000000000..9e97deb706 --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags/time_select.rb @@ -0,0 +1,8 @@ +module ActionView + module Helpers + module Tags + class TimeSelect < DateSelect #:nodoc: + end + end + end +end diff --git a/actionpack/lib/action_view/helpers/tags/time_zone_select.rb b/actionpack/lib/action_view/helpers/tags/time_zone_select.rb new file mode 100644 index 0000000000..0a176157c3 --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags/time_zone_select.rb @@ -0,0 +1,20 @@ +module ActionView + module Helpers + module Tags + class TimeZoneSelect < Base #:nodoc: + def initialize(object_name, method_name, template_object, priority_zones, options, html_options) + @priority_zones = priority_zones + @html_options = html_options + + super(object_name, method_name, template_object, options) + end + + def render + select_content_tag( + time_zone_options_for_select(value(@object) || @options[:default], @priority_zones, @options[:model] || ActiveSupport::TimeZone), @options, @html_options + ) + end + end + end + end +end diff --git a/actionpack/lib/action_view/helpers/tags/url_field.rb b/actionpack/lib/action_view/helpers/tags/url_field.rb new file mode 100644 index 0000000000..1ffdfe0b3c --- /dev/null +++ b/actionpack/lib/action_view/helpers/tags/url_field.rb @@ -0,0 +1,8 @@ +module ActionView + module Helpers + module Tags + class UrlField < TextField #:nodoc: + end + end + end +end diff --git a/actionpack/lib/action_view/helpers/text_helper.rb b/actionpack/lib/action_view/helpers/text_helper.rb index 21074efe86..3dc651501e 100644 --- a/actionpack/lib/action_view/helpers/text_helper.rb +++ b/actionpack/lib/action_view/helpers/text_helper.rb @@ -1,6 +1,5 @@ require 'active_support/core_ext/object/blank' require 'active_support/core_ext/string/filters' -require 'action_view/helpers/tag_helper' module ActionView # = Action View Text Helpers @@ -31,6 +30,7 @@ module ActionView extend ActiveSupport::Concern include SanitizeHelper + include TagHelper # The preferred method of outputting text in your views is to use the # <%= "text" %> eRuby syntax. The regular _puts_ and _print_ methods # do not operate as expected in an eRuby code block. If you absolutely must @@ -90,11 +90,11 @@ module ActionView # Highlights one or more +phrases+ everywhere in +text+ by inserting it into # a <tt>:highlighter</tt> string. The highlighter can be specialized by passing <tt>:highlighter</tt> # as a single-quoted string with \1 where the phrase is to be inserted (defaults to - # '<strong class="highlight">\1</strong>') + # '<mark>\1</mark>') # # ==== Examples # highlight('You searched for: rails', 'rails') - # # => You searched for: <strong class="highlight">rails</strong> + # # => You searched for: <mark>rails</mark> # # highlight('You searched for: ruby, rails, dhh', 'actionpack') # # => You searched for: ruby, rails, dhh @@ -111,16 +111,16 @@ module ActionView def highlight(text, phrases, *args) options = args.extract_options! unless args.empty? - options[:highlighter] = args[0] || '<strong class="highlight">\1</strong>' + options[:highlighter] = args[0] || '<mark>\1</mark>' end - options.reverse_merge!(:highlighter => '<strong class="highlight">\1</strong>') + options.reverse_merge!(:highlighter => '<mark>\1</mark>') text = sanitize(text) unless options[:sanitize] == false if text.blank? || phrases.blank? text else match = Array(phrases).map { |p| Regexp.escape(p) }.join('|') - text.gsub(/(#{match})(?!(?:[^<]*?)(?:["'])[^<>]*>)/i, options[:highlighter]) + text.gsub(/(#{match})(?![^<]*?>)/i, options[:highlighter]) end.html_safe end @@ -162,15 +162,15 @@ module ActionView options.reverse_merge!(:radius => 100, :omission => "...") phrase = Regexp.escape(phrase) - return unless found_pos = text.mb_chars =~ /(#{phrase})/i + return unless found_pos = text =~ /(#{phrase})/i start_pos = [ found_pos - options[:radius], 0 ].max - end_pos = [ [ found_pos + phrase.mb_chars.length + options[:radius] - 1, 0].max, text.mb_chars.length ].min + end_pos = [ [ found_pos + phrase.length + options[:radius] - 1, 0].max, text.length ].min prefix = start_pos > 0 ? options[:omission] : "" - postfix = end_pos < text.mb_chars.length - 1 ? options[:omission] : "" + postfix = end_pos < text.length - 1 ? options[:omission] : "" - prefix + text.mb_chars[start_pos..end_pos].strip + postfix + prefix + text[start_pos..end_pos].strip + postfix end # Attempts to pluralize the +singular+ word unless +count+ is 1. If diff --git a/actionpack/lib/action_view/helpers/translation_helper.rb b/actionpack/lib/action_view/helpers/translation_helper.rb index be64dc823e..cc74eff53a 100644 --- a/actionpack/lib/action_view/helpers/translation_helper.rb +++ b/actionpack/lib/action_view/helpers/translation_helper.rb @@ -45,11 +45,18 @@ module ActionView # you know what kind of output to expect when you call translate in a template. def translate(key, options = {}) options.merge!(:rescue_format => :html) unless options.key?(:rescue_format) - translation = I18n.translate(scope_key_by_partial(key), options) - if html_safe_translation_key?(key) && translation.respond_to?(:html_safe) - translation.html_safe + if html_safe_translation_key?(key) + html_safe_options = options.dup + options.except(*I18n::RESERVED_KEYS).each do |name, value| + unless name == :count && value.is_a?(Numeric) + html_safe_options[name] = ERB::Util.html_escape(value.to_s) + end + end + translation = I18n.translate(scope_key_by_partial(key), html_safe_options) + + translation.respond_to?(:html_safe) ? translation.html_safe : translation else - translation + I18n.translate(scope_key_by_partial(key), options) end end alias :t :translate diff --git a/actionpack/lib/action_view/helpers/url_helper.rb b/actionpack/lib/action_view/helpers/url_helper.rb index 0c2e1aa3a9..b5fc882e31 100644 --- a/actionpack/lib/action_view/helpers/url_helper.rb +++ b/actionpack/lib/action_view/helpers/url_helper.rb @@ -97,12 +97,12 @@ module ActionView # <%= url_for(:back) %> # # if request.env["HTTP_REFERER"] is not set or is blank # # => javascript:history.back() - def url_for(options = {}) - options ||= {} + def url_for(options = nil) case options when String options - when Hash + when nil, Hash + options ||= {} options = options.symbolize_keys.reverse_merge!(:only_path => options[:host].nil?) super when :back @@ -301,7 +301,7 @@ module ActionView # # <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"> @@ -323,34 +323,25 @@ module ActionView # # def button_to(name, options = {}, html_options = {}) html_options = html_options.stringify_keys - convert_boolean_attributes!(html_options, %w( disabled )) + convert_boolean_attributes!(html_options, %w(disabled)) - method_tag = '' - if (method = html_options.delete('method')) && %w{put delete}.include?(method.to_s) - method_tag = tag('input', :type => 'hidden', :name => '_method', :value => method.to_s) - end + url = options.is_a?(String) ? options : url_for(options) + remote = html_options.delete('remote') + + method = html_options.delete('method').to_s + method_tag = %w{put delete}.include?(method) ? method_tag(method) : "" - form_method = method.to_s == 'get' ? 'get' : 'post' + form_method = method == 'get' ? 'get' : 'post' 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) - end + form_options.merge!(:method => form_method, :action => url) + form_options.merge!("data-remote" => "true") if remote - url = options.is_a?(String) ? options : self.url_for(options) - name ||= url + request_token_tag = form_method == 'post' ? token_tag : '' html_options = convert_options_to_data_attributes(options, html_options) + html_options.merge!("type" => "submit", "value" => name || url) - html_options.merge!("type" => "submit", "value" => name) - - 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 @@ -476,7 +467,7 @@ module ActionView # string given as the value. # * <tt>:subject</tt> - Preset the subject line of the email. # * <tt>:body</tt> - Preset the body of the email. - # * <tt>:cc</tt> - Carbon Copy addition recipients on the email. + # * <tt>:cc</tt> - Carbon Copy additional recipients on the email. # * <tt>:bcc</tt> - Blind Carbon Copy additional recipients on the email. # # ==== Examples @@ -503,7 +494,7 @@ module ActionView extras = %w{ cc bcc body subject }.map { |item| option = html_options.delete(item) || next - "#{item}=#{Rack::Utils.escape(option).gsub("+", "%20")}" + "#{item}=#{Rack::Utils.escape_path(option)}" }.compact extras = extras.empty? ? '' : '?' + ERB::Util.html_escape(extras.join('&')) @@ -599,11 +590,7 @@ module ActionView # We ignore any extra parameters in the request_uri if the # 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 - else - request_uri = request.path - end + request_uri = url_string.index("?") ? request.fullpath : request.path if url_string =~ /^\w+:\/\// url_string == "#{request.protocol}#{request.host_with_port}#{request_uri}" @@ -624,7 +611,7 @@ module ActionView html_options["data-disable-with"] = disable_with if disable_with html_options["data-confirm"] = confirm if confirm - add_method_to_attributes!(html_options, method) if method + add_method_to_attributes!(html_options, method) if method html_options else @@ -633,32 +620,16 @@ module ActionView end def link_to_remote_options?(options) - options.is_a?(Hash) && options.key?('remote') && options.delete('remote') + options.is_a?(Hash) && options.delete('remote') end def add_method_to_attributes!(html_options, method) if method && method.to_s.downcase != "get" && html_options["rel"] !~ /nofollow/ - html_options["rel"] = "#{html_options["rel"]} nofollow".strip + html_options["rel"] = "#{html_options["rel"]} nofollow".lstrip end html_options["data-method"] = method end - def options_for_javascript(options) - if options.empty? - '{}' - else - "{#{options.keys.map { |k| "#{k}:#{options[k]}" }.sort.join(', ')}}" - end - end - - def array_or_string_for_javascript(option) - if option.kind_of?(Array) - "['#{option.join('\',\'')}']" - elsif !option.nil? - "'#{option}'" - end - end - # 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 @@ -686,6 +657,19 @@ module ActionView bool_attrs.each { |x| html_options[x] = x if html_options.delete(x) } html_options end + + def token_tag(token=nil) + if token == false || !protect_against_forgery? + '' + else + token ||= form_authenticity_token + tag(:input, :type => "hidden", :name => request_forgery_protection_token.to_s, :value => token) + end + end + + def method_tag(method) + tag('input', :type => 'hidden', :name => '_method', :value => method.to_s) + end end end end diff --git a/actionpack/lib/action_view/locale/en.yml b/actionpack/lib/action_view/locale/en.yml index eb816b9446..8e9db634fb 100644 --- a/actionpack/lib/action_view/locale/en.yml +++ b/actionpack/lib/action_view/locale/en.yml @@ -37,6 +37,7 @@ # precision: # significant: false # strip_insignificant_zeros: false + format: "%n%" # Used in number_to_precision() precision: @@ -146,9 +147,8 @@ # Default value for :prompt => true in FormOptionsHelper prompt: "Please select" - # Default translation keys for submit FormHelper + # Default translation keys for submit and button FormHelper submit: create: 'Create %{model}' update: 'Update %{model}' submit: 'Save %{model}' - diff --git a/actionpack/lib/action_view/log_subscriber.rb b/actionpack/lib/action_view/log_subscriber.rb index bf90d012bf..cc3a871576 100644 --- a/actionpack/lib/action_view/log_subscriber.rb +++ b/actionpack/lib/action_view/log_subscriber.rb @@ -12,9 +12,8 @@ module ActionView alias :render_partial :render_template alias :render_collection :render_template - # TODO: Ideally, ActionView should have its own logger so it does not depend on AC.logger def logger - ActionController::Base.logger if defined?(ActionController::Base) + ActionView::Base.logger end protected diff --git a/actionpack/lib/action_view/lookup_context.rb b/actionpack/lib/action_view/lookup_context.rb index fa4bf70f77..90c4a2759a 100644 --- a/actionpack/lib/action_view/lookup_context.rb +++ b/actionpack/lib/action_view/lookup_context.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/object/blank' require 'active_support/core_ext/module/remove_method' @@ -20,7 +19,7 @@ module ActionView def self.register_detail(name, options = {}, &block) self.registered_details << name - initialize = registered_details.map { |n| "self.#{n} = details[:#{n}]" } + initialize = registered_details.map { |n| "@details[:#{n}] = details[:#{n}] || default_#{n}" } Accessors.send :define_method, :"default_#{name}", &block Accessors.module_eval <<-METHOD, __FILE__, __LINE__ + 1 @@ -29,7 +28,7 @@ module ActionView end def #{name}=(value) - value = Array.wrap(value.presence || default_#{name}) + value = value.present? ? Array(value) : default_#{name} _set_detail(:#{name}, value) if value != @details[:#{name}] end @@ -44,7 +43,7 @@ module ActionView module Accessors #:nodoc: end - register_detail(:locale) { [I18n.locale, I18n.default_locale] } + register_detail(:locale) { [I18n.locale, I18n.default_locale].uniq } register_detail(:formats) { Mime::SET.symbols } register_detail(:handlers){ Template::Handlers.extensions } @@ -56,7 +55,11 @@ module ActionView @details_keys = Hash.new def self.get(details) - @details_keys[details.freeze] ||= new + @details_keys[details] ||= new + end + + def self.clear + @details_keys.clear end def initialize @@ -85,9 +88,9 @@ module ActionView protected def _set_detail(key, value) + @details = @details.dup if @details_key @details_key = nil - @details = @details.dup if @details.frozen? - @details[key] = value.freeze + @details[key] = value end end @@ -98,7 +101,7 @@ module ActionView # 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::PathSet.new(Array.wrap(paths)) + @view_paths = ActionView::PathSet.new(Array(paths)) end def find(name, prefixes = [], partial = false, keys = [], options = {}) @@ -147,27 +150,18 @@ module ActionView # 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.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 + prefixes = nil if prefixes.blank? + parts = name.to_s.split('/') + parts.shift if parts.first.empty? + name = parts.pop - parts = name.split('/') - name = parts.pop + return name, prefixes || [""] if parts.empty? - prefixes = if prefixes.blank? - [parts.join('/')] - else - prefixes.map { |prefix| [prefix, *parts].compact.join('/') } - end + parts = parts.join('/') + prefixes = prefixes ? prefixes.map { |p| "#{p}/#{parts}" } : [parts] return name, prefixes end - - def handlers_regexp #:nodoc: - @@handlers_regexp ||= /\.(?:#{default_handlers.join('|')})$/ - end end include Accessors @@ -227,7 +221,6 @@ module ActionView 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 diff --git a/actionpack/lib/action_view/railtie.rb b/actionpack/lib/action_view/railtie.rb index 80391d72cc..43371a1c49 100644 --- a/actionpack/lib/action_view/railtie.rb +++ b/actionpack/lib/action_view/railtie.rb @@ -8,6 +8,10 @@ module ActionView config.action_view.stylesheet_expansions = {} config.action_view.javascript_expansions = { :defaults => %w(jquery jquery_ujs) } + initializer "action_view.logger" do + ActiveSupport.on_load(:action_view) { self.logger ||= Rails.logger } + end + initializer "action_view.cache_asset_ids" do |app| unless app.config.cache_classes ActiveSupport.on_load(:action_view) do diff --git a/actionpack/lib/action_view/renderer/abstract_renderer.rb b/actionpack/lib/action_view/renderer/abstract_renderer.rb index c0936441ac..a588abcee3 100644 --- a/actionpack/lib/action_view/renderer/abstract_renderer.rb +++ b/actionpack/lib/action_view/renderer/abstract_renderer.rb @@ -12,27 +12,15 @@ module ActionView end protected - + def extract_details(options) details = {} @lookup_context.registered_details.each do |key| next unless value = options[key] - details[key] = Array.wrap(value) + details[key] = Array(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 - - def formats_regexp - @@formats_regexp ||= /\.(#{Mime::SET.symbols.join('|')})$/ - end def instrument(name, options={}) ActiveSupport::Notifications.instrument("render_#{name}.action_view", options){ yield } diff --git a/actionpack/lib/action_view/renderer/partial_renderer.rb b/actionpack/lib/action_view/renderer/partial_renderer.rb index 15cb9d0f76..3033294883 100644 --- a/actionpack/lib/action_view/renderer/partial_renderer.rb +++ b/actionpack/lib/action_view/renderer/partial_renderer.rb @@ -82,16 +82,17 @@ module ActionView # # This will render the partial "advertisement/_ad.html.erb" regardless of which controller this is being called from. # - # == Rendering objects with the RecordIdentifier + # == Rendering objects that respond to `to_partial_path` # - # Instead of explicitly naming the location of a partial, you can also let the RecordIdentifier do the work if - # you're following its conventions for RecordIdentifier#partial_path. Examples: + # Instead of explicitly naming the location of a partial, you can also let PartialRenderer do the work + # and pick the proper path by checking `to_partial_path` method. # - # # @account is an Account instance, so it uses the RecordIdentifier to replace + # # @account.to_partial_path returns 'accounts/account', so it can be used to replace: # # <%= render :partial => "accounts/account", :locals => { :account => @account} %> # <%= render :partial => @account %> # - # # @posts is an array of Post instances, so it uses the RecordIdentifier to replace + # # @posts is an array of Post instances, so every post record returns 'posts/post' on `to_partial_path`, + # # that's why we can replace: # # <%= render :partial => "posts/post", :collection => @posts %> # <%= render :partial => @posts %> # @@ -106,13 +107,14 @@ module ActionView # # Instead of <%= render :partial => "account", :locals => { :account => @buyer } %> # <%= render "account", :account => @buyer %> # - # # @account is an Account instance, so it uses the RecordIdentifier to replace - # # <%= render :partial => "accounts/account", :locals => { :account => @account } %> - # <%= render(@account) %> + # # @account.to_partial_path returns 'accounts/account', so it can be used to replace: + # # <%= render :partial => "accounts/account", :locals => { :account => @account} %> + # <%= render @account %> # - # # @posts is an array of Post instances, so it uses the RecordIdentifier to replace + # # @posts is an array of Post instances, so every post record returns 'posts/post' on `to_partial_path`, + # # that's why we can replace: # # <%= render :partial => "posts/post", :collection => @posts %> - # <%= render(@posts) %> + # <%= render @posts %> # # == Rendering partials with layouts # @@ -205,7 +207,7 @@ module ActionView # Deadline: <%= user.deadline %> # <%- end -%> # <% end %> - class PartialRenderer < AbstractRenderer #:nodoc: + class PartialRenderer < AbstractRenderer PARTIAL_NAMES = Hash.new { |h,k| h[k] = {} } def initialize(*) @@ -297,7 +299,6 @@ module ActionView "and is followed by any combinations of letters, numbers, or underscores.") end - extract_format(@path, @details) self end @@ -366,27 +367,26 @@ module ActionView 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 + raise ArgumentError.new("'#{object.inspect}' is not an ActiveModel-compatible object. It must implement :to_partial_path.") end - @partial_names[path] ||= path.dup.tap do |object_path| - merge_prefix_into_object_path(@context_prefix, object_path) - end + @partial_names[path] ||= merge_prefix_into_object_path(@context_prefix, path.dup) end def merge_prefix_into_object_path(prefix, object_path) if prefix.include?(?/) && object_path.include?(?/) - overlap = [] + prefixes = [] 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] + break if dir == object_path_array[index] + prefixes << dir end - object_path.gsub!(/^#{overlap.join('/')}\//,'') - object_path.insert(0, "#{File.dirname(prefix)}/") + (prefixes << object_path).join("/") + else + object_path end end diff --git a/actionpack/lib/action_view/renderer/streaming_template_renderer.rb b/actionpack/lib/action_view/renderer/streaming_template_renderer.rb index 1ccf5a8ddb..f46aabd2be 100644 --- a/actionpack/lib/action_view/renderer/streaming_template_renderer.rb +++ b/actionpack/lib/action_view/renderer/streaming_template_renderer.rb @@ -1,7 +1,4 @@ -# 1.9 ships with Fibers but we need to require the extra -# methods explicitly. We only load those extra methods if -# Fiber is available in the first place. -require 'fiber' if defined?(Fiber) +require 'fiber' module ActionView # == TODO diff --git a/actionpack/lib/action_view/renderer/template_renderer.rb b/actionpack/lib/action_view/renderer/template_renderer.rb index ac91d333ba..f3abc6d533 100644 --- a/actionpack/lib/action_view/renderer/template_renderer.rb +++ b/actionpack/lib/action_view/renderer/template_renderer.rb @@ -1,12 +1,10 @@ require 'active_support/core_ext/object/try' -require 'active_support/core_ext/array/wrap' module ActionView class TemplateRenderer < AbstractRenderer #:nodoc: def render(context, options) @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]) @@ -26,6 +24,8 @@ module ActionView elsif options.key?(:template) options[:template].respond_to?(:render) ? options[:template] : find_template(options[:template], options[:prefixes], false, keys, @details) + else + raise ArgumentError, "You invoked render but did not give any of :partial, :template, :inline, :file or :text option." end end @@ -58,14 +58,28 @@ module ActionView # context object. If no layout is found, it checks if at least a layout with # the given name exists across all details before raising the error. def find_layout(layout, keys) - begin - with_layout_format do - layout =~ /^\// ? - with_fallbacks { find_template(layout, nil, false, keys, @details) } : find_template(layout, nil, false, keys, @details) + with_layout_format { resolve_layout(layout, keys) } + end + + def resolve_layout(layout, keys) + case layout + when String + begin + if layout =~ /^\// + with_fallbacks { find_template(layout, nil, false, keys, @details) } + else + 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 - rescue ActionView::MissingTemplate - all_details = @details.merge(:formats => @lookup_context.default_formats) - raise unless template_exists?(layout, nil, false, keys, all_details) + when Proc + resolve_layout(layout.call, keys) + when FalseClass + nil + else + layout end end end diff --git a/actionpack/lib/action_view/template.rb b/actionpack/lib/action_view/template.rb index 10797c010f..edb3d427d5 100644 --- a/actionpack/lib/action_view/template.rb +++ b/actionpack/lib/action_view/template.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/object/blank' require 'active_support/core_ext/object/try' require 'active_support/core_ext/kernel/singleton_class' @@ -122,7 +121,7 @@ module ActionView @locals = details[:locals] || [] @virtual_path = details[:virtual_path] @updated_at = details[:updated_at] || Time.now - @formats = Array.wrap(format).map { |f| f.is_a?(Mime::Type) ? f.ref : f } + @formats = Array(format).map { |f| f.is_a?(Mime::Type) ? f.ref : f } end # Returns if the underlying handler supports streaming. If so, @@ -173,6 +172,50 @@ module ActionView @inspect ||= defined?(Rails.root) ? identifier.sub("#{Rails.root}/", '') : identifier end + # 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 <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 + # with any template engine, as we process out the encoding comment + # before passing the source on to the template engine, leaving a + # blank line in its stead. + def encode! + return unless source.encoding == Encoding::BINARY + + # Look for # encoding: *. If we find one, we'll encode the + # String in that encoding, otherwise, we'll use the + # default external encoding. + if source.sub!(/\A#{ENCODING_FLAG}/, '') + encoding = magic_encoding = $1 + else + encoding = Encoding.default_external + end + + # Tag the source with the default external encoding + # or the encoding specified in the file + source.force_encoding(encoding) + + # If the user didn't specify an encoding, and the handler + # handles encodings, we simply pass the String as is to + # the handler (with the default_external tag) + if !magic_encoding && @handler.respond_to?(:handles_encoding?) && @handler.handles_encoding? + source + # Otherwise, if the String is valid in the encoding, + # encode immediately to default_internal. This means + # that if a handler doesn't handle encodings, it will + # always get Strings in the default_internal + elsif source.valid_encoding? + source.encode! + # Otherwise, since the String is invalid in the encoding + # specified, raise an exception + else + raise WrongEncodingError.new(source, encoding) + end + end + protected # Compile a template. This method ensures a template is compiled @@ -195,15 +238,7 @@ module ActionView end # Among other things, this method is responsible for properly setting - # the encoding of the source. Until this point, we assume that the - # source is BINARY data. If no additional information is supplied, - # 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 - # with any template engine, as we process out the encoding comment - # before passing the source on to the template engine, leaving a - # blank line in its stead. + # the encoding of the compiled template. # # If the template engine handles encodings, we send the encoded # String to the engine without further processing. This allows @@ -215,40 +250,8 @@ module ActionView # In general, this means that templates will be UTF-8 inside of Rails, # regardless of the original source encoding. def compile(view, mod) #:nodoc: + encode! method_name = self.method_name - - if source.encoding_aware? - # Look for # encoding: *. If we find one, we'll encode the - # String in that encoding, otherwise, we'll use the - # default external encoding. - if source.sub!(/\A#{ENCODING_FLAG}/, '') - encoding = magic_encoding = $1 - else - encoding = Encoding.default_external - end - - # Tag the source with the default external encoding - # or the encoding specified in the file - source.force_encoding(encoding) - - # If the user didn't specify an encoding, and the handler - # handles encodings, we simply pass the String as is to - # the handler (with the default_external tag) - if !magic_encoding && @handler.respond_to?(:handles_encoding?) && @handler.handles_encoding? - source - # Otherwise, if the String is valid in the encoding, - # encode immediately to default_internal. This means - # that if a handler doesn't handle encodings, it will - # always get Strings in the default_internal - elsif source.valid_encoding? - source.encode! - # Otherwise, since the String is invalid in the encoding - # specified, raise an exception - else - raise WrongEncodingError.new(source, encoding) - end - end - code = @handler.call(self) # Make sure that the resulting String to be evalled is in the @@ -261,20 +264,18 @@ module ActionView end end_src - if source.encoding_aware? - # Make sure the source is in the encoding of the returned code - source.force_encoding(code.encoding) + # Make sure the source is in the encoding of the returned code + source.force_encoding(code.encoding) - # In case we get back a String from a handler that is not in - # BINARY or the default_internal, encode it to the default_internal - source.encode! + # In case we get back a String from a handler that is not in + # BINARY or the default_internal, encode it to the default_internal + source.encode! - # Now, validate that the source we got back from the template - # handler is valid in the default_internal. This is for handlers - # that handle encoding but screw up - unless source.valid_encoding? - raise WrongEncodingError.new(@source, Encoding.default_internal) - end + # Now, validate that the source we got back from the template + # handler is valid in the default_internal. This is for handlers + # that handle encoding but screw up + unless source.valid_encoding? + raise WrongEncodingError.new(@source, Encoding.default_internal) end begin @@ -287,7 +288,7 @@ module ActionView logger.debug "Backtrace: #{e.backtrace.join("\n")}" end - raise ActionView::Template::Error.new(self, {}, e) + raise ActionView::Template::Error.new(self, e) end end @@ -296,9 +297,12 @@ module ActionView e.sub_template_of(self) raise e else - assigns = view.respond_to?(:assigns) ? view.assigns : {} - template = @virtual_path ? refresh(view) : self - raise Template::Error.new(template, assigns, e) + template = self + unless template.source + template = refresh(view) + template.encode! + end + raise Template::Error.new(template, e) end end diff --git a/actionpack/lib/action_view/template/error.rb b/actionpack/lib/action_view/template/error.rb index 587e37a84f..d8258f7b11 100644 --- a/actionpack/lib/action_view/template/error.rb +++ b/actionpack/lib/action_view/template/error.rb @@ -1,4 +1,3 @@ -require "active_support/core_ext/array/wrap" require "active_support/core_ext/enumerable" module ActionView @@ -30,7 +29,7 @@ module ActionView def initialize(paths, path, prefixes, partial, details, *) @path = path - prefixes = Array.wrap(prefixes) + prefixes = Array(prefixes) template_type = if partial "partial" elsif path =~ /layouts/i @@ -56,9 +55,9 @@ module ActionView attr_reader :original_exception, :backtrace - def initialize(template, assigns, original_exception) + def initialize(template, original_exception) super(original_exception.message) - @template, @assigns, @original_exception = template, assigns.dup, original_exception + @template, @original_exception = template, original_exception @sub_templates = nil @backtrace = original_exception.backtrace end diff --git a/actionpack/lib/action_view/template/handlers.rb b/actionpack/lib/action_view/template/handlers.rb index aa693335e3..67978ada7e 100644 --- a/actionpack/lib/action_view/template/handlers.rb +++ b/actionpack/lib/action_view/template/handlers.rb @@ -17,15 +17,12 @@ module ActionView #:nodoc: @@template_extensions ||= @@template_handlers.keys end - # Register a class that knows how to handle template files with the given + # Register an object that knows how to handle template files with the given # extension. This can be used to implement new template types. - # The constructor for the class must take the ActiveView::Base instance - # as a parameter, and the class must implement a +render+ method that - # takes the contents of the template to render as well as the Hash of - # local assigns available to the template. The +render+ method ought to - # return the rendered template as a string. - def register_template_handler(extension, klass) - @@template_handlers[extension.to_sym] = klass + # The handler must respond to `:call`, which will be passed the template + # and should return the rendered template as a String. + def register_template_handler(extension, handler) + @@template_handlers[extension.to_sym] = handler end def template_handler_extensions diff --git a/actionpack/lib/action_view/template/handlers/builder.rb b/actionpack/lib/action_view/template/handlers/builder.rb index 2c52cfd90e..34397c3bcf 100644 --- a/actionpack/lib/action_view/template/handlers/builder.rb +++ b/actionpack/lib/action_view/template/handlers/builder.rb @@ -6,12 +6,21 @@ module ActionView self.default_format = Mime::XML def call(template) - require 'builder' + require_engine "xml = ::Builder::XmlMarkup.new(:indent => 2);" + "self.output_buffer = xml.target!;" + template.source + ";xml.target!;" end + + protected + + def require_engine + @required ||= begin + require "builder" + true + end + end end end end diff --git a/actionpack/lib/action_view/template/handlers/erb.rb b/actionpack/lib/action_view/template/handlers/erb.rb index 77720e2bc8..323df67c97 100644 --- a/actionpack/lib/action_view/template/handlers/erb.rb +++ b/actionpack/lib/action_view/template/handlers/erb.rb @@ -1,5 +1,5 @@ require 'action_dispatch/http/mime_type' -require 'active_support/core_ext/class/attribute_accessors' +require 'active_support/core_ext/class/attribute' require 'erubis' module ActionView @@ -67,23 +67,19 @@ module ActionView end def call(template) - if template.source.encoding_aware? - # First, convert to BINARY, so in case the encoding is - # wrong, we can still find an encoding tag - # (<%# encoding %>) inside the String using a regular - # expression - template_source = template.source.dup.force_encoding("BINARY") + # First, convert to BINARY, so in case the encoding is + # wrong, we can still find an encoding tag + # (<%# encoding %>) inside the String using a regular + # expression + template_source = template.source.dup.force_encoding("BINARY") - erb = template_source.gsub(ENCODING_TAG, '') - encoding = $2 + erb = template_source.gsub(ENCODING_TAG, '') + encoding = $2 - erb.force_encoding valid_encoding(template.source.dup, encoding) + erb.force_encoding valid_encoding(template.source.dup, encoding) - # Always make sure we return a String in the default_internal - erb.encode! - else - erb = template.source.dup - end + # Always make sure we return a String in the default_internal + erb.encode! self.class.erb_implementation.new( erb, diff --git a/actionpack/lib/action_view/template/resolver.rb b/actionpack/lib/action_view/template/resolver.rb index f855ea257c..7fa86866a7 100644 --- a/actionpack/lib/action_view/template/resolver.rb +++ b/actionpack/lib/action_view/template/resolver.rb @@ -1,6 +1,5 @@ require "pathname" require "active_support/core_ext/class" -require "active_support/core_ext/io" require "action_view/template" module ActionView diff --git a/actionpack/lib/action_view/test_case.rb b/actionpack/lib/action_view/test_case.rb index 9ebe498192..fece499c94 100644 --- a/actionpack/lib/action_view/test_case.rb +++ b/actionpack/lib/action_view/test_case.rb @@ -59,8 +59,10 @@ module ActionView end def determine_default_helper_class(name) - mod = name.sub(/Test$/, '').safe_constantize + mod = name.sub(/Test$/, '').constantize mod.is_a?(Class) ? nil : mod + rescue NameError + nil end def helper_method(*methods) @@ -183,32 +185,32 @@ module ActionView alias_method :_view, :view - INTERNAL_IVARS = %w{ - @__name__ - @__io__ - @_assertion_wrapped - @_assertions - @_result - @_routes - @controller - @layouts - @locals - @method_name - @output_buffer - @partials - @passed - @rendered - @request - @routes - @templates - @options - @test_passed - @view - @view_context_class - } + INTERNAL_IVARS = [ + :@__name__, + :@__io__, + :@_assertion_wrapped, + :@_assertions, + :@_result, + :@_routes, + :@controller, + :@layouts, + :@locals, + :@method_name, + :@output_buffer, + :@partials, + :@passed, + :@rendered, + :@request, + :@routes, + :@templates, + :@options, + :@test_passed, + :@view, + :@view_context_class + ] def _user_defined_ivars - instance_variables.map(&:to_s) - INTERNAL_IVARS + instance_variables - INTERNAL_IVARS end # Returns a Hash of instance variables and their values, as defined by @@ -216,8 +218,8 @@ module ActionView # rendered. This is generally intended for internal use and extension # frameworks. def view_assigns - Hash[_user_defined_ivars.map do |var| - [var[1, var.length].to_sym, instance_variable_get(var)] + Hash[_user_defined_ivars.map do |ivar| + [ivar[1..-1].to_sym, instance_variable_get(ivar)] end] end @@ -233,10 +235,8 @@ module ActionView super end end - end include Behavior - end end diff --git a/actionpack/lib/sprockets/assets.rake b/actionpack/lib/sprockets/assets.rake index a61a121d55..2e92fe416b 100644 --- a/actionpack/lib/sprockets/assets.rake +++ b/actionpack/lib/sprockets/assets.rake @@ -1,12 +1,16 @@ require "fileutils" namespace :assets do - def ruby_rake_task(task) + def ruby_rake_task(task, fork = true) 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) + if $0 =~ /rake\.bat\Z/i + Kernel.exec $0, *args + else + fork ? ruby(*args) : Kernel.exec(FileUtils::RUBY, *args) + end end # We are currently running with no explicit bundler group @@ -59,14 +63,14 @@ namespace :assets do # 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 + ruby_rake_task("assets:precompile:nondigest", false) if Rails.application.config.assets.digest end - task :primary => ["assets:environment", "tmp:cache:clear"] do + task :primary => ["assets:cache:clean"] do internal_precompile end - task :nondigest => ["assets:environment", "tmp:cache:clear"] do + task :nondigest => ["assets:cache:clean"] do internal_precompile(false) end end @@ -77,13 +81,19 @@ namespace :assets do end namespace :clean do - task :all => ["assets:environment", "tmp:cache:clear"] do + task :all => ["assets:cache:clean"] 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 + namespace :cache do + task :clean => ["assets:environment"] do + Rails.application.assets.cache.clear + end + end + task :environment do if Rails.application.config.assets.initialize_on_precompile Rake::Task["environment"].invoke diff --git a/actionpack/lib/sprockets/compressors.rb b/actionpack/lib/sprockets/compressors.rb index cb3e13314b..8b728d6570 100644 --- a/actionpack/lib/sprockets/compressors.rb +++ b/actionpack/lib/sprockets/compressors.rb @@ -1,38 +1,28 @@ module Sprockets module Compressors + extend self + @@css_compressors = {} @@js_compressors = {} @@default_css_compressor = nil @@default_js_compressor = nil - def self.register_css_compressor(name, klass, options = {}) + def 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]} + @@css_compressors[name.to_sym] = { :klass => klass.to_s, :require => options[:require] } end - def self.register_js_compressor(name, klass, options = {}) + def 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]} + @@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 + def registered_css_compressor(name) + find_registered_compressor name, @@css_compressors, @@default_css_compressor 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 + def registered_js_compressor(name) + find_registered_compressor name, @@js_compressors, @@default_js_compressor end # The default compressors must be registered in default plugins (ex. Sass-Rails) @@ -43,6 +33,18 @@ module Sprockets 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') + + private + + def find_registered_compressor(name, compressors_hash, default_compressor_name) + if name.respond_to?(:to_sym) + compressor = compressors_hash[name.to_sym] || compressors_hash[default_compressor_name] + require compressor[:require] if compressor[:require] + compressor[:klass].constantize.new + else + name + end + end end # An asset compressor which does nothing. diff --git a/actionpack/lib/sprockets/helpers/rails_helper.rb b/actionpack/lib/sprockets/helpers/rails_helper.rb index ddf9b08b54..976ae5a76d 100644 --- a/actionpack/lib/sprockets/helpers/rails_helper.rb +++ b/actionpack/lib/sprockets/helpers/rails_helper.rb @@ -26,10 +26,10 @@ module Sprockets 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)) + super(dep.pathname.to_s, { :src => path_to_asset(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)) + super(source.to_s, { :src => path_to_asset(source, :ext => 'js', :body => body, :digest => digest) }.merge!(options)) end end.join("\n").html_safe end @@ -43,10 +43,10 @@ module Sprockets 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)) + super(dep.pathname.to_s, { :href => path_to_asset(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)) + super(source.to_s, { :href => path_to_asset(source, :ext => 'css', :body => body, :protocol => :request, :digest => digest) }.merge!(options)) end end.join("\n").html_safe end @@ -56,19 +56,25 @@ module Sprockets path = asset_paths.compute_public_path(source, asset_prefix, options.merge(:body => true)) options[:body] ? "#{path}?body=1" : path end + alias_method :path_to_asset, :asset_path # aliased to avoid conflicts with an asset_path named route def image_path(source) - asset_path(source) + path_to_asset(source) end alias_method :path_to_image, :image_path # aliased to avoid conflicts with an image_path named route + def font_path(source) + path_to_asset(source) + end + alias_method :path_to_font, :font_path # aliased to avoid conflicts with an font_path named route + def javascript_path(source) - asset_path(source) + path_to_asset(source, :ext => 'js') 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) + path_to_asset(source, :ext => 'css') end alias_method :path_to_stylesheet, :stylesheet_path # aliased to avoid conflicts with an stylesheet_path named route @@ -113,11 +119,6 @@ module Sprockets 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) @@ -154,7 +155,7 @@ module Sprockets end def rewrite_extension(source, dir, ext) - if ext && File.extname(source).empty? + if ext && File.extname(source) != ".#{ext}" "#{source}.#{ext}" else source diff --git a/actionpack/lib/sprockets/railtie.rb b/actionpack/lib/sprockets/railtie.rb index 3d330bd91a..2bc482a39d 100644 --- a/actionpack/lib/sprockets/railtie.rb +++ b/actionpack/lib/sprockets/railtie.rb @@ -10,8 +10,6 @@ module Sprockets # 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 @@ -23,9 +21,12 @@ module Sprockets require 'sprockets' app.assets = Sprockets::Environment.new(app.root.to_s) do |env| - env.logger = ::Rails.logger env.version = ::Rails.env + "-#{config.assets.version}" + if config.assets.logger != false + env.logger = config.assets.logger || ::Rails.logger + end + if config.assets.cache_store != false env.cache = ActiveSupport::Cache.lookup_store(config.assets.cache_store) || ::Rails.cache end diff --git a/actionpack/lib/sprockets/static_compiler.rb b/actionpack/lib/sprockets/static_compiler.rb index 32a9d66e6e..9bbb464474 100644 --- a/actionpack/lib/sprockets/static_compiler.rb +++ b/actionpack/lib/sprockets/static_compiler.rb @@ -8,9 +8,10 @@ module Sprockets @env = env @target = target @paths = paths - @digest = options.key?(:digest) ? options.delete(:digest) : true - @manifest = options.key?(:manifest) ? options.delete(:manifest) : true + @digest = options.fetch(:digest, true) + @manifest = options.fetch(:manifest, true) @manifest_path = options.delete(:manifest_path) || target + @zip_files = options.delete(:zip_files) || /\.(?:css|html|js|svg|txt|xml)$/ end def compile @@ -36,7 +37,7 @@ module Sprockets 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)$/ + asset.write_to("#{filename}.gz") if filename.to_s =~ @zip_files end end |