diff options
Diffstat (limited to 'actionpack/lib')
110 files changed, 2235 insertions, 1563 deletions
diff --git a/actionpack/lib/abstract_controller.rb b/actionpack/lib/abstract_controller.rb index 867a7954e0..fe9802e395 100644 --- a/actionpack/lib/abstract_controller.rb +++ b/actionpack/lib/abstract_controller.rb @@ -10,12 +10,11 @@ module AbstractController autoload :Base autoload :Callbacks autoload :Collector + autoload :DoubleRenderError, "abstract_controller/rendering" autoload :Helpers - autoload :Layouts autoload :Logger autoload :Rendering autoload :Translation autoload :AssetPaths - autoload :ViewPaths autoload :UrlFor end diff --git a/actionpack/lib/abstract_controller/base.rb b/actionpack/lib/abstract_controller/base.rb index af5de815bb..c00f0d0c6f 100644 --- a/actionpack/lib/abstract_controller/base.rb +++ b/actionpack/lib/abstract_controller/base.rb @@ -8,7 +8,8 @@ module AbstractController class Error < StandardError #:nodoc: end - class ActionNotFound < StandardError #:nodoc: + # Raised when a non-existing controller action is triggered. + class ActionNotFound < StandardError end # <tt>AbstractController::Base</tt> is a low-level API. Nobody should be @@ -120,14 +121,14 @@ module AbstractController # # The actual method that is called is determined by calling # #method_for_action. If no method can handle the action, then an - # ActionNotFound error is raised. + # AbstractController::ActionNotFound error is raised. # # ==== Returns # * <tt>self</tt> def process(action, *args) - @_action_name = action_name = action.to_s + @_action_name = action.to_s - unless action_name = method_for_action(action_name) + unless action_name = _find_action_name(@_action_name) raise ActionNotFound, "The action '#{action}' could not be found for #{self.class.name}" end @@ -160,7 +161,7 @@ module AbstractController # ==== Returns # * <tt>TrueClass</tt>, <tt>FalseClass</tt> def available_action?(action_name) - method_for_action(action_name).present? + _find_action_name(action_name).present? end private @@ -204,6 +205,24 @@ module AbstractController end # Takes an action name and returns the name of the method that will + # handle the action. + # + # It checks if the action name is valid and returns false otherwise. + # + # See method_for_action for more information. + # + # ==== Parameters + # * <tt>action_name</tt> - An action name to find a method name for + # + # ==== Returns + # * <tt>string</tt> - The name of the method that handles the action + # * false - No valid method name could be found. + # Raise AbstractController::ActionNotFound. + def _find_action_name(action_name) + _valid_action_name?(action_name) && method_for_action(action_name) + end + + # Takes an action name and returns the name of the method that will # handle the action. In normal cases, this method returns the same # name as it receives. By default, if #method_for_action receives # a name that is not an action, it will look for an #action_missing @@ -218,14 +237,14 @@ module AbstractController # the case. # # If none of these conditions are true, and method_for_action - # returns nil, an ActionNotFound exception will be raised. + # returns nil, an AbstractController::ActionNotFound exception will be raised. # # ==== Parameters # * <tt>action_name</tt> - An action name to find a method name for # # ==== Returns # * <tt>string</tt> - The name of the method that handles the action - # * <tt>nil</tt> - No method name could be found. Raise ActionNotFound. + # * <tt>nil</tt> - No method name could be found. def method_for_action(action_name) if action_method?(action_name) action_name @@ -233,5 +252,10 @@ module AbstractController "_handle_action_missing" end end + + # Checks if the action name is valid and returns false otherwise. + def _valid_action_name?(action_name) + action_name.to_s !~ Regexp.new(File::SEPARATOR) + end end end diff --git a/actionpack/lib/abstract_controller/callbacks.rb b/actionpack/lib/abstract_controller/callbacks.rb index 21c6191691..69aca308d6 100644 --- a/actionpack/lib/abstract_controller/callbacks.rb +++ b/actionpack/lib/abstract_controller/callbacks.rb @@ -38,7 +38,7 @@ module AbstractController def _normalize_callback_option(options, from, to) # :nodoc: if from = options[from] from = Array(from).map {|o| "action_name == '#{o}'"}.join(" || ") - options[to] = Array(options[to]) << from + options[to] = Array(options[to]).unshift(from) end end @@ -178,41 +178,35 @@ module AbstractController # set up before_action, prepend_before_action, skip_before_action, etc. # for each of before, after, and around. [:before, :after, :around].each do |callback| - class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 - # Append a before, after or around callback. See _insert_callbacks - # for details on the allowed parameters. - def #{callback}_action(*names, &blk) # def before_action(*names, &blk) - _insert_callbacks(names, blk) do |name, options| # _insert_callbacks(names, blk) do |name, options| - set_callback(:process_action, :#{callback}, name, options) # set_callback(:process_action, :before, name, options) - end # end - end # end - - alias_method :#{callback}_filter, :#{callback}_action - - # Prepend a before, after or around callback. See _insert_callbacks - # for details on the allowed parameters. - def prepend_#{callback}_action(*names, &blk) # def prepend_before_action(*names, &blk) - _insert_callbacks(names, blk) do |name, options| # _insert_callbacks(names, blk) do |name, options| - set_callback(:process_action, :#{callback}, name, options.merge(:prepend => true)) # set_callback(:process_action, :before, name, options.merge(:prepend => true)) - end # end - end # end - - alias_method :prepend_#{callback}_filter, :prepend_#{callback}_action - - # Skip a before, after or around callback. See _insert_callbacks - # for details on the allowed parameters. - def skip_#{callback}_action(*names) # def skip_before_action(*names) - _insert_callbacks(names) do |name, options| # _insert_callbacks(names) do |name, options| - skip_callback(:process_action, :#{callback}, name, options) # skip_callback(:process_action, :before, name, options) - end # end - end # end - - alias_method :skip_#{callback}_filter, :skip_#{callback}_action - - # *_action is the same as append_*_action - alias_method :append_#{callback}_action, :#{callback}_action # alias_method :append_before_action, :before_action - alias_method :append_#{callback}_filter, :#{callback}_action # alias_method :append_before_filter, :before_action - RUBY_EVAL + define_method "#{callback}_action" do |*names, &blk| + _insert_callbacks(names, blk) do |name, options| + set_callback(:process_action, callback, name, options) + end + end + + alias_method :"#{callback}_filter", :"#{callback}_action" + + define_method "prepend_#{callback}_action" do |*names, &blk| + _insert_callbacks(names, blk) do |name, options| + set_callback(:process_action, callback, name, options.merge(:prepend => true)) + end + end + + alias_method :"prepend_#{callback}_filter", :"prepend_#{callback}_action" + + # Skip a before, after or around callback. See _insert_callbacks + # for details on the allowed parameters. + define_method "skip_#{callback}_action" do |*names| + _insert_callbacks(names) do |name, options| + skip_callback(:process_action, callback, name, options) + end + end + + alias_method :"skip_#{callback}_filter", :"skip_#{callback}_action" + + # *_action is the same as append_*_action + alias_method :"append_#{callback}_action", :"#{callback}_action" # alias_method :append_before_action, :before_action + alias_method :"append_#{callback}_filter", :"#{callback}_action" # alias_method :append_before_filter, :before_action end end end diff --git a/actionpack/lib/abstract_controller/collector.rb b/actionpack/lib/abstract_controller/collector.rb index 09b9e7ddf0..ddd56b354a 100644 --- a/actionpack/lib/abstract_controller/collector.rb +++ b/actionpack/lib/abstract_controller/collector.rb @@ -23,7 +23,17 @@ module AbstractController protected def method_missing(symbol, &block) - mime_constant = Mime.const_get(symbol.upcase) + const_name = symbol.upcase + + unless Mime.const_defined?(const_name) + raise NoMethodError, "To respond to a custom format, register it as a MIME type first: " \ + "http://guides.rubyonrails.org/action_controller_overview.html#restful-downloads. " \ + "If you meant to respond to a variant like :tablet or :phone, not a custom format, " \ + "be sure to nest your variant response within a format response: " \ + "format.html { |html| html.tablet { ... } }" + end + + mime_constant = Mime.const_get(const_name) if Mime::SET.include?(mime_constant) AbstractController::Collector.generate_method_for_mime(mime_constant) diff --git a/actionpack/lib/abstract_controller/helpers.rb b/actionpack/lib/abstract_controller/helpers.rb index 5ae8c6c3b0..e77e4e01e9 100644 --- a/actionpack/lib/abstract_controller/helpers.rb +++ b/actionpack/lib/abstract_controller/helpers.rb @@ -12,7 +12,24 @@ module AbstractController self._helper_methods = Array.new end + class MissingHelperError < LoadError + def initialize(error, path) + @error = error + @path = "helpers/#{path}.rb" + set_backtrace error.backtrace + + if error.path =~ /^#{path}(\.rb)?$/ + super("Missing helper file helpers/%s.rb" % path) + else + raise error + end + end + end + module ClassMethods + MissingHelperError = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('AbstractController::Helpers::ClassMethods::MissingHelperError', + 'AbstractController::Helpers::MissingHelperError') + # When a class is inherited, wrap its helper module in a new module. # This ensures that the parent class's module can be changed # independently of the child class's. @@ -134,7 +151,7 @@ module AbstractController begin require_dependency(file_name) rescue LoadError => e - raise MissingHelperError.new(e, file_name) + raise AbstractController::Helpers::MissingHelperError.new(e, file_name) end file_name.camelize.constantize when Module @@ -145,15 +162,6 @@ module AbstractController end end - class MissingHelperError < LoadError - def initialize(error, path) - @error = error - @path = "helpers/#{path}.rb" - set_backtrace error.backtrace - super("Missing helper file helpers/%s.rb" % path) - end - end - private # Makes all the (instance) methods in the helper module available to templates # rendered through this controller. diff --git a/actionpack/lib/abstract_controller/layouts.rb b/actionpack/lib/abstract_controller/layouts.rb deleted file mode 100644 index 8e7bdf620e..0000000000 --- a/actionpack/lib/abstract_controller/layouts.rb +++ /dev/null @@ -1,423 +0,0 @@ -require "active_support/core_ext/module/remove_method" - -module AbstractController - # Layouts reverse the common pattern of including shared headers and footers in many templates to isolate changes in - # repeated setups. The inclusion pattern has pages that look like this: - # - # <%= render "shared/header" %> - # Hello World - # <%= render "shared/footer" %> - # - # This approach is a decent way of keeping common structures isolated from the changing content, but it's verbose - # and if you ever want to change the structure of these two includes, you'll have to change all the templates. - # - # With layouts, you can flip it around and have the common structure know where to insert changing content. This means - # that the header and footer are only mentioned in one place, like this: - # - # // The header part of this layout - # <%= yield %> - # // The footer part of this layout - # - # And then you have content pages that look like this: - # - # hello world - # - # At rendering time, the content page is computed and then inserted in the layout, like this: - # - # // The header part of this layout - # hello world - # // The footer part of this layout - # - # == Accessing shared variables - # - # Layouts have access to variables specified in the content pages and vice versa. This allows you to have layouts with - # references that won't materialize before rendering time: - # - # <h1><%= @page_title %></h1> - # <%= yield %> - # - # ...and content pages that fulfill these references _at_ rendering time: - # - # <% @page_title = "Welcome" %> - # Off-world colonies offers you a chance to start a new life - # - # The result after rendering is: - # - # <h1>Welcome</h1> - # Off-world colonies offers you a chance to start a new life - # - # == Layout assignment - # - # You can either specify a layout declaratively (using the #layout class method) or give - # it the same name as your controller, and place it in <tt>app/views/layouts</tt>. - # If a subclass does not have a layout specified, it inherits its layout using normal Ruby inheritance. - # - # For instance, if you have PostsController and a template named <tt>app/views/layouts/posts.html.erb</tt>, - # that template will be used for all actions in PostsController and controllers inheriting - # from PostsController. - # - # If you use a module, for instance Weblog::PostsController, you will need a template named - # <tt>app/views/layouts/weblog/posts.html.erb</tt>. - # - # Since all your controllers inherit from ApplicationController, they will use - # <tt>app/views/layouts/application.html.erb</tt> if no other layout is specified - # or provided. - # - # == Inheritance Examples - # - # class BankController < ActionController::Base - # # bank.html.erb exists - # - # class ExchangeController < BankController - # # exchange.html.erb exists - # - # class CurrencyController < BankController - # - # class InformationController < BankController - # layout "information" - # - # class TellerController < InformationController - # # teller.html.erb exists - # - # class EmployeeController < InformationController - # # employee.html.erb exists - # layout nil - # - # class VaultController < BankController - # layout :access_level_layout - # - # class TillController < BankController - # layout false - # - # In these examples, we have three implicit lookup scenarios: - # * 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 TillController does not use a layout at all. - # - # == Types of layouts - # - # Layouts are basically just regular templates, but the name of this template needs not be specified statically. Sometimes - # you want to alternate layouts depending on runtime information, such as whether someone is logged in or not. This can - # be done either by specifying a method reference as a symbol or using an inline method (as a proc). - # - # The method reference is the preferred approach to variable layouts and is used like this: - # - # class WeblogController < ActionController::Base - # layout :writers_and_readers - # - # def index - # # fetching posts - # end - # - # private - # def writers_and_readers - # logged_in? ? "writer_layout" : "reader_layout" - # end - # end - # - # Now when a new request for the index action is processed, the layout will vary depending on whether the person accessing - # is logged in or not. - # - # If you want to use an inline method, such as a proc, do something like this: - # - # class WeblogController < ActionController::Base - # layout proc { |controller| controller.logged_in? ? "writer_layout" : "reader_layout" } - # end - # - # If an argument isn't given to the proc, it's evaluated in the context of - # the current controller anyway. - # - # class WeblogController < ActionController::Base - # layout proc { logged_in? ? "writer_layout" : "reader_layout" } - # end - # - # Of course, the most common way of specifying a layout is still just as a plain template name: - # - # class WeblogController < ActionController::Base - # layout "weblog_standard" - # end - # - # The template will be looked always in <tt>app/views/layouts/</tt> folder. But you can point - # <tt>layouts</tt> folder direct also. <tt>layout "layouts/demo"</tt> is the same as <tt>layout "demo"</tt>. - # - # 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 - # a given action or set of actions without a layout, or restricting a layout to only a single action or a set of actions. The - # <tt>:only</tt> and <tt>:except</tt> options can be passed to the layout call. For example: - # - # class WeblogController < ActionController::Base - # layout "weblog_standard", except: :rss - # - # # ... - # - # end - # - # This will assign "weblog_standard" as the WeblogController's layout for all actions except for the +rss+ action, which will - # be rendered directly, without wrapping a layout around the rendered view. - # - # Both the <tt>:only</tt> and <tt>:except</tt> condition can accept an arbitrary number of method references, so - # #<tt>except: [ :rss, :text_only ]</tt> is valid, as is <tt>except: :rss</tt>. - # - # == Using a different layout in the action render call - # - # If most of your actions use the same layout, it makes perfect sense to define a controller-wide layout as described above. - # Sometimes you'll have exceptions where one action wants to use a different layout than the rest of the controller. - # You can do this by passing a <tt>:layout</tt> option to the <tt>render</tt> call. For example: - # - # class WeblogController < ActionController::Base - # layout "weblog_standard" - # - # def help - # render action: "help", layout: "help" - # end - # end - # - # This will override the controller-wide "weblog_standard" layout, and will render the help action with the "help" layout instead. - module Layouts - extend ActiveSupport::Concern - - include Rendering - - included do - class_attribute :_layout, :_layout_conditions, :instance_accessor => false - self._layout = nil - self._layout_conditions = {} - _write_layout_method - end - - delegate :_layout_conditions, to: :class - - module ClassMethods - def inherited(klass) # :nodoc: - super - klass._write_layout_method - end - - # This module is mixed in if layout conditions are provided. This means - # that if no layout conditions are used, this method is not used - module LayoutConditions # :nodoc: - private - - # Determines whether the current action has a layout definition by - # checking the action name against the :only and :except conditions - # set by the <tt>layout</tt> method. - # - # ==== Returns - # * <tt> Boolean</tt> - True if the action has a layout definition, false otherwise. - def _conditional_layout? - return unless super - - conditions = _layout_conditions - - if only = conditions[:only] - only.include?(action_name) - elsif except = conditions[:except] - !except.include?(action_name) - else - true - end - end - end - - # Specify the layout to use for this class. - # - # If the specified layout is a: - # String:: the String is the template name - # Symbol:: call the method specified by the symbol, which will return the template name - # false:: There is no layout - # true:: raise an ArgumentError - # nil:: Force default layout behavior with inheritance - # - # ==== Parameters - # * <tt>layout</tt> - The layout to use. - # - # ==== Options (conditions) - # * :only - A list of actions to apply this layout to. - # * :except - Apply this layout to all actions but this one. - def layout(layout, conditions = {}) - include LayoutConditions unless conditions.empty? - - conditions.each {|k, v| conditions[k] = Array(v).map {|a| a.to_s} } - self._layout_conditions = conditions - - self._layout = layout - _write_layout_method - end - - # If no layout is supplied, look for a template named the return - # value of this method. - # - # ==== Returns - # * <tt>String</tt> - A template name - def _implied_layout_name # :nodoc: - controller_path - end - - # Creates a _layout method to be called by _default_layout . - # - # If a layout is not explicitly mentioned then look for a layout with the controller's name. - # if nothing is found then try same procedure to find super class's layout. - def _write_layout_method # :nodoc: - remove_possible_method(:_layout) - - prefixes = _implied_layout_name =~ /\blayouts/ ? [] : ["layouts"] - default_behavior = "lookup_context.find_all('#{_implied_layout_name}', #{prefixes.inspect}).first || super" - name_clause = if name - default_behavior - else - <<-RUBY - super - RUBY - end - - layout_definition = case _layout - when String - _layout.inspect - when Symbol - <<-RUBY - #{_layout}.tap do |layout| - return #{default_behavior} if layout.nil? - unless layout.is_a?(String) || !layout - raise ArgumentError, "Your layout method :#{_layout} returned \#{layout}. It " \ - "should have returned a String, false, or nil" - end - end - RUBY - when Proc - define_method :_layout_from_proc, &_layout - protected :_layout_from_proc - <<-RUBY - result = _layout_from_proc(#{_layout.arity == 0 ? '' : 'self'}) - return #{default_behavior} if result.nil? - result - RUBY - when false - nil - when true - raise ArgumentError, "Layouts must be specified as a String, Symbol, Proc, false, or nil" - when nil - name_clause - end - - self.class_eval <<-RUBY, __FILE__, __LINE__ + 1 - def _layout - if _conditional_layout? - #{layout_definition} - else - #{name_clause} - end - end - private :_layout - RUBY - end - end - - def _normalize_options(options) # :nodoc: - super - - if _include_layout?(options) - layout = options.delete(:layout) { :default } - options[:layout] = _layout_for_option(layout) - end - end - - attr_internal_writer :action_has_layout - - def initialize(*) # :nodoc: - @_action_has_layout = true - super - end - - # Controls whether an action should be rendered using a layout. - # If you want to disable any <tt>layout</tt> settings for the - # current action so that it is rendered without a layout then - # either override this method in your controller to return false - # for that action or set the <tt>action_has_layout</tt> attribute - # to false before rendering. - def action_has_layout? - @_action_has_layout - end - - private - - def _conditional_layout? - true - end - - # This will be overwritten by _write_layout_method - def _layout; end - - # Determine the layout for a given name, taking into account the name type. - # - # ==== Parameters - # * <tt>name</tt> - The name of the template - def _layout_for_option(name) - case name - 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, - "String, Proc, :default, true, or false, expected for `layout'; you passed #{name.inspect}" - end - end - - 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>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 - value = _layout if action_has_layout? - rescue NameError => e - raise e, "Could not render layout: #{e.message}" - end - - if require_layout && action_has_layout? && !value - raise ArgumentError, - "There was no default layout for #{self.class} in #{view_paths.inspect}" - end - - _normalize_layout(value) - end - - def _include_layout?(options) - (options.keys & [:text, :inline, :partial]).empty? || options.key?(:layout) - end - end -end diff --git a/actionpack/lib/abstract_controller/rendering.rb b/actionpack/lib/abstract_controller/rendering.rb index 3f34add790..9d10140ed2 100644 --- a/actionpack/lib/abstract_controller/rendering.rb +++ b/actionpack/lib/abstract_controller/rendering.rb @@ -1,5 +1,8 @@ -require "abstract_controller/base" -require "action_view" +require 'active_support/concern' +require 'active_support/core_ext/class/attribute' +require 'action_view' +require 'action_view/view_paths' +require 'set' module AbstractController class DoubleRenderError < Error @@ -10,91 +13,18 @@ module AbstractController end end - # This is a class to fix I18n global state. Whenever you provide I18n.locale during a request, - # it will trigger the lookup_context and consequently expire the cache. - class I18nProxy < ::I18n::Config #:nodoc: - attr_reader :original_config, :lookup_context - - def initialize(original_config, lookup_context) - original_config = original_config.original_config if original_config.respond_to?(:original_config) - @original_config, @lookup_context = original_config, lookup_context - end - - def locale - @original_config.locale - end - - def locale=(value) - @lookup_context.locale = value - end - end - module Rendering extend ActiveSupport::Concern - include AbstractController::ViewPaths - - included do - class_attribute :protected_instance_variables - self.protected_instance_variables = [] - end - - # Overwrite process to setup I18n proxy. - def process(*) #:nodoc: - old_config, I18n.config = I18n.config, I18nProxy.new(I18n.config, lookup_context) - super - ensure - I18n.config = old_config - end - - module ClassMethods - def view_context_class - @view_context_class ||= begin - routes = respond_to?(:_routes) && _routes - helpers = respond_to?(:_helpers) && _helpers - - Class.new(ActionView::Base) do - if routes - include routes.url_helpers - include routes.mounted_helpers - end - - if helpers - include helpers - end - end - end - end - end - - attr_internal_writer :view_context_class - - def view_context_class - @_view_context_class ||= self.class.view_context_class - end - - # An instance of a view class. The default view class is ActionView::Base - # - # The view class must have the following methods: - # View.new[lookup_context, assigns, controller] - # Create a new ActionView instance for a controller - # View#render[options] - # Returns String with the rendered template - # - # Override this method in a module to change the default behavior. - def view_context - view_context_class.new(view_renderer, view_assigns, self) - end - - # Returns an object that is able to render templates. - def view_renderer - @_view_renderer ||= ActionView::Renderer.new(lookup_context) - end + include ActionView::ViewPaths # Normalize arguments, options and then delegates render_to_body and # sticks the result in self.response_body. + # :api: public def render(*args, &block) options = _normalize_render(*args, &block) self.response_body = render_to_body(options) + _process_format(rendered_format, options) if rendered_format + self.response_body end # Raw rendering of a template to a string. @@ -113,84 +43,78 @@ module AbstractController render_to_body(options) end - # Raw rendering of a template. - # :api: plugin + # Performs the actual template rendering. + # :api: public def render_to_body(options = {}) - _process_options(options) - _render_template(options) end - # Find and renders a template based on the options given. - # :api: private - def _render_template(options) #:nodoc: - lookup_context.rendered_format = nil if options[:formats] - view_renderer.render(view_context, options) + # Returns Content-Type of rendered content + # :api: public + def rendered_format + Mime::TEXT end - DEFAULT_PROTECTED_INSTANCE_VARIABLES = [ - :@_action_name, :@_response_body, :@_formats, :@_prefixes, :@_config, - :@_view_context_class, :@_view_renderer, :@_lookup_context - ] + DEFAULT_PROTECTED_INSTANCE_VARIABLES = Set.new %w( + @_action_name @_response_body @_formats @_prefixes @_config + @_view_context_class @_view_renderer @_lookup_context + @_routes @_db_runtime + ).map(&:to_sym) # This method should return a hash with assigns. # You can overwrite this configuration per controller. # :api: public def view_assigns - hash = {} - variables = instance_variables - variables -= protected_instance_variables - variables -= DEFAULT_PROTECTED_INSTANCE_VARIABLES - variables.each { |name| hash[name[1..-1]] = instance_variable_get(name) } - hash - end - - private + protected_vars = _protected_ivars + variables = instance_variables - # Normalize args and options. - # :api: private - def _normalize_render(*args, &block) - options = _normalize_args(*args, &block) - _normalize_options(options) - options + variables.reject! { |s| protected_vars.include? s } + variables.each_with_object({}) { |name, hash| + hash[name.slice(1, name.length)] = instance_variable_get(name) + } end # Normalize args by converting render "foo" to render :action => "foo" and # render "foo/bar" to render :file => "foo/bar". # :api: plugin def _normalize_args(action=nil, options={}) - case action - when NilClass - when Hash - options = action - when String, Symbol - action = action.to_s - key = action.include?(?/) ? :file : :action - options[key] = action + if action.is_a? Hash + action else - options[:partial] = action + options end - - options end # Normalize options. # :api: plugin def _normalize_options(options) - if options[:partial] == true - options[:partial] = action_name - end - - if (options.keys & [:partial, :file, :template]).empty? - options[:prefixes] ||= _prefixes - end - - options[:template] ||= (options[:action] || action_name).to_s options end # Process extra options. # :api: plugin def _process_options(options) + options + end + + # Process the rendered format. + # :api: private + def _process_format(format, options = {}) + end + + # Normalize args and options. + # :api: private + def _normalize_render(*args, &block) + options = _normalize_args(*args, &block) + #TODO: remove defined? when we restore AP <=> AV dependency + if defined?(request) && request && request.variant.present? + options[:variant] = request.variant + end + _normalize_options(options) + options + end + + def _protected_ivars # :nodoc: + DEFAULT_PROTECTED_INSTANCE_VARIABLES end end end diff --git a/actionpack/lib/abstract_controller/url_for.rb b/actionpack/lib/abstract_controller/url_for.rb index 4a95e1f276..72d07b0927 100644 --- a/actionpack/lib/abstract_controller/url_for.rb +++ b/actionpack/lib/abstract_controller/url_for.rb @@ -11,7 +11,7 @@ module AbstractController def _routes raise "In order to use #url_for, you must include routing helpers explicitly. " \ - "For instance, `include Rails.application.routes.url_helpers" + "For instance, `include Rails.application.routes.url_helpers`." end module ClassMethods diff --git a/actionpack/lib/abstract_controller/view_paths.rb b/actionpack/lib/abstract_controller/view_paths.rb deleted file mode 100644 index c08b3a0e2a..0000000000 --- a/actionpack/lib/abstract_controller/view_paths.rb +++ /dev/null @@ -1,96 +0,0 @@ -require 'action_view/base' - -module AbstractController - module ViewPaths - extend ActiveSupport::Concern - - included do - class_attribute :_view_paths - self._view_paths = ActionView::PathSet.new - self._view_paths.freeze - end - - delegate :template_exists?, :view_paths, :formats, :formats=, - :locale, :locale=, :to => :lookup_context - - module ClassMethods - def parent_prefixes - @parent_prefixes ||= begin - parent_controller = superclass - prefixes = [] - - until parent_controller.abstract? - prefixes << parent_controller.controller_path - parent_controller = parent_controller.superclass - end - - prefixes - end - end - end - - # The prefixes used in render "foo" shortcuts. - def _prefixes - @_prefixes ||= begin - parent_prefixes = self.class.parent_prefixes - parent_prefixes.dup.unshift(controller_path) - end - end - - # LookupContext is the object responsible to hold all information required to lookup - # templates, i.e. view paths and details. Check ActionView::LookupContext for more - # information. - def lookup_context - @_lookup_context ||= - ActionView::LookupContext.new(self.class._view_paths, details_for_lookup, _prefixes) - end - - def details_for_lookup - { } - end - - def append_view_path(path) - lookup_context.view_paths.push(*path) - end - - def prepend_view_path(path) - lookup_context.view_paths.unshift(*path) - end - - module ClassMethods - # Append a path to the list of view paths for this controller. - # - # ==== Parameters - # * <tt>path</tt> - If a String is provided, it gets converted into - # the default view path. You may also provide a custom view path - # (see ActionView::PathSet for more information) - def append_view_path(path) - self._view_paths = view_paths + Array(path) - end - - # Prepend a path to the list of view paths for this controller. - # - # ==== Parameters - # * <tt>path</tt> - If a String is provided, it gets converted into - # the default view path. You may also provide a custom view path - # (see ActionView::PathSet for more information) - def prepend_view_path(path) - self._view_paths = ActionView::PathSet.new(Array(path) + view_paths) - end - - # A list of all of the default view paths for this controller. - def view_paths - _view_paths - end - - # Set the view paths. - # - # ==== Parameters - # * <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(paths)) - end - end - end -end diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb index cc03da4904..50bc26a80f 100644 --- a/actionpack/lib/action_controller.rb +++ b/actionpack/lib/action_controller.rb @@ -46,20 +46,11 @@ module ActionController def self.eager_load! super ActionController::Caching.eager_load! - HTML.eager_load! end end -# All of these simply register additional autoloads -require 'action_view' -require 'action_view/vendor/html-scanner' - -ActiveSupport.on_load(:action_view) do - ActionView::RoutingUrlFor.send(:include, ActionDispatch::Routing::UrlFor) -end - # Common Active Support usage in Action Controller -require 'active_support/core_ext/class/attribute_accessors' +require 'active_support/core_ext/module/attribute_accessors' require 'active_support/core_ext/load_error' require 'active_support/core_ext/module/attr_internal' require 'active_support/core_ext/name_error' diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index d7b09b67c0..e6fe6b0b00 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -1,9 +1,10 @@ +require 'action_view' require "action_controller/log_subscriber" require "action_controller/metal/params_wrapper" module ActionController # Action Controllers are the core of a web request in \Rails. They are made up of one or more actions that are executed - # on request and then either render a template or redirect to another action. An action is defined as a public method + # on request and then either it renders a template or redirects to another action. An action is defined as a public method # on the controller, which will automatically be made accessible to the web-server through \Rails Routes. # # By default, only the ApplicationController in a \Rails application inherits from <tt>ActionController::Base</tt>. All other @@ -44,7 +45,7 @@ module ActionController # # def server_ip # location = request.env["SERVER_ADDR"] - # render text: "This server hosted at #{location}" + # render plain: "This server hosted at #{location}" # end # # == Parameters @@ -59,7 +60,7 @@ module ActionController # <input type="text" name="post[address]" value="hyacintvej"> # # A request stemming from a form holding these inputs will include <tt>{ "post" => { "name" => "david", "address" => "hyacintvej" } }</tt>. - # If the address input had been named "post[address][street]", the params would have included + # If the address input had been named <tt>post[address][street]</tt>, the params would have included # <tt>{ "post" => { "address" => { "street" => "hyacintvej" } } }</tt>. There's no limit to the depth of the nesting. # # == Sessions @@ -85,7 +86,7 @@ module ActionController # or you can remove the entire session with +reset_session+. # # Sessions are stored by default in a browser cookie that's cryptographically signed, but unencrypted. - # This prevents the user from tampering with the session but also allows him to see its contents. + # This prevents the user from tampering with the session but also allows them to see its contents. # # Do not put secret information in cookie-based sessions! # @@ -200,7 +201,7 @@ module ActionController end MODULES = [ - AbstractController::Layouts, + AbstractController::Rendering, AbstractController::Translation, AbstractController::AssetPaths, @@ -208,6 +209,7 @@ module ActionController HideActions, UrlFor, Redirecting, + ActionView::Layouts, Rendering, Renderers::All, ConditionalGet, @@ -248,10 +250,17 @@ module ActionController end # Define some internal variables that should not be propagated to the view. - self.protected_instance_variables = [ + PROTECTED_IVARS = AbstractController::Rendering::DEFAULT_PROTECTED_INSTANCE_VARIABLES + [ :@_status, :@_headers, :@_params, :@_env, :@_response, :@_request, - :@_view_runtime, :@_stream, :@_url_options, :@_action_has_layout - ] + :@_view_runtime, :@_stream, :@_url_options, :@_action_has_layout ] + + def _protected_ivars # :nodoc: + PROTECTED_IVARS + end + + def self.protected_instance_variables + PROTECTED_IVARS + end 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 5352e5ffe2..12d798d0c1 100644 --- a/actionpack/lib/action_controller/caching.rb +++ b/actionpack/lib/action_controller/caching.rb @@ -9,7 +9,7 @@ module ActionController # You can read more about each approach by clicking the modules below. # # Note: To turn off all caching, set - # config.action_controller.perform_caching = false. + # config.action_controller.perform_caching = false # # == \Caching stores # diff --git a/actionpack/lib/action_controller/caching/fragments.rb b/actionpack/lib/action_controller/caching/fragments.rb index 879d5fdd94..2694d4c12f 100644 --- a/actionpack/lib/action_controller/caching/fragments.rb +++ b/actionpack/lib/action_controller/caching/fragments.rb @@ -90,7 +90,13 @@ module ActionController end def instrument_fragment_cache(name, key) # :nodoc: - ActiveSupport::Notifications.instrument("#{name}.action_controller", :key => key){ yield } + payload = { + controller: controller_name, + action: action_name, + key: key + } + + ActiveSupport::Notifications.instrument("#{name}.action_controller", payload) { yield } end end end diff --git a/actionpack/lib/action_controller/log_subscriber.rb b/actionpack/lib/action_controller/log_subscriber.rb index 9279d8bcea..b1acca2435 100644 --- a/actionpack/lib/action_controller/log_subscriber.rb +++ b/actionpack/lib/action_controller/log_subscriber.rb @@ -50,7 +50,16 @@ module ActionController def unpermitted_parameters(event) unpermitted_keys = event.payload[:keys] - debug("Unpermitted parameters: #{unpermitted_keys.join(", ")}") + debug("Unpermitted parameter#{'s' if unpermitted_keys.size > 1}: #{unpermitted_keys.join(", ")}") + end + + def deep_munge(event) + message = "Value for params[:#{event.payload[:keys].join('][:')}] was set "\ + "to nil, because it was one of [], [null] or [null, null, ...]. "\ + "Go to http://guides.rubyonrails.org/security.html#unsafe-query-generation "\ + "for more information."\ + + debug(message) end %w(write_fragment read_fragment exist_fragment? diff --git a/actionpack/lib/action_controller/metal.rb b/actionpack/lib/action_controller/metal.rb index b84c9e78c3..696fbf6e09 100644 --- a/actionpack/lib/action_controller/metal.rb +++ b/actionpack/lib/action_controller/metal.rb @@ -70,7 +70,8 @@ module ActionController # can do the following: # # class HelloController < ActionController::Metal - # include ActionController::Rendering + # include AbstractController::Rendering + # include ActionView::Layouts # append_view_path "#{Rails.root}/app/views" # # def index @@ -231,5 +232,9 @@ module ActionController new.dispatch(name, klass.new(env)) end end + + def _status_code + @_status + end end end diff --git a/actionpack/lib/action_controller/metal/data_streaming.rb b/actionpack/lib/action_controller/metal/data_streaming.rb index 75c4d3ef99..1abd8d3a33 100644 --- a/actionpack/lib/action_controller/metal/data_streaming.rb +++ b/actionpack/lib/action_controller/metal/data_streaming.rb @@ -96,7 +96,7 @@ module ActionController #:nodoc: end # Sends the given binary data to the browser. This method is similar to - # <tt>render text: data</tt>, but also allows you to specify whether + # <tt>render plain: data</tt>, but also allows you to specify whether # the browser should display the response as a file attachment (i.e. in a # download dialog) or as inline data. You may also set the content type, # the apparent file name, and other things. diff --git a/actionpack/lib/action_controller/metal/flash.rb b/actionpack/lib/action_controller/metal/flash.rb index b078beb675..65351284b9 100644 --- a/actionpack/lib/action_controller/metal/flash.rb +++ b/actionpack/lib/action_controller/metal/flash.rb @@ -11,6 +11,23 @@ module ActionController #:nodoc: end module ClassMethods + # Creates new flash types. You can pass as many types as you want to create + # flash types other than the default <tt>alert</tt> and <tt>notice</tt> in + # your controllers and views. For instance: + # + # # in application_controller.rb + # class ApplicationController < ActionController::Base + # add_flash_types :warning + # end + # + # # in your controller + # redirect_to user_path(@user), warning: "Incomplete profile" + # + # # in your view + # <%= warning %> + # + # This method will automatically define a new method for each of the given + # names, and it will be available in your views. def add_flash_types(*types) types.each do |type| next if _flash_types.include?(type) @@ -20,7 +37,7 @@ module ActionController #:nodoc: end helper_method type - _flash_types << type + self._flash_types += [type] end end end diff --git a/actionpack/lib/action_controller/metal/force_ssl.rb b/actionpack/lib/action_controller/metal/force_ssl.rb index b8afce42c9..a2cb6d1e66 100644 --- a/actionpack/lib/action_controller/metal/force_ssl.rb +++ b/actionpack/lib/action_controller/metal/force_ssl.rb @@ -48,7 +48,7 @@ module ActionController # You can pass any of the following options to affect the redirect status and response # * <tt>status</tt> - Redirect with a custom status (default is 301 Moved Permanently) # * <tt>flash</tt> - Set a flash message when redirecting - # * <tt>alert</tt> - Set a alert message when redirecting + # * <tt>alert</tt> - Set an alert message when redirecting # * <tt>notice</tt> - Set a notice message when redirecting # # ==== Action Options diff --git a/actionpack/lib/action_controller/metal/head.rb b/actionpack/lib/action_controller/metal/head.rb index 8237db15ca..84a9112144 100644 --- a/actionpack/lib/action_controller/metal/head.rb +++ b/actionpack/lib/action_controller/metal/head.rb @@ -1,8 +1,6 @@ module ActionController module Head - extend ActiveSupport::Concern - - # Return a response that has no content (merely headers). The options + # Returns a response that has no content (merely headers). The options # argument is interpreted to be a hash of header names and values. # This allows you to easily return a response that consists only of # significant headers: @@ -29,7 +27,7 @@ module ActionController self.status = status self.location = url_for(location) if location - if include_content?(self.status) + if include_content?(self._status_code) self.content_type = content_type || (Mime[formats.first] if formats) self.response.charset = false if self.response self.response_body = " " diff --git a/actionpack/lib/action_controller/metal/helpers.rb b/actionpack/lib/action_controller/metal/helpers.rb index 243fd40a7e..a9c3e438fb 100644 --- a/actionpack/lib/action_controller/metal/helpers.rb +++ b/actionpack/lib/action_controller/metal/helpers.rb @@ -5,7 +5,7 @@ module ActionController # # In addition to using the standard template helpers provided, creating custom helpers to # extract complicated logic or reusable functionality is strongly encouraged. By default, each controller - # will include all helpers. + # will include all helpers. These helpers are only accessible on the controller through <tt>.helpers</tt> # # In previous versions of \Rails the controller will include a helper whose # name matches that of the controller, e.g., <tt>MyController</tt> will automatically @@ -73,7 +73,11 @@ module ActionController # Provides a proxy to access helpers methods from outside the view. def helpers - @helper_proxy ||= ActionView::Base.new.extend(_helpers) + @helper_proxy ||= begin + proxy = ActionView::Base.new + proxy.config = config.inheritable_copy + proxy.extend(_helpers) + end end # Overwrite modules_for_helpers to accept :all as argument, which loads diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb index e7be751cd8..3111992f82 100644 --- a/actionpack/lib/action_controller/metal/http_authentication.rb +++ b/actionpack/lib/action_controller/metal/http_authentication.rb @@ -11,11 +11,11 @@ module ActionController # http_basic_authenticate_with name: "dhh", password: "secret", except: :index # # def index - # render text: "Everyone can see me!" + # render plain: "Everyone can see me!" # end # # def edit - # render text: "I'm only accessible if you know the password" + # render plain: "I'm only accessible if you know the password" # end # end # @@ -100,7 +100,7 @@ module ActionController end def user_name_and_password(request) - decode_credentials(request).split(/:/, 2) + decode_credentials(request).split(':', 2) end def decode_credentials(request) @@ -139,11 +139,11 @@ module ActionController # before_action :authenticate, except: [:index] # # def index - # render text: "Everyone can see me!" + # render plain: "Everyone can see me!" # end # # def edit - # render text: "I'm only accessible if you know the password" + # render plain: "I'm only accessible if you know the password" # end # # private @@ -333,11 +333,11 @@ module ActionController # before_action :authenticate, except: [ :index ] # # def index - # render text: "Everyone can see me!" + # render plain: "Everyone can see me!" # end # # def edit - # render text: "I'm only accessible if you know the password" + # render plain: "I'm only accessible if you know the password" # end # # private diff --git a/actionpack/lib/action_controller/metal/instrumentation.rb b/actionpack/lib/action_controller/metal/instrumentation.rb index d3aa8f90c5..b0e164bc57 100644 --- a/actionpack/lib/action_controller/metal/instrumentation.rb +++ b/actionpack/lib/action_controller/metal/instrumentation.rb @@ -67,7 +67,7 @@ module ActionController private - # A hook invoked everytime a before callback is halted. + # A hook invoked every time a before callback is halted. def halted_callback_hook(filter) ActiveSupport::Notifications.instrument("halted_callback.action_controller", :filter => filter) end diff --git a/actionpack/lib/action_controller/metal/live.rb b/actionpack/lib/action_controller/metal/live.rb index 8092fd639f..4c0554d27b 100644 --- a/actionpack/lib/action_controller/metal/live.rb +++ b/actionpack/lib/action_controller/metal/live.rb @@ -1,5 +1,6 @@ require 'action_dispatch/http/response' require 'delegate' +require 'active_support/json' module ActionController # Mix this module in to your controller, and all actions in that controller @@ -32,9 +33,86 @@ module ActionController # the main thread. Make sure your actions are thread safe, and this shouldn't # be a problem (don't share state across threads, etc). module Live + # This class provides the ability to write an SSE (Server Sent Event) + # to an IO stream. The class is initialized with a stream and can be used + # to either write a JSON string or an object which can be converted to JSON. + # + # Writing an object will convert it into standard SSE format with whatever + # options you have configured. You may choose to set the following options: + # + # 1) Event. If specified, an event with this name will be dispatched on + # the browser. + # 2) Retry. The reconnection time in milliseconds used when attempting + # to send the event. + # 3) Id. If the connection dies while sending an SSE to the browser, then + # the server will receive a +Last-Event-ID+ header with value equal to +id+. + # + # After setting an option in the constructor of the SSE object, all future + # SSEs sent across the stream will use those options unless overridden. + # + # Example Usage: + # + # class MyController < ActionController::Base + # include ActionController::Live + # + # def index + # response.headers['Content-Type'] = 'text/event-stream' + # sse = SSE.new(response.stream, retry: 300, event: "event-name") + # sse.write({ name: 'John'}) + # sse.write({ name: 'John'}, id: 10) + # sse.write({ name: 'John'}, id: 10, event: "other-event") + # sse.write({ name: 'John'}, id: 10, event: "other-event", retry: 500) + # ensure + # sse.close + # end + # end + # + # Note: SSEs are not currently supported by IE. However, they are supported + # by Chrome, Firefox, Opera, and Safari. + class SSE + + WHITELISTED_OPTIONS = %w( retry event id ) + + def initialize(stream, options = {}) + @stream = stream + @options = options + end + + def close + @stream.close + end + + def write(object, options = {}) + case object + when String + perform_write(object, options) + else + perform_write(ActiveSupport::JSON.encode(object), options) + end + end + + private + + def perform_write(json, options) + current_options = @options.merge(options).stringify_keys + + WHITELISTED_OPTIONS.each do |option_name| + if (option_value = current_options[option_name]) + @stream.write "#{option_name}: #{option_value}\n" + end + end + + message = json.gsub("\n", "\ndata: ") + @stream.write "data: #{message}\n\n" + end + end + class Buffer < ActionDispatch::Response::Buffer #:nodoc: + include MonitorMixin + def initialize(response) - @error_callback = nil + @error_callback = lambda { true } + @cv = new_cond super(response, SizedQueue.new(10)) end @@ -48,14 +126,25 @@ module ActionController end def each + @response.sending! while str = @buf.pop yield str end + @response.sent! end def close - super - @buf.push nil + synchronize do + super + @buf.push nil + @cv.broadcast + end + end + + def await_close + synchronize do + @cv.wait_until { @closed } + end end def on_error(&block) @@ -91,12 +180,20 @@ module ActionController end end - def commit! - headers.freeze + private + + def before_committed super + jar = request.cookie_jar + # The response can be committed multiple times + jar.write self unless committed? end - private + def before_sending + super + request.cookie_jar.commit! + headers.freeze + end def build_buffer(response, body) buf = Live::Buffer.new response @@ -117,6 +214,7 @@ module ActionController t1 = Thread.current locals = t1.keys.map { |key| [key, t1[key]] } + error = nil # This processes the action in a child thread. It lets us return the # response code and headers back up the rack stack, and still process # the body in parallel with sending data to the client @@ -131,14 +229,18 @@ module ActionController begin super(name) rescue => e - begin - @_response.stream.write(ActionView::Base.streaming_completion_on_exception) if request.format == :html - @_response.stream.call_on_error - rescue => exception - log_error(exception) - ensure - log_error(e) - @_response.stream.close + if @_response.committed? + begin + @_response.stream.write(ActionView::Base.streaming_completion_on_exception) if request.format == :html + @_response.stream.call_on_error + rescue => exception + log_error(exception) + ensure + log_error(e) + @_response.stream.close + end + else + error = e end ensure @_response.commit! @@ -146,6 +248,7 @@ module ActionController } @_response.await_commit + raise error if error end def log_error(exception) @@ -160,7 +263,7 @@ module ActionController def response_body=(body) super - response.stream.close if response + response.close if response end def set_response!(request) diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index 834d44f045..1974bbf529 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -181,6 +181,73 @@ module ActionController #:nodoc: # end # end # + # Formats can have different variants. + # + # The request variant is a specialization of the request format, like <tt>:tablet</tt>, + # <tt>:phone</tt>, or <tt>:desktop</tt>. + # + # We often want to render different html/json/xml templates for phones, + # tablets, and desktop browsers. Variants make it easy. + # + # You can set the variant in a +before_action+: + # + # request.variant = :tablet if request.user_agent =~ /iPad/ + # + # Respond to variants in the action just like you respond to formats: + # + # respond_to do |format| + # format.html do |variant| + # variant.tablet # renders app/views/projects/show.html+tablet.erb + # variant.phone { extra_setup; render ... } + # variant.none { special_setup } # executed only if there is no variant set + # end + # end + # + # Provide separate templates for each format and variant: + # + # app/views/projects/show.html.erb + # app/views/projects/show.html+tablet.erb + # app/views/projects/show.html+phone.erb + # + # When you're not sharing any code within the format, you can simplify defining variants + # using the inline syntax: + # + # respond_to do |format| + # format.js { render "trash" } + # format.html.phone { redirect_to progress_path } + # format.html.none { render "trash" } + # end + # + # Variants also support common `any`/`all` block that formats have. + # + # It works for both inline: + # + # respond_to do |format| + # format.html.any { render text: "any" } + # format.html.phone { render text: "phone" } + # end + # + # and block syntax: + # + # respond_to do |format| + # format.html do |variant| + # variant.any(:tablet, :phablet){ render text: "any" } + # variant.phone { render text: "phone" } + # end + # end + # + # You can also set an array of variants: + # + # request.variant = [:tablet, :phone] + # + # which will work similarly to formats and MIME types negotiation. If there will be no + # :tablet variant declared, :phone variant will be picked: + # + # respond_to do |format| + # format.html.none + # format.html.phone # this gets rendered + # end + # # Be sure to check the documentation of +respond_with+ and # <tt>ActionController::MimeResponds.respond_to</tt> for more examples. def respond_to(*mimes, &block) @@ -260,7 +327,7 @@ module ActionController #:nodoc: # * for other requests - i.e. data formats such as xml, json, csv etc, if # the resource passed to +respond_with+ responds to <code>to_<format></code>, # the method attempts to render the resource in the requested format - # directly, e.g. for an xml request, the response is equivalent to calling + # directly, e.g. for an xml request, the response is equivalent to calling # <code>render xml: resource</code>. # # === Nested resources @@ -321,11 +388,14 @@ module ActionController #:nodoc: # 2. <tt>:action</tt> - overwrites the default render action used after an # unsuccessful html +post+ request. def respond_with(*resources, &block) - 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 self.class.mimes_for_respond_to.empty? + raise "In order to use respond_with, first you need to declare the " \ + "formats your controller responds to in the class level." + end if collector = retrieve_collector_from_mimes(&block) options = resources.size == 1 ? {} : resources.extract_options! + options = options.clone options[:default_response] = collector.response (options.delete(:responder) || self.class.responder).call(self, resources, options) end @@ -359,14 +429,12 @@ module ActionController #:nodoc: # is available. def retrieve_collector_from_mimes(mimes=nil, &block) #:nodoc: mimes ||= collect_mimes_from_class_level - collector = Collector.new(mimes) + collector = Collector.new(mimes, request.variant) block.call(collector) if block_given? format = collector.negotiate_format(request) if format - self.content_type ||= format.to_s - lookup_context.formats = [format.to_sym] - lookup_context.rendered_format = lookup_context.formats.first + _process_format(format) collector else raise ActionController::UnknownFormat @@ -397,11 +465,13 @@ module ActionController #:nodoc: # request, with this response then being accessible by calling #response. class Collector include AbstractController::Collector - attr_accessor :order, :format + attr_accessor :format - def initialize(mimes) - @order, @responses = [], {} - mimes.each { |mime| send(mime) } + def initialize(mimes, variant = nil) + @responses = {} + @variant = variant + + mimes.each { |mime| @responses["Mime::#{mime.upcase}".constantize] = nil } end def any(*args, &block) @@ -415,16 +485,62 @@ module ActionController #:nodoc: def custom(mime_type, &block) mime_type = Mime::Type.lookup(mime_type.to_s) unless mime_type.is_a?(Mime::Type) - @order << mime_type - @responses[mime_type] ||= block + @responses[mime_type] ||= if block_given? + block + else + VariantCollector.new(@variant) + end end def response - @responses.fetch(format, @responses[Mime::ALL]) + response = @responses.fetch(format, @responses[Mime::ALL]) + if response.is_a?(VariantCollector) # `format.html.phone` - variant inline syntax + response.variant + elsif response.nil? || response.arity == 0 # `format.html` - just a format, call its block + response + else # `format.html{ |variant| variant.phone }` - variant block syntax + variant_collector = VariantCollector.new(@variant) + response.call(variant_collector) # call format block with variants collector + variant_collector.variant + end end def negotiate_format(request) - @format = request.negotiate_mime(order) + @format = request.negotiate_mime(@responses.keys) + end + + class VariantCollector #:nodoc: + def initialize(variant = nil) + @variant = variant + @variants = {} + end + + def any(*args, &block) + if block_given? + if args.any? && args.none?{ |a| a == @variant } + args.each{ |v| @variants[v] = block } + else + @variants[:any] = block + end + end + end + alias :all :any + + def method_missing(name, *args, &block) + @variants[name] = block if block_given? + end + + def variant + if @variant.nil? + @variants[:none] || @variants[:any] + elsif (@variants.keys & @variant).any? + @variant.each do |v| + return @variants[v] if @variants.key?(v) + end + else + @variants[:any] + end + end end end end diff --git a/actionpack/lib/action_controller/metal/params_wrapper.rb b/actionpack/lib/action_controller/metal/params_wrapper.rb index c9f1d8dcb4..2ca8955741 100644 --- a/actionpack/lib/action_controller/metal/params_wrapper.rb +++ b/actionpack/lib/action_controller/metal/params_wrapper.rb @@ -231,7 +231,12 @@ module ActionController # by the metal call stack. def process_action(*args) if _wrapper_enabled? - wrapped_hash = _wrap_parameters request.request_parameters + if request.parameters[_wrapper_key].present? + wrapped_hash = _extract_parameters(request.parameters) + else + wrapped_hash = _wrap_parameters request.request_parameters + end + wrapped_keys = request.request_parameters.keys wrapped_filtered_hash = _wrap_parameters request.filtered_parameters.slice(*wrapped_keys) @@ -259,14 +264,16 @@ module ActionController # Returns the list of parameters which will be selected for wrapped. def _wrap_parameters(parameters) - value = if include_only = _wrapper_options.include + { _wrapper_key => _extract_parameters(parameters) } + end + + def _extract_parameters(parameters) + if include_only = _wrapper_options.include parameters.slice(*include_only) else exclude = _wrapper_options.exclude || [] parameters.except(*(exclude + EXCLUDE_PARAMETERS)) end - - { _wrapper_key => value } end # Checks if we should perform parameters wrapping. diff --git a/actionpack/lib/action_controller/metal/rack_delegation.rb b/actionpack/lib/action_controller/metal/rack_delegation.rb index bdf6e88699..6921834044 100644 --- a/actionpack/lib/action_controller/metal/rack_delegation.rb +++ b/actionpack/lib/action_controller/metal/rack_delegation.rb @@ -6,7 +6,7 @@ module ActionController extend ActiveSupport::Concern delegate :headers, :status=, :location=, :content_type=, - :status, :location, :content_type, :to => "@_response" + :status, :location, :content_type, :_status_code, :to => "@_response" def dispatch(action, request) set_response!(request) diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb index e9031f3fac..2812038938 100644 --- a/actionpack/lib/action_controller/metal/redirecting.rb +++ b/actionpack/lib/action_controller/metal/redirecting.rb @@ -58,7 +58,7 @@ module ActionController # redirect_to post_url(@post), alert: "Watch it, mister!" # redirect_to post_url(@post), status: :found, notice: "Pay attention to the road" # redirect_to post_url(@post), status: 301, flash: { updated_post_id: @post.id } - # redirect_to { action: 'atom' }, alert: "Something serious happened" + # redirect_to({ action: 'atom' }, alert: "Something serious happened") # # When using <tt>redirect_to :back</tt>, if there is no referrer, ActionController::RedirectBackError will be raised. You may specify some fallback # behavior for this case by rescuing ActionController::RedirectBackError. @@ -71,6 +71,26 @@ module ActionController self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.h(location)}\">redirected</a>.</body></html>" end + def _compute_redirect_to_location(options) #:nodoc: + case options + # 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 (":"). + # See http://tools.ietf.org/html/rfc3986#section-3.1 + # The protocol relative scheme starts with a double slash "//". + when /\A([a-z][a-z\d\-+\.]*:|\/\/).*/i + options + when String + request.protocol + request.host_with_port + options + when :back + request.headers["Referer"] or raise RedirectBackError + when Proc + _compute_redirect_to_location options.call + else + url_for(options) + end.delete("\0\r\n") + end + private def _extract_redirect_to_status(options, response_status) if options.is_a?(Hash) && options.key?(:status) @@ -81,24 +101,5 @@ module ActionController 302 end end - - def _compute_redirect_to_location(options) - case options - # 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 (":"). - # The protocol relative scheme starts with a double slash "//" - when %r{\A(\w[\w+.-]*:|//).*} - options - when String - request.protocol + request.host_with_port + options - when :back - request.headers["Referer"] or raise RedirectBackError - when Proc - _compute_redirect_to_location options.call - else - url_for(options) - end.delete("\0\r\n") - end end end diff --git a/actionpack/lib/action_controller/metal/renderers.rb b/actionpack/lib/action_controller/metal/renderers.rb index 5272dc6cdb..29ce5abd55 100644 --- a/actionpack/lib/action_controller/metal/renderers.rb +++ b/actionpack/lib/action_controller/metal/renderers.rb @@ -6,6 +6,17 @@ module ActionController Renderers.add(key, &block) end + # See <tt>Renderers.remove</tt> + def self.remove_renderer(key) + Renderers.remove(key) + end + + class MissingRenderer < LoadError + def initialize(format) + super "No renderer defined for format: #{format}" + end + end + module Renderers extend ActiveSupport::Concern @@ -36,8 +47,8 @@ module ActionController nil end - # Hash of available renderers, mapping a renderer name to its proc. - # Default keys are :json, :js, :xml. + # A Set containing renderer names that correspond to available renderer procs. + # Default values are <tt>:json</tt>, <tt>:js</tt>, <tt>:xml</tt>. RENDERERS = Set.new # Adds a new renderer to call within controller actions. @@ -77,6 +88,17 @@ module ActionController RENDERERS << key.to_sym end + # This method is the opposite of add method. + # + # Usage: + # + # ActionController::Renderers.remove(:csv) + def self.remove(key) + RENDERERS.delete(key.to_sym) + method = "_render_option_#{key}" + remove_method(method) if method_defined?(method) + end + module All extend ActiveSupport::Concern include Renderers diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb index bea6b88f91..93e7d6954c 100644 --- a/actionpack/lib/action_controller/metal/rendering.rb +++ b/actionpack/lib/action_controller/metal/rendering.rb @@ -2,7 +2,7 @@ module ActionController module Rendering extend ActiveSupport::Concern - include AbstractController::Rendering + RENDER_FORMATS_IN_PRIORITY = [:body, :text, :plain, :html] # Before processing, set the request formats in current controller formats. def process_action(*) #:nodoc: @@ -12,29 +12,46 @@ module ActionController # Check for double render errors and set the content_type after rendering. def render(*args) #:nodoc: - raise ::AbstractController::DoubleRenderError if response_body + raise ::AbstractController::DoubleRenderError if self.response_body super - self.content_type ||= Mime[lookup_context.rendered_format].to_s - response_body end # Overwrite render_to_string because body can now be set to a rack body. def render_to_string(*) - if self.response_body = super + result = super + if result.respond_to?(:each) string = "" - response_body.each { |r| string << r } + result.each { |r| string << r } string + else + result end - ensure - self.response_body = nil end - def render_to_body(*) - super || " " + def render_to_body(options = {}) + super || _render_in_priorities(options) || ' ' end private + def _render_in_priorities(options) + RENDER_FORMATS_IN_PRIORITY.each do |format| + return options[format] if options.key?(format) + end + + nil + end + + def _process_format(format, options = {}) + super + + if options[:plain] + self.content_type = Mime::TEXT + else + self.content_type ||= format.to_s + end + end + # Normalize arguments by catching blocks and setting them on :update. def _normalize_args(action=nil, options={}, &blk) #:nodoc: options = super @@ -44,12 +61,14 @@ module ActionController # Normalize both text and status options. def _normalize_options(options) #:nodoc: - if options.key?(:text) && options[:text].respond_to?(:to_text) - options[:text] = options[:text].to_text + _normalize_text(options) + + if options[:html] + options[:html] = ERB::Util.html_escape(options[:html]) end - if options.delete(:nothing) || (options.key?(:text) && options[:text].nil?) - options[:text] = " " + if options.delete(:nothing) || _any_render_format_is_nil?(options) + options[:body] = " " end if options[:status] @@ -59,6 +78,18 @@ module ActionController super end + def _normalize_text(options) + RENDER_FORMATS_IN_PRIORITY.each do |format| + if options.key?(format) && options[format].respond_to?(:to_text) + options[format] = options[format].to_text + end + end + end + + def _any_render_format_is_nil?(options) + RENDER_FORMATS_IN_PRIORITY.any? { |format| options.key?(format) && options[format].nil? } + end + # Process controller specific options, as status, content-type and location. def _process_options(options) #:nodoc: status, content_type, location = options.values_at(:status, :content_type, :location) diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index 573c739da4..1355fe87d0 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -5,14 +5,24 @@ module ActionController #:nodoc: class InvalidAuthenticityToken < ActionControllerError #:nodoc: end + class InvalidCrossOriginRequest < ActionControllerError #:nodoc: + end + # Controller actions are protected from Cross-Site Request Forgery (CSRF) attacks # by including a token in the rendered html for your application. This token is # stored as a random string in the session, to which an attacker does not have # access. When a request reaches your application, \Rails verifies the received # token with the token in the session. Only HTML and JavaScript requests are checked, # so this will not protect your XML API (presumably you'll have a different - # authentication scheme there anyway). Also, GET requests are not protected as these - # should be idempotent. + # authentication scheme there anyway). + # + # GET requests are not protected since they don't have side effects like writing + # to the database and don't leak sensitive information. JavaScript requests are + # an exception: a third-party site can use a <script> tag to reference a JavaScript + # URL on your site. When your JavaScript response loads on their site, it executes. + # With carefully crafted JavaScript on their end, sensitive data in your JavaScript + # response may be extracted. To prevent this, only XmlHttpRequest (known as XHR or + # Ajax) requests are allowed to make GET requests for JavaScript responses. # # It's important to remember that XML or JSON requests are also affected and if # you're building an API you'll need something like: @@ -58,6 +68,10 @@ module ActionController #:nodoc: config_accessor :allow_forgery_protection self.allow_forgery_protection = true if allow_forgery_protection.nil? + # Controls whether a CSRF failure logs a warning. On by default. + config_accessor :log_warning_on_csrf_failure + self.log_warning_on_csrf_failure = true + helper_method :form_authenticity_token helper_method :protect_against_forgery? end @@ -65,17 +79,16 @@ module ActionController #:nodoc: module ClassMethods # Turn on request forgery protection. Bear in mind that only non-GET, HTML/JavaScript requests are checked. # + # class ApplicationController < ActionController::Base + # protect_from_forgery + # end + # # class FooController < ApplicationController # protect_from_forgery except: :index # - # You can disable csrf protection on controller-by-controller basis: - # + # You can disable CSRF protection on controller by skipping the verification before_action: # skip_before_action :verify_authenticity_token # - # It can also be disabled for specific controller actions: - # - # skip_before_action :verify_authenticity_token, except: [:create] - # # Valid Options: # # * <tt>:only/:except</tt> - Passed to the <tt>before_action</tt> call. Set which actions are verified. @@ -89,6 +102,7 @@ module ActionController #:nodoc: self.forgery_protection_strategy = protection_method_class(options[:with] || :null_session) self.request_forgery_protection_token ||= :authenticity_token prepend_before_action :verify_authenticity_token, options + append_after_action :verify_same_origin_request end private @@ -124,6 +138,9 @@ module ActionController #:nodoc: @loaded = true end + # no-op + def destroy; end + def exists? true end @@ -166,18 +183,63 @@ module ActionController #:nodoc: end protected + # The actual before_action that is used to verify the CSRF token. + # Don't override this directly. Provide your own forgery protection + # strategy instead. If you override, you'll disable same-origin + # `<script>` verification. + # + # Lean on the protect_from_forgery declaration to mark which actions are + # due for same-origin request verification. If protect_from_forgery is + # enabled on an action, this before_action flags its after_action to + # verify that JavaScript responses are for XHR requests, ensuring they + # follow the browser's same-origin policy. + def verify_authenticity_token + mark_for_same_origin_verification! + + if !verified_request? + if logger && log_warning_on_csrf_failure + logger.warn "Can't verify CSRF token authenticity" + end + handle_unverified_request + end + end + def handle_unverified_request forgery_protection_strategy.new(self).handle_unverified_request end - # The actual before_action that is used. Modify this to change how you handle unverified requests. - def verify_authenticity_token - unless verified_request? - logger.warn "Can't verify CSRF token authenticity" if logger - handle_unverified_request + CROSS_ORIGIN_JAVASCRIPT_WARNING = "Security warning: an embedded " \ + "<script> tag on another site requested protected JavaScript. " \ + "If you know what you're doing, go ahead and disable forgery " \ + "protection on this action to permit cross-origin JavaScript embedding." + private_constant :CROSS_ORIGIN_JAVASCRIPT_WARNING + + # If `verify_authenticity_token` was run (indicating that we have + # forgery protection enabled for this request) then also verify that + # we aren't serving an unauthorized cross-origin response. + def verify_same_origin_request + if marked_for_same_origin_verification? && non_xhr_javascript_response? + logger.warn CROSS_ORIGIN_JAVASCRIPT_WARNING if logger + raise ActionController::InvalidCrossOriginRequest, CROSS_ORIGIN_JAVASCRIPT_WARNING end end + # GET requests are checked for cross-origin JavaScript after rendering. + def mark_for_same_origin_verification! + @marked_for_same_origin_verification = request.get? + end + + # If the `verify_authenticity_token` before_action ran, verify that + # JavaScript responses are only served to same-origin GET requests. + def marked_for_same_origin_verification? + @marked_for_same_origin_verification ||= false + end + + # Check for cross-origin JavaScript responses. + def non_xhr_javascript_response? + content_type =~ %r(\Atext/javascript) && !request.xhr? + end + # Returns true or false if a request is verified. Checks: # # * is it a GET or HEAD request? Gets should be safe and idempotent @@ -185,7 +247,7 @@ module ActionController #:nodoc: # * Does the X-CSRF-Token header match the form_authenticity_token def verified_request? !protect_against_forgery? || request.get? || request.head? || - form_authenticity_token == params[request_forgery_protection_token] || + form_authenticity_token == form_authenticity_param || form_authenticity_token == request.headers['X-CSRF-Token'] end diff --git a/actionpack/lib/action_controller/metal/responder.rb b/actionpack/lib/action_controller/metal/responder.rb index fd5b661209..5096558c67 100644 --- a/actionpack/lib/action_controller/metal/responder.rb +++ b/actionpack/lib/action_controller/metal/responder.rb @@ -22,7 +22,7 @@ module ActionController #:nodoc: # # 3) if the responder does not <code>respond_to :to_xml</code>, call <code>#to_format</code> on it. # - # === Builtin HTTP verb semantics + # === Built-in HTTP verb semantics # # The default \Rails responder holds semantics for each HTTP verb. Depending on the # content type, verb and the resource status, it will behave differently. @@ -144,7 +144,7 @@ module ActionController #:nodoc: undef_method(:to_json) if method_defined?(:to_json) undef_method(:to_yaml) if method_defined?(:to_yaml) - # Initializes a new responder an invoke the proper format. If the format is + # Initializes a new responder and invokes the proper format. If the format is # not defined, call to_format. # def self.call(*args) @@ -202,6 +202,7 @@ module ActionController #:nodoc: # This is the common behavior for formats associated with APIs, such as :xml and :json. def api_behavior(error) raise error unless resourceful? + raise MissingRenderer.new(format) unless has_renderer? if get? display resource @@ -269,6 +270,11 @@ module ActionController #:nodoc: resource.respond_to?(:errors) && !resource.errors.empty? end + # Check whether the necessary Renderer is available + def has_renderer? + Renderers::RENDERERS.include?(format) + end + # By default, render the <code>:edit</code> action for HTML requests with errors, unless # the verb was POST. # diff --git a/actionpack/lib/action_controller/metal/streaming.rb b/actionpack/lib/action_controller/metal/streaming.rb index 73e9b5660d..62d5931b45 100644 --- a/actionpack/lib/action_controller/metal/streaming.rb +++ b/actionpack/lib/action_controller/metal/streaming.rb @@ -193,31 +193,29 @@ module ActionController #:nodoc: module Streaming extend ActiveSupport::Concern - include AbstractController::Rendering - protected - # Set proper cache control and transfer encoding when streaming - def _process_options(options) #:nodoc: - super - if options[:stream] - if env["HTTP_VERSION"] == "HTTP/1.0" - options.delete(:stream) - else - headers["Cache-Control"] ||= "no-cache" - headers["Transfer-Encoding"] = "chunked" - headers.delete("Content-Length") + # Set proper cache control and transfer encoding when streaming + def _process_options(options) #:nodoc: + super + if options[:stream] + if env["HTTP_VERSION"] == "HTTP/1.0" + options.delete(:stream) + else + headers["Cache-Control"] ||= "no-cache" + headers["Transfer-Encoding"] = "chunked" + headers.delete("Content-Length") + end end end - end - # 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) - else - super + # 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) + else + super + end end - end end end diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index 44703221f3..d86d49c9dc 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -3,6 +3,7 @@ require 'active_support/core_ext/array/wrap' require 'active_support/rescuable' require 'action_dispatch/http/upload' require 'stringio' +require 'set' module ActionController # Raised when a required parameter is missing. @@ -17,7 +18,7 @@ module ActionController def initialize(param) # :nodoc: @param = param - super("param not found: #{param}") + super("param is missing or the value is empty: #{param}") end end @@ -31,7 +32,7 @@ module ActionController def initialize(params) # :nodoc: @params = params - super("found unpermitted parameters: #{params.join(", ")}") + super("found unpermitted parameter#{'s' if params.size > 1 }: #{params.join(", ")}") end end @@ -125,6 +126,13 @@ module ActionController @permitted = self.class.permit_all_parameters end + # Attribute that keeps track of converted arrays, if any, to avoid double + # looping in the common use case permit + mass-assignment. Defined in a + # method to instantiate it only if needed. + def converted_arrays + @converted_arrays ||= Set.new + end + # Returns +true+ if the parameter is permitted, +false+ otherwise. # # params = ActionController::Parameters.new @@ -149,8 +157,10 @@ module ActionController # Person.new(params) # => #<Person id: nil, name: "Francesco"> def permit! each_pair do |key, value| - convert_hashes_to_parameters(key, value) - self[key].permit! if self[key].respond_to? :permit! + value = convert_hashes_to_parameters(key, value) + Array.wrap(value).each do |_| + _.permit! if _.respond_to? :permit! + end end @permitted = true @@ -201,6 +211,7 @@ module ActionController # You may declare that the parameter should be an array of permitted scalars # by mapping it to an empty array: # + # params = ActionController::Parameters.new(tags: ['rails', 'parameters']) # params.permit(tags: []) # # You can also use +permit+ on nested parameters, like: @@ -283,7 +294,7 @@ module ActionController # params.fetch(:none, 'Francesco') # => "Francesco" # params.fetch(:none) { 'Francesco' } # => "Francesco" def fetch(key, *args) - convert_hashes_to_parameters(key, super) + convert_hashes_to_parameters(key, super, false) rescue KeyError raise ActionController::ParameterMissing.new(key) end @@ -297,7 +308,7 @@ module ActionController # params.slice(:d) # => {} def slice(*keys) self.class.new(super).tap do |new_instance| - new_instance.instance_variable_set :@permitted, @permitted + new_instance.permitted = @permitted end end @@ -311,24 +322,38 @@ module ActionController # copy_params.permitted? # => true def dup super.tap do |duplicate| - duplicate.instance_variable_set :@permitted, @permitted + duplicate.permitted = @permitted end end + protected + def permitted=(new_permitted) + @permitted = new_permitted + end + private - def convert_hashes_to_parameters(key, value) - if value.is_a?(Parameters) || !value.is_a?(Hash) + def convert_hashes_to_parameters(key, value, assign_if_converted=true) + converted = convert_value_to_parameters(value) + self[key] = converted if assign_if_converted && !converted.equal?(value) + converted + end + + def convert_value_to_parameters(value) + if value.is_a?(Array) && !converted_arrays.member?(value) + converted = value.map { |_| convert_value_to_parameters(_) } + converted_arrays << converted + converted + elsif value.is_a?(Parameters) || !value.is_a?(Hash) value else - # Convert to Parameters on first access - self[key] = self.class.new(value) + self.class.new(value) end end def each_element(object) if object.is_a?(Array) object.map { |el| yield el }.compact - elsif object.is_a?(Hash) && object.keys.all? { |k| k =~ /\A-?\d+\z/ } + elsif fields_for_style?(object) hash = object.class.new object.each { |k,v| hash[k] = yield v } hash @@ -337,6 +362,10 @@ module ActionController end end + def fields_for_style?(object) + object.is_a?(Hash) && object.all? { |k, v| k =~ /\A-?\d+\z/ && v.is_a?(Hash) } + end + def unpermitted_parameters!(params) unpermitted_keys = unpermitted_keys(params) if unpermitted_keys.any? @@ -415,7 +444,7 @@ module ActionController # Slicing filters out non-declared keys. slice(*filter.keys).each do |key, value| - return unless value + next unless value if filter[key] == EMPTY_ARRAY # Declaration { comment_ids: [] }. @@ -473,7 +502,7 @@ module ActionController # end # end # - # In order to use <tt>accepts_nested_attribute_for</tt> with Strong \Parameters, you + # In order to use <tt>accepts_nested_attributes_for</tt> with Strong \Parameters, you # will need to specify which nested attributes should be whitelisted. # # class Person diff --git a/actionpack/lib/action_controller/metal/testing.rb b/actionpack/lib/action_controller/metal/testing.rb index 0377b8c4cf..dd8da4b5dc 100644 --- a/actionpack/lib/action_controller/metal/testing.rb +++ b/actionpack/lib/action_controller/metal/testing.rb @@ -17,7 +17,6 @@ module ActionController def recycle! @_url_options = nil - self.response_body = nil self.formats = nil self.params = nil end diff --git a/actionpack/lib/action_controller/metal/url_for.rb b/actionpack/lib/action_controller/metal/url_for.rb index 754249cbc8..37d4a96ee1 100644 --- a/actionpack/lib/action_controller/metal/url_for.rb +++ b/actionpack/lib/action_controller/metal/url_for.rb @@ -30,9 +30,9 @@ module ActionController :_recall => request.symbolized_path_parameters ).freeze - if (same_origin = _routes.equal?(env["action_dispatch.routes"])) || + if (same_origin = _routes.equal?(env["action_dispatch.routes".freeze])) || (script_name = env["ROUTES_#{_routes.object_id}_SCRIPT_NAME"]) || - (original_script_name = env['ORIGINAL_SCRIPT_NAME']) + (original_script_name = env['ORIGINAL_SCRIPT_NAME'.freeze]) @_url_options.dup.tap do |options| if original_script_name diff --git a/actionpack/lib/action_controller/railtie.rb b/actionpack/lib/action_controller/railtie.rb index 5379547c57..a2fc814221 100644 --- a/actionpack/lib/action_controller/railtie.rb +++ b/actionpack/lib/action_controller/railtie.rb @@ -1,9 +1,9 @@ require "rails" require "action_controller" require "action_dispatch/railtie" -require "action_view/railtie" require "abstract_controller/railties/routes_helpers" require "action_controller/railties/helpers" +require "action_view/railtie" module ActionController class Railtie < Rails::Railtie #:nodoc: diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index 5ed3d2ebc1..c6a8f581de 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -17,8 +17,9 @@ module ActionController @_templates = Hash.new(0) @_layouts = Hash.new(0) @_files = Hash.new(0) + @_subscribers = [] - ActiveSupport::Notifications.subscribe("render_template.action_view") do |_name, _start, _finish, _id, payload| + @_subscribers << ActiveSupport::Notifications.subscribe("render_template.action_view") do |_name, _start, _finish, _id, payload| path = payload[:layout] if path @_layouts[path] += 1 @@ -28,7 +29,7 @@ module ActionController end end - ActiveSupport::Notifications.subscribe("!render_template.action_view") do |_name, _start, _finish, _id, payload| + @_subscribers << ActiveSupport::Notifications.subscribe("!render_template.action_view") do |_name, _start, _finish, _id, payload| path = payload[:virtual_path] next unless path partial = path =~ /^.*\/_[^\/]*$/ @@ -41,7 +42,7 @@ module ActionController @_templates[path] += 1 end - ActiveSupport::Notifications.subscribe("!render_template.action_view") do |_name, _start, _finish, _id, payload| + @_subscribers << ActiveSupport::Notifications.subscribe("!render_template.action_view") do |_name, _start, _finish, _id, payload| next if payload[:virtual_path] # files don't have virtual path path = payload[:identifier] @@ -53,8 +54,9 @@ module ActionController end def teardown_subscriptions - ActiveSupport::Notifications.unsubscribe("render_template.action_view") - ActiveSupport::Notifications.unsubscribe("!render_template.action_view") + @_subscribers.each do |subscriber| + ActiveSupport::Notifications.unsubscribe(subscriber) + end end def process(*args) @@ -213,6 +215,9 @@ module ActionController # Clear the combined params hash in case it was already referenced. @env.delete("action_dispatch.request.parameters") + # Clear the filter cache variables so they're not stale + @filtered_parameters = @filtered_env = @filtered_path = nil + params = self.request_parameters.dup %w(controller action only_path).each do |k| params.delete(k) @@ -255,6 +260,29 @@ module ActionController end end + class LiveTestResponse < Live::Response + def recycle! + @body = nil + initialize + end + + def body + @body ||= super + end + + # Was the response successful? + alias_method :success?, :successful? + + # Was the URL not found? + alias_method :missing?, :not_found? + + # Were we redirected? + alias_method :redirect?, :redirection? + + # Was there a server-side error? + alias_method :error?, :server_error? + end + # Methods #destroy and #load! are overridden to avoid calling methods on the # @store object, which does not exist for the TestSession class. class TestSession < Rack::Session::Abstract::SessionHash #:nodoc: @@ -460,8 +488,8 @@ module ActionController # - +session+: A hash of parameters to store in the session. This may be +nil+. # - +flash+: A hash of parameters to store in the flash. This may be +nil+. # - # You can also simulate POST, PATCH, PUT, DELETE, HEAD, and OPTIONS requests with - # +post+, +patch+, +put+, +delete+, +head+, and +options+. + # You can also simulate POST, PATCH, PUT, DELETE, and HEAD requests with + # +post+, +patch+, +put+, +delete+, and +head+. # # Note that the request method is not verified. The different methods are # available to make the tests more expressive. @@ -522,6 +550,31 @@ module ActionController end end + # Simulate a HTTP request to +action+ by specifying request method, + # parameters and set/volley the response. + # + # - +action+: The controller action to call. + # - +http_method+: Request method used to send the http request. Possible values + # are +GET+, +POST+, +PATCH+, +PUT+, +DELETE+, +HEAD+. Defaults to +GET+. + # - +parameters+: The HTTP parameters. This may be +nil+, a hash, or a + # string that is appropriately encoded (+application/x-www-form-urlencoded+ + # or +multipart/form-data+). + # - +session+: A hash of parameters to store in the session. This may be +nil+. + # - +flash+: A hash of parameters to store in the flash. This may be +nil+. + # + # Example calling +create+ action and sending two params: + # + # process :create, 'POST', user: { name: 'Gaurish Sharma', email: 'user@example.com' } + # + # Example sending parameters, +nil+ session and setting a flash message: + # + # process :view, 'GET', { id: 7 }, nil, { notice: 'This is flash message' } + # + # To simulate +GET+, +POST+, +PATCH+, +PUT+, +DELETE+ and +HEAD+ requests + # prefer using #get, #post, #patch, #put, #delete and #head methods + # respectively which will make tests more expressive. + # + # Note that the request method is not verified. def process(action, http_method = 'GET', *args) check_required_ivars @@ -565,10 +618,13 @@ module ActionController name = @request.parameters[:action] + @controller.recycle! @controller.process(name) if cookies = @request.env['action_dispatch.cookies'] - cookies.write(@response) + unless @response.committed? + cookies.write(@response) + end end @response.prepare! @@ -579,13 +635,14 @@ module ActionController end def setup_controller_request_and_response - @request = build_request - @response = build_response - @response.request = @request - @controller = nil unless defined? @controller + response_klass = TestResponse + if klass = self.class.controller_class + if klass < ActionController::Live + response_klass = LiveTestResponse + end unless @controller begin @controller = klass.new @@ -595,6 +652,10 @@ module ActionController end end + @request = build_request + @response = build_response response_klass + @response.request = @request + if @controller @controller.request = @request @controller.params = {} @@ -605,8 +666,8 @@ module ActionController TestRequest.new end - def build_response - TestResponse.new + def build_response(klass) + klass.new end included do diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb index 24a3d4741e..11b5e6be33 100644 --- a/actionpack/lib/action_dispatch.rb +++ b/actionpack/lib/action_dispatch.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2013 David Heinemeier Hansson +# Copyright (c) 2004-2014 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -52,7 +52,6 @@ module ActionDispatch autoload :DebugExceptions autoload :ExceptionWrapper autoload :Flash - autoload :Head autoload :ParamsParser autoload :PublicExceptions autoload :Reloader @@ -74,18 +73,16 @@ module ActionDispatch autoload :MimeNegotiation autoload :Parameters autoload :ParameterFilter - autoload :FilterParameters - autoload :FilterRedirect autoload :Upload autoload :UploadedFile, 'action_dispatch/http/upload' autoload :URL end module Session - autoload :AbstractStore, 'action_dispatch/middleware/session/abstract_store' - autoload :CookieStore, 'action_dispatch/middleware/session/cookie_store' - autoload :MemCacheStore, 'action_dispatch/middleware/session/mem_cache_store' - autoload :CacheStore, 'action_dispatch/middleware/session/cache_store' + autoload :AbstractStore, 'action_dispatch/middleware/session/abstract_store' + autoload :CookieStore, 'action_dispatch/middleware/session/cookie_store' + autoload :MemCacheStore, 'action_dispatch/middleware/session/mem_cache_store' + autoload :CacheStore, 'action_dispatch/middleware/session/cache_store' end mattr_accessor :test_app diff --git a/actionpack/lib/action_dispatch/http/filter_parameters.rb b/actionpack/lib/action_dispatch/http/filter_parameters.rb index 289e204ac8..2b851cc28d 100644 --- a/actionpack/lib/action_dispatch/http/filter_parameters.rb +++ b/actionpack/lib/action_dispatch/http/filter_parameters.rb @@ -6,8 +6,8 @@ module ActionDispatch module Http # Allows you to specify sensitive parameters which will be replaced from # the request log by looking in the query string of the request and all - # subhashes of the params hash to filter. If a block is given, each key and - # value of the params hash and all subhashes is passed to it, the value + # sub-hashes of the params hash to filter. If a block is given, each key and + # value of the params hash and all sub-hashes is passed to it, the value # or key can be replaced using String#replace or similar method. # # env["action_dispatch.parameter_filter"] = [:password] diff --git a/actionpack/lib/action_dispatch/http/filter_redirect.rb b/actionpack/lib/action_dispatch/http/filter_redirect.rb index 900ce1c646..cd603649c3 100644 --- a/actionpack/lib/action_dispatch/http/filter_redirect.rb +++ b/actionpack/lib/action_dispatch/http/filter_redirect.rb @@ -5,7 +5,8 @@ module ActionDispatch FILTERED = '[FILTERED]'.freeze # :nodoc: def filtered_location - if !location_filter.empty? && location_filter_match? + filters = location_filter + if !filters.empty? && location_filter_match?(filters) FILTERED else location @@ -15,15 +16,15 @@ module ActionDispatch private def location_filter - if request.present? + if request request.env['action_dispatch.redirect_filter'] || [] else [] end end - def location_filter_match? - location_filter.any? do |filter| + def location_filter_match?(filters) + filters.any? do |filter| if String === filter location.include?(filter) elsif Regexp === filter diff --git a/actionpack/lib/action_dispatch/http/headers.rb b/actionpack/lib/action_dispatch/http/headers.rb index 2666cd4b0a..3e607bbde1 100644 --- a/actionpack/lib/action_dispatch/http/headers.rb +++ b/actionpack/lib/action_dispatch/http/headers.rb @@ -1,5 +1,10 @@ module ActionDispatch module Http + # Provides access to the request's HTTP headers from the environment. + # + # env = { "CONTENT_TYPE" => "text/plain" } + # headers = ActionDispatch::Http::Headers.new(env) + # headers["Content-Type"] # => "text/plain" class Headers CGI_VARIABLES = %w( CONTENT_TYPE CONTENT_LENGTH @@ -14,21 +19,32 @@ module ActionDispatch include Enumerable attr_reader :env - def initialize(env = {}) + def initialize(env = {}) # :nodoc: @env = env end + # Returns the value for the given key mapped to @env. def [](key) @env[env_name(key)] end + # Sets the given value for the key mapped to @env. def []=(key, value) @env[env_name(key)] = value end - def key?(key); @env.key? key; end + def key?(key) + @env.key? env_name(key) + end alias :include? :key? + # Returns the value for the given key mapped to @env. + # + # If the key is not found and an optional code block is not provided, + # raises a <tt>KeyError</tt> exception. + # + # If the code block is provided, then it will be run and + # its result returned. def fetch(key, *args, &block) @env.fetch env_name(key), *args, &block end @@ -37,12 +53,17 @@ module ActionDispatch @env.each(&block) end + # Returns a new Http::Headers instance containing the contents of + # <tt>headers_or_env</tt> and the original instance. def merge(headers_or_env) headers = Http::Headers.new(env.dup) headers.merge!(headers_or_env) headers end + # Adds the contents of <tt>headers_or_env</tt> to original instance + # entries; duplicate keys are overwritten with the values from + # <tt>headers_or_env</tt>. def merge!(headers_or_env) headers_or_env.each do |key, value| self[env_name(key)] = value @@ -50,6 +71,8 @@ module ActionDispatch end private + # Converts a HTTP header name to an environment variable name if it is + # not contained within the headers hash. def env_name(key) key = key.to_s if key =~ HTTP_HEADER diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb index 40bb060d52..0b2b60d2e4 100644 --- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb +++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb @@ -10,6 +10,8 @@ module ActionDispatch self.ignore_accept_header = false end + attr_reader :variant + # The MIME type of the HTTP request, such as Mime::XML. # # For backward compatibility, the post \format is extracted from the @@ -48,7 +50,7 @@ module ActionDispatch # GET /posts/5 | request.format => Mime::HTML or MIME::JS, or request.accepts.first # def format(view_path = []) - formats.first + formats.first || Mime::NullType.instance end def formats @@ -64,6 +66,20 @@ module ActionDispatch end end + # Sets the \variant for template. + def variant=(variant) + if variant.is_a?(Symbol) + @variant = [variant] + elsif variant.is_a?(Array) && variant.any? && variant.all?{ |v| v.is_a?(Symbol) } + @variant = variant + else + raise ArgumentError, "request.variant must be set to a Symbol or an Array of Symbols, not a #{variant.class}. " \ + "For security reasons, never directly set the variant to a user-provided value, " \ + "like params[:variant].to_sym. Check user-provided value against a whitelist first, " \ + "then set the variant: request.variant = :tablet if params[:variant] == 'tablet'" + end + end + # Sets the \format by string extension, which can be used to force custom formats # that are not controlled by the extension. # @@ -113,7 +129,7 @@ module ActionDispatch end end - order.include?(Mime::ALL) ? formats.first : nil + order.include?(Mime::ALL) ? format : nil end protected diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb index ef144c3c76..9450be838c 100644 --- a/actionpack/lib/action_dispatch/http/mime_type.rb +++ b/actionpack/lib/action_dispatch/http/mime_type.rb @@ -1,5 +1,6 @@ require 'set' -require 'active_support/core_ext/class/attribute_accessors' +require 'singleton' +require 'active_support/core_ext/module/attribute_accessors' require 'active_support/core_ext/string/starts_ends_with' module Mime @@ -27,7 +28,7 @@ module Mime class << self def [](type) return type if type.is_a?(Type) - Type.lookup_by_extension(type) || NullType.new + Type.lookup_by_extension(type) end def fetch(type) @@ -173,7 +174,7 @@ module Mime end def parse(accept_header) - if accept_header !~ /,/ + if !accept_header.include?(',') accept_header = accept_header.split(PARAMETER_SEPARATOR_REGEXP).first parse_trailing_star(accept_header) || [Mime::Type.lookup(accept_header)].compact else @@ -292,13 +293,13 @@ module Mime end class NullType + include Singleton + def nil? true end - def ref - nil - end + def ref; end def respond_to_missing?(method, include_private = false) method.to_s.ends_with? '?' diff --git a/actionpack/lib/action_dispatch/http/mime_types.rb b/actionpack/lib/action_dispatch/http/mime_types.rb index a6b3aee5e7..0e4da36038 100644 --- a/actionpack/lib/action_dispatch/http/mime_types.rb +++ b/actionpack/lib/action_dispatch/http/mime_types.rb @@ -7,6 +7,7 @@ Mime::Type.register "text/javascript", :js, %w( application/javascript applicati Mime::Type.register "text/css", :css Mime::Type.register "text/calendar", :ics Mime::Type.register "text/csv", :csv +Mime::Type.register "text/vcard", :vcf Mime::Type.register "image/png", :png, [], %w(png) Mime::Type.register "image/jpeg", :jpeg, [], %w(jpg jpeg jpe pjpeg) diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb index 8e992070f1..dcb299ed03 100644 --- a/actionpack/lib/action_dispatch/http/parameters.rb +++ b/actionpack/lib/action_dispatch/http/parameters.rb @@ -57,22 +57,25 @@ module ActionDispatch # you'll get a weird error down the road, but our form handling # should really prevent that from happening def normalize_encode_params(params) - if params.is_a?(String) - return params.force_encoding(Encoding::UTF_8).encode! - elsif !params.is_a?(Hash) - return params - end - - new_hash = {} - params.each do |key, val| - new_key = key.is_a?(String) ? key.dup.force_encoding(Encoding::UTF_8).encode! : key - new_hash[new_key] = if val.is_a?(Array) - val.map! { |el| normalize_encode_params(el) } + case params + when String + params.force_encoding(Encoding::UTF_8).encode! + when Hash + if params.has_key?(:tempfile) + UploadedFile.new(params) else - normalize_encode_params(val) + params.each_with_object({}) do |(key, val), new_hash| + new_key = key.is_a?(String) ? key.dup.force_encoding(Encoding::UTF_8).encode! : key + new_hash[new_key] = if val.is_a?(Array) + val.map! { |el| normalize_encode_params(el) } + else + normalize_encode_params(val) + end + end.with_indifferent_access end + else + params end - new_hash.with_indifferent_access end end end diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index 4ca1d35489..cdb3e44b3a 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -18,7 +18,6 @@ module ActionDispatch include ActionDispatch::Http::MimeNegotiation include ActionDispatch::Http::Parameters include ActionDispatch::Http::FilterParameters - include ActionDispatch::Http::Upload include ActionDispatch::Http::URL autoload :Session, 'action_dispatch/request/session' @@ -65,6 +64,7 @@ module ActionDispatch # Ordered Collections Protocol (WebDAV) (http://www.ietf.org/rfc/rfc3648.txt) # Web Distributed Authoring and Versioning (WebDAV) Access Control Protocol (http://www.ietf.org/rfc/rfc3744.txt) # Web Distributed Authoring and Versioning (WebDAV) SEARCH (http://www.ietf.org/rfc/rfc5323.txt) + # Calendar Extensions to WebDAV (http://www.ietf.org/rfc/rfc4791.txt) # PATCH Method for HTTP (http://www.ietf.org/rfc/rfc5789.txt) RFC2616 = %w(OPTIONS GET HEAD POST PUT DELETE TRACE CONNECT) RFC2518 = %w(PROPFIND PROPPATCH MKCOL COPY MOVE LOCK UNLOCK) @@ -72,9 +72,10 @@ module ActionDispatch RFC3648 = %w(ORDERPATCH) RFC3744 = %w(ACL) RFC5323 = %w(SEARCH) + RFC4791 = %w(MKCALENDAR) RFC5789 = %w(PATCH) - HTTP_METHODS = RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC5789 + HTTP_METHODS = RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC4791 + RFC5789 HTTP_METHOD_LOOKUP = {} @@ -153,6 +154,13 @@ module ActionDispatch Http::Headers.new(@env) end + # Returns a +String+ with the last requested path including their params. + # + # # get '/foo' + # request.original_fullpath # => '/foo' + # + # # get '/foo?bar' + # request.original_fullpath # => '/foo?bar' def original_fullpath @original_fullpath ||= (env["ORIGINAL_FULLPATH"] || fullpath) end @@ -226,7 +234,7 @@ module ActionDispatch def raw_post unless @env.include? 'RAW_POST_DATA' raw_post_body = body - @env['RAW_POST_DATA'] = raw_post_body.read(@env['CONTENT_LENGTH'].to_i) + @env['RAW_POST_DATA'] = raw_post_body.read(content_length) raw_post_body.rewind if raw_post_body.respond_to?(:rewind) end @env['RAW_POST_DATA'] @@ -272,7 +280,7 @@ module ActionDispatch # Override Rack's GET method to support indifferent access def GET - @env["action_dispatch.request.query_parameters"] ||= (normalize_encode_params(super) || {}) + @env["action_dispatch.request.query_parameters"] ||= Utils.deep_munge((normalize_encode_params(super) || {})) rescue TypeError => e raise ActionController::BadRequest.new(:query, e) end @@ -280,7 +288,7 @@ module ActionDispatch # Override Rack's POST method to support indifferent access def POST - @env["action_dispatch.request.request_parameters"] ||= (normalize_encode_params(super) || {}) + @env["action_dispatch.request.request_parameters"] ||= Utils.deep_munge((normalize_encode_params(super) || {})) rescue TypeError => e raise ActionController::BadRequest.new(:request, e) end @@ -300,17 +308,24 @@ module ActionDispatch LOCALHOST =~ remote_addr && LOCALHOST =~ remote_ip end - protected + # Extracted into ActionDispatch::Request::Utils.deep_munge, but kept here for backwards compatibility. + def deep_munge(hash) + ActiveSupport::Deprecation.warn( + "This method has been extracted into ActionDispatch::Request::Utils.deep_munge. Please start using that instead." + ) - def parse_query(qs) - Utils.deep_munge(super) + Utils.deep_munge(hash) end - private + protected + def parse_query(qs) + Utils.deep_munge(super) + end - def check_method(name) - HTTP_METHOD_LOOKUP[name] || raise(ActionController::UnknownHttpMethod, "#{name}, accepted HTTP methods are #{HTTP_METHODS.to_sentence(:locale => :en)}") - name - end + private + def check_method(name) + HTTP_METHOD_LOOKUP[name] || raise(ActionController::UnknownHttpMethod, "#{name}, accepted HTTP methods are #{HTTP_METHODS.to_sentence(:locale => :en)}") + name + end end end diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index d3696cbb8a..eaea93b730 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -1,4 +1,5 @@ -require 'active_support/core_ext/class/attribute_accessors' +require 'active_support/core_ext/module/attribute_accessors' +require 'action_dispatch/http/filter_redirect' require 'monitor' module ActionDispatch # :nodoc: @@ -41,7 +42,7 @@ module ActionDispatch # :nodoc: # Get and set headers for this response. attr_accessor :header - + alias_method :headers=, :header= alias_method :headers, :header @@ -90,7 +91,10 @@ module ActionDispatch # :nodoc: end def each(&block) - @buf.each(&block) + @response.sending! + x = @buf.each(&block) + @response.sent! + x end def close @@ -117,6 +121,8 @@ module ActionDispatch # :nodoc: @blank = false @cv = new_cond @committed = false + @sending = false + @sent = false @content_type = nil @charset = nil @@ -137,17 +143,37 @@ module ActionDispatch # :nodoc: end end + def await_sent + synchronize { @cv.wait_until { @sent } } + end + def commit! synchronize do + before_committed @committed = true @cv.broadcast end end - def committed? - @committed + def sending! + synchronize do + before_sending + @sending = true + @cv.broadcast + end + end + + def sent! + synchronize do + @sent = true + @cv.broadcast + end end + def sending?; synchronize { @sending }; end + def committed?; synchronize { @committed }; end + def sent?; synchronize { @sent }; end + # Sets the HTTP status code. def status=(status) @status = Rack::Utils.status_code(status) @@ -181,9 +207,9 @@ module ActionDispatch # :nodoc: end alias_method :status_message, :message - def respond_to?(method) + def respond_to?(method, include_private = false) if method.to_s == 'to_path' - stream.respond_to?(:to_path) + stream.respond_to?(method) else super end @@ -270,8 +296,17 @@ module ActionDispatch # :nodoc: cookies end + def _status_code + @status + end private + def before_committed + end + + def before_sending + end + def merge_default_headers(original, default) return original unless default.respond_to?(:merge) @@ -312,7 +347,7 @@ module ActionDispatch # :nodoc: header.delete CONTENT_TYPE [status, header, []] else - [status, header, self] + [status, header, Rack::BodyProxy.new(self){}] end end end diff --git a/actionpack/lib/action_dispatch/http/upload.rb b/actionpack/lib/action_dispatch/http/upload.rb index b57c84dec8..45bf751d09 100644 --- a/actionpack/lib/action_dispatch/http/upload.rb +++ b/actionpack/lib/action_dispatch/http/upload.rb @@ -18,6 +18,7 @@ module ActionDispatch # A +Tempfile+ object with the actual uploaded file. Note that some of # its interface is available directly. attr_accessor :tempfile + alias :to_io :tempfile # A string with the headers of the multipart request. attr_accessor :headers @@ -73,18 +74,5 @@ module ActionDispatch filename.force_encoding(Encoding::UTF_8).encode! if filename end end - - module Upload # :nodoc: - # Replace file upload hash with UploadedFile objects - # when normalize and encode parameters. - def normalize_encode_params(value) - if Hash === value && value.has_key?(:tempfile) - UploadedFile.new(value) - else - super - end - end - private :normalize_encode_params - end end end diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb index 6f5a52c568..c9860af909 100644 --- a/actionpack/lib/action_dispatch/http/url.rb +++ b/actionpack/lib/action_dispatch/http/url.rb @@ -29,15 +29,12 @@ module ActionDispatch extract_subdomains(host, tld_length).join('.') end - def url_for(options = {}) - options = options.dup - path = options.delete(:script_name).to_s.chomp("/") - path << options.delete(:path).to_s - - params = options[:params].is_a?(Hash) ? options[:params] : options.slice(:params) - params.reject! { |_,v| v.to_param.nil? } + def url_for(options) + path = options[:script_name].to_s.chomp("/") + path << options[:path].to_s result = build_host_url(options) + if options[:trailing_slash] if path.include?('?') result << path.sub(/\?/, '/\&') @@ -47,7 +44,16 @@ module ActionDispatch else result << path end - result << "?#{params.to_query}" unless params.empty? + + if options.key? :params + params = options[:params].is_a?(Hash) ? + options[:params] : + { params: options[:params] } + + params.reject! { |_,v| v.to_param.nil? } + result << "?#{params.to_query}" unless params.empty? + end + result << "##{Journey::Router::Utils.escape_fragment(options[:anchor].to_param.to_s)}" if options[:anchor] result end @@ -55,7 +61,7 @@ module ActionDispatch private def build_host_url(options) - if options[:host].blank? && options[:only_path].blank? + unless options[:host] || options[:only_path] raise ArgumentError, 'Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true' end @@ -130,7 +136,7 @@ module ActionDispatch case options[:protocol] when "//" - nil + options[:port] when "https://" options[:port].to_i == 443 ? nil : options[:port] else diff --git a/actionpack/lib/action_dispatch/journey/formatter.rb b/actionpack/lib/action_dispatch/journey/formatter.rb index e288f026c7..57f0963731 100644 --- a/actionpack/lib/action_dispatch/journey/formatter.rb +++ b/actionpack/lib/action_dispatch/journey/formatter.rb @@ -18,7 +18,11 @@ module ActionDispatch match_route(name, constraints) do |route| parameterized_parts = extract_parameterized_parts(route, options, recall, parameterize) - next if !name && route.requirements.empty? && route.parts.empty? + + # Skip this route unless a name has been provided or it is a + # standard Rails route since we can't determine whether an options + # hash passed to url_for matches a Rack application or a redirect. + next unless name || route.dispatcher? missing_keys = missing_keys(route, parameterized_parts) next unless missing_keys.empty? @@ -29,8 +33,8 @@ module ActionDispatch return [route.format(parameterized_parts), params] end - message = "No route matches #{constraints.inspect}" - message << " missing required keys: #{missing_keys.inspect}" if name + message = "No route matches #{Hash[constraints.sort].inspect}" + message << " missing required keys: #{missing_keys.sort.inspect}" if name raise ActionController::UrlGenerationError, message end @@ -117,9 +121,9 @@ module ActionDispatch def possibles(cache, options, depth = 0) cache.fetch(:___routes) { [] } + options.find_all { |pair| cache.key?(pair) - }.map { |pair| + }.flat_map { |pair| possibles(cache[pair], options, depth + 1) - }.flatten(1) + } end # Returns +true+ if no missing keys are present, otherwise +false+. diff --git a/actionpack/lib/action_dispatch/journey/gtg/builder.rb b/actionpack/lib/action_dispatch/journey/gtg/builder.rb index 7d2791714b..450588cda6 100644 --- a/actionpack/lib/action_dispatch/journey/gtg/builder.rb +++ b/actionpack/lib/action_dispatch/journey/gtg/builder.rb @@ -27,7 +27,7 @@ module ActionDispatch marked[s] = true # mark s s.group_by { |state| symbol(state) }.each do |sym, ps| - u = ps.map { |l| followpos(l) }.flatten + u = ps.flat_map { |l| followpos(l) } next if u.empty? if u.uniq == [DUMMY] @@ -90,7 +90,7 @@ module ActionDispatch firstpos(node.left) end when Nodes::Or - node.children.map { |c| firstpos(c) }.flatten.uniq + node.children.flat_map { |c| firstpos(c) }.uniq when Nodes::Unary firstpos(node.left) when Nodes::Terminal @@ -105,7 +105,7 @@ module ActionDispatch when Nodes::Star firstpos(node.left) when Nodes::Or - node.children.map { |c| lastpos(c) }.flatten.uniq + node.children.flat_map { |c| lastpos(c) }.uniq when Nodes::Cat if nullable?(node.right) lastpos(node.left) | lastpos(node.right) diff --git a/actionpack/lib/action_dispatch/journey/gtg/simulator.rb b/actionpack/lib/action_dispatch/journey/gtg/simulator.rb index 58ad803841..94b0a24344 100644 --- a/actionpack/lib/action_dispatch/journey/gtg/simulator.rb +++ b/actionpack/lib/action_dispatch/journey/gtg/simulator.rb @@ -19,6 +19,14 @@ module ActionDispatch end def simulate(string) + ms = memos(string) { return } + MatchData.new(ms) + end + + alias :=~ :simulate + alias :match :simulate + + def memos(string) input = StringScanner.new(string) state = [0] while sym = input.scan(%r([/.?]|[^/.?]+)) @@ -29,15 +37,10 @@ module ActionDispatch tt.accepting? s } - return if acceptance_states.empty? + return yield if acceptance_states.empty? - memos = acceptance_states.map { |x| tt.memo(x) }.flatten.compact - - MatchData.new(memos) + acceptance_states.flat_map { |x| tt.memo(x) }.compact end - - alias :=~ :simulate - alias :match :simulate end end end diff --git a/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb b/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb index da0cddd93c..990d2127ee 100644 --- a/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb +++ b/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb @@ -9,8 +9,8 @@ module ActionDispatch attr_reader :memos def initialize - @regexp_states = Hash.new { |h,k| h[k] = {} } - @string_states = Hash.new { |h,k| h[k] = {} } + @regexp_states = {} + @string_states = {} @accepting = {} @memos = Hash.new { |h,k| h[k] = [] } end @@ -40,12 +40,22 @@ module ActionDispatch end def move(t, a) - move_string(t, a).concat(move_regexp(t, a)) - end + return [] if t.empty? + + regexps = [] - def to_json - require 'json' + t.map { |s| + if states = @regexp_states[s] + regexps.concat states.map { |re, v| re === a ? v : nil } + end + if states = @string_states[s] + states[a] + end + }.compact.concat regexps + end + + def as_json(options = nil) simple_regexp = Hash.new { |h,k| h[k] = {} } @regexp_states.each do |from, hash| @@ -54,11 +64,11 @@ module ActionDispatch end end - JSON.dump({ + { regexp_states: simple_regexp, string_states: @string_states, accepting: @accepting - }) + } end def to_svg @@ -111,44 +121,35 @@ module ActionDispatch end def []=(from, to, sym) - case sym - when String - @string_states[from][sym] = to - when Regexp - @regexp_states[from][sym] = to - else - raise ArgumentError, 'unknown symbol: %s' % sym.class - end + to_mappings = states_hash_for(sym)[from] ||= {} + to_mappings[sym] = to end def states - ss = @string_states.keys + @string_states.values.map(&:values).flatten - rs = @regexp_states.keys + @regexp_states.values.map(&:values).flatten + ss = @string_states.keys + @string_states.values.flat_map(&:values) + rs = @regexp_states.keys + @regexp_states.values.flat_map(&:values) (ss + rs).uniq end def transitions - @string_states.map { |from, hash| + @string_states.flat_map { |from, hash| hash.map { |s, to| [from, s, to] } - }.flatten(1) + @regexp_states.map { |from, hash| + } + @regexp_states.flat_map { |from, hash| hash.map { |s, to| [from, s, to] } - }.flatten(1) + } end private - def move_regexp(t, a) - return [] if t.empty? - - t.map { |s| - @regexp_states[s].map { |re, v| re === a ? v : nil } - }.flatten.compact.uniq - end - - def move_string(t, a) - return [] if t.empty? - - t.map { |s| @string_states[s][a] }.compact + def states_hash_for(sym) + case sym + when String + @string_states + when Regexp + @regexp_states + else + raise ArgumentError, 'unknown symbol: %s' % sym.class + end end end end diff --git a/actionpack/lib/action_dispatch/journey/nfa/dot.rb b/actionpack/lib/action_dispatch/journey/nfa/dot.rb index 5c33a872e5..47bf76bdbf 100644 --- a/actionpack/lib/action_dispatch/journey/nfa/dot.rb +++ b/actionpack/lib/action_dispatch/journey/nfa/dot.rb @@ -16,9 +16,9 @@ module ActionDispatch # end # " #{n.object_id} [label=\"#{label}\", shape=box];" #} - #memo_edges = memos.map { |k, memos| + #memo_edges = memos.flat_map { |k, memos| # (memos || []).map { |v| " #{k} -> #{v.object_id};" } - #}.flatten.uniq + #}.uniq <<-eodot digraph nfa { diff --git a/actionpack/lib/action_dispatch/journey/nfa/simulator.rb b/actionpack/lib/action_dispatch/journey/nfa/simulator.rb index 5b40da6569..b23270db3c 100644 --- a/actionpack/lib/action_dispatch/journey/nfa/simulator.rb +++ b/actionpack/lib/action_dispatch/journey/nfa/simulator.rb @@ -34,7 +34,7 @@ module ActionDispatch return if acceptance_states.empty? - memos = acceptance_states.map { |x| tt.memo(x) }.flatten.compact + memos = acceptance_states.flat_map { |x| tt.memo(x) }.compact MatchData.new(memos) end diff --git a/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb b/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb index a3017aeea1..66e414213a 100644 --- a/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb +++ b/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb @@ -42,7 +42,7 @@ module ActionDispatch end def states - (@table.keys + @table.values.map(&:keys).flatten).uniq + (@table.keys + @table.values.flat_map(&:keys)).uniq end # Returns a generalized transition graph with reduced states. The states @@ -93,7 +93,7 @@ module ActionDispatch # Returns set of NFA states to which there is a transition on ast symbol # +a+ from some state +s+ in +t+. def following_states(t, a) - Array(t).map { |s| inverted[s][a] }.flatten.uniq + Array(t).flat_map { |s| inverted[s][a] }.uniq end # Returns set of NFA states to which there is a transition on ast symbol @@ -107,7 +107,7 @@ module ActionDispatch end def alphabet - inverted.values.map(&:keys).flatten.compact.uniq.sort_by { |x| x.to_s } + inverted.values.flat_map(&:keys).compact.uniq.sort_by { |x| x.to_s } end # Returns a set of NFA states reachable from some NFA state +s+ in set @@ -131,9 +131,9 @@ module ActionDispatch end def transitions - @table.map { |to, hash| + @table.flat_map { |to, hash| hash.map { |from, sym| [from, sym, to] } - }.flatten(1) + } end private diff --git a/actionpack/lib/action_dispatch/journey/parser.rb b/actionpack/lib/action_dispatch/journey/parser.rb index bb4cbb00e2..d129ba7e16 100644 --- a/actionpack/lib/action_dispatch/journey/parser.rb +++ b/actionpack/lib/action_dispatch/journey/parser.rb @@ -1,6 +1,6 @@ # # DO NOT MODIFY!!!! -# This file is automatically generated by Racc 1.4.9 +# This file is automatically generated by Racc 1.4.11 # from Racc grammer file "". # @@ -9,42 +9,38 @@ require 'racc/parser.rb' require 'action_dispatch/journey/parser_extras' module ActionDispatch - module Journey # :nodoc: - class Parser < Racc::Parser # :nodoc: + module Journey + class Parser < Racc::Parser ##### State transition tables begin ### racc_action_table = [ - 17, 21, 13, 15, 14, 7, nil, 16, 8, 19, - 13, 15, 14, 7, 23, 16, 8, 19, 13, 15, - 14, 7, nil, 16, 8, 13, 15, 14, 7, nil, - 16, 8, 13, 15, 14, 7, nil, 16, 8 ] + 13, 15, 14, 7, 21, 16, 8, 19, 13, 15, + 14, 7, 17, 16, 8, 13, 15, 14, 7, 24, + 16, 8, 13, 15, 14, 7, 19, 16, 8 ] racc_action_check = [ - 1, 17, 1, 1, 1, 1, nil, 1, 1, 1, - 20, 20, 20, 20, 20, 20, 20, 20, 7, 7, - 7, 7, nil, 7, 7, 19, 19, 19, 19, nil, - 19, 19, 0, 0, 0, 0, nil, 0, 0 ] + 2, 2, 2, 2, 17, 2, 2, 2, 0, 0, + 0, 0, 1, 0, 0, 19, 19, 19, 19, 20, + 19, 19, 7, 7, 7, 7, 22, 7, 7 ] racc_action_pointer = [ - 30, 0, nil, nil, nil, nil, nil, 16, nil, nil, - nil, nil, nil, nil, nil, nil, nil, 1, nil, 23, - 8, nil, nil, nil ] + 6, 12, -2, nil, nil, nil, nil, 20, nil, nil, + nil, nil, nil, nil, nil, nil, nil, 4, nil, 13, + 13, nil, 17, nil, nil ] racc_action_default = [ - -18, -18, -2, -3, -4, -5, -6, -18, -9, -10, - -11, -12, -13, -14, -15, -16, -17, -18, -1, -18, - -18, 24, -8, -7 ] + -19, -19, -2, -3, -4, -5, -6, -19, -10, -11, + -12, -13, -14, -15, -16, -17, -18, -19, -1, -19, + -19, 25, -8, -9, -7 ] racc_goto_table = [ - 18, 1, nil, nil, nil, nil, nil, nil, 20, nil, - nil, nil, nil, nil, nil, nil, nil, nil, 22, 18 ] + 1, 22, 18, 23, nil, nil, nil, 20 ] racc_goto_check = [ - 2, 1, nil, nil, nil, nil, nil, nil, 1, nil, - nil, nil, nil, nil, nil, nil, nil, nil, 2, 2 ] + 1, 2, 1, 3, nil, nil, nil, 1 ] racc_goto_pointer = [ - nil, 1, -1, nil, nil, nil, nil, nil, nil, nil, + nil, 0, -18, -16, nil, nil, nil, nil, nil, nil, nil ] racc_goto_default = [ @@ -61,19 +57,20 @@ racc_reduce_table = [ 1, 12, :_reduce_none, 3, 15, :_reduce_7, 3, 13, :_reduce_8, - 1, 16, :_reduce_9, + 3, 13, :_reduce_9, + 1, 16, :_reduce_10, 1, 14, :_reduce_none, 1, 14, :_reduce_none, 1, 14, :_reduce_none, 1, 14, :_reduce_none, - 1, 19, :_reduce_14, - 1, 17, :_reduce_15, - 1, 18, :_reduce_16, - 1, 20, :_reduce_17 ] + 1, 19, :_reduce_15, + 1, 17, :_reduce_16, + 1, 18, :_reduce_17, + 1, 20, :_reduce_18 ] -racc_reduce_n = 18 +racc_reduce_n = 19 -racc_shift_n = 24 +racc_shift_n = 25 racc_token_table = { false => 0, @@ -137,12 +134,12 @@ Racc_debug_parser = false # reduce 0 omitted def _reduce_1(val, _values, result) - result = Cat.new(val.first, val.last) + result = Cat.new(val.first, val.last) result end def _reduce_2(val, _values, result) - result = val.first + result = val.first result end @@ -155,21 +152,24 @@ end # reduce 6 omitted def _reduce_7(val, _values, result) - result = Group.new(val[1]) + result = Group.new(val[1]) result end def _reduce_8(val, _values, result) - result = Or.new([val.first, val.last]) + result = Or.new([val.first, val.last]) result end def _reduce_9(val, _values, result) - result = Star.new(Symbol.new(val.last)) + result = Or.new([val.first, val.last]) result end -# reduce 10 omitted +def _reduce_10(val, _values, result) + result = Star.new(Symbol.new(val.last)) + result +end # reduce 11 omitted @@ -177,23 +177,25 @@ end # reduce 13 omitted -def _reduce_14(val, _values, result) - result = Slash.new('/') - result -end +# reduce 14 omitted def _reduce_15(val, _values, result) - result = Symbol.new(val.first) + result = Slash.new('/') result end def _reduce_16(val, _values, result) - result = Literal.new(val.first) + result = Symbol.new(val.first) result end def _reduce_17(val, _values, result) - result = Dot.new(val.first) + result = Literal.new(val.first) + result +end + +def _reduce_18(val, _values, result) + result = Dot.new(val.first) result end diff --git a/actionpack/lib/action_dispatch/journey/parser.y b/actionpack/lib/action_dispatch/journey/parser.y index 040f8d5922..0ead222551 100644 --- a/actionpack/lib/action_dispatch/journey/parser.y +++ b/actionpack/lib/action_dispatch/journey/parser.y @@ -4,7 +4,7 @@ token SLASH LITERAL SYMBOL LPAREN RPAREN DOT STAR OR rule expressions - : expressions expression { result = Cat.new(val.first, val.last) } + : expression expressions { result = Cat.new(val.first, val.last) } | expression { result = val.first } | or ; @@ -17,7 +17,8 @@ rule : LPAREN expressions RPAREN { result = Group.new(val[1]) } ; or - : expressions OR expression { result = Or.new([val.first, val.last]) } + : expression OR expression { result = Or.new([val.first, val.last]) } + | expression OR or { result = Or.new([val.first, val.last]) } ; star : STAR { result = Star.new(Symbol.new(val.last)) } diff --git a/actionpack/lib/action_dispatch/journey/path/pattern.rb b/actionpack/lib/action_dispatch/journey/path/pattern.rb index d37aa1fbe5..28c75618de 100644 --- a/actionpack/lib/action_dispatch/journey/path/pattern.rb +++ b/actionpack/lib/action_dispatch/journey/path/pattern.rb @@ -28,6 +28,11 @@ module ActionDispatch @required_names = nil @re = nil @offsets = nil + @format = Visitors::FormatBuilder.new.accept(spec) + end + + def format_path(path_options) + @format.evaluate path_options end def ast @@ -53,9 +58,9 @@ module ActionDispatch end def optional_names - @optional_names ||= spec.grep(Nodes::Group).map { |group| + @optional_names ||= spec.grep(Nodes::Group).flat_map { |group| group.grep(Nodes::Symbol) - }.flatten.map { |n| n.name }.uniq + }.map { |n| n.name }.uniq end class RegexpOffsets < Journey::Visitors::Visitor # :nodoc: diff --git a/actionpack/lib/action_dispatch/journey/route.rb b/actionpack/lib/action_dispatch/journey/route.rb index 50e1853094..d4df96314f 100644 --- a/actionpack/lib/action_dispatch/journey/route.rb +++ b/actionpack/lib/action_dispatch/journey/route.rb @@ -16,6 +16,14 @@ module ActionDispatch @app = app @path = path + # Unwrap any constraints so we can see what's inside for route generation. + # This allows the formatter to skip over any mounted applications or redirects + # that shouldn't be matched when using a url_for without a route name. + while app.is_a?(Routing::Mapper::Constraints) do + app = app.app + end + @dispatcher = app.is_a?(Routing::RouteSet::Dispatcher) + @constraints = constraints @defaults = defaults @required_defaults = nil @@ -68,7 +76,7 @@ module ActionDispatch value.to_s == defaults[key].to_s && !required_parts.include?(key) end - Visitors::Formatter.new(path_options).accept(path.spec) + path.format_path path_options end def optimized_path @@ -93,6 +101,14 @@ module ActionDispatch end end + def glob? + !path.spec.grep(Nodes::Star).empty? + end + + def dispatcher? + @dispatcher + end + def matches?(request) constraints.all? do |method, value| next true unless request.respond_to?(method) diff --git a/actionpack/lib/action_dispatch/journey/router.rb b/actionpack/lib/action_dispatch/journey/router.rb index da32f1bfe7..36561c71a1 100644 --- a/actionpack/lib/action_dispatch/journey/router.rb +++ b/actionpack/lib/action_dispatch/journey/router.rb @@ -54,7 +54,7 @@ module ActionDispatch end def call(env) - env['PATH_INFO'] = normalize_path(env['PATH_INFO']) + env['PATH_INFO'] = Utils.normalize_path(env['PATH_INFO']) find_routes(env).each do |match, parameters, route| script_name, path_info, set_params = env.values_at('SCRIPT_NAME', @@ -103,12 +103,6 @@ module ActionDispatch private - def normalize_path(path) - path = "/#{path}" - path.squeeze!('/') - path - end - def partitioned_routes routes.partitioned_routes end @@ -127,8 +121,7 @@ module ActionDispatch def filter_routes(path) return [] unless ast - data = simulator.match(path) - data ? data.memos : [] + simulator.memos(path) { [] } end def find_routes env diff --git a/actionpack/lib/action_dispatch/journey/router/utils.rb b/actionpack/lib/action_dispatch/journey/router/utils.rb index 462f1a122d..ac4ecb1e65 100644 --- a/actionpack/lib/action_dispatch/journey/router/utils.rb +++ b/actionpack/lib/action_dispatch/journey/router/utils.rb @@ -1,5 +1,3 @@ -require 'uri' - module ActionDispatch module Journey # :nodoc: class Router # :nodoc: @@ -7,46 +5,85 @@ module ActionDispatch # Normalizes URI path. # # Strips off trailing slash and ensures there is a leading slash. + # Also converts downcase url encoded string to uppercase. # # normalize_path("/foo") # => "/foo" # normalize_path("/foo/") # => "/foo" # normalize_path("foo") # => "/foo" # normalize_path("") # => "/" + # normalize_path("/%ab") # => "/%AB" def self.normalize_path(path) path = "/#{path}" path.squeeze!('/') path.sub!(%r{/+\Z}, '') + path.gsub!(/(%[a-f0-9]{2})/) { $1.upcase } path = '/' if path == '' path end # URI path and fragment escaping # http://tools.ietf.org/html/rfc3986 - module UriEscape # :nodoc: - # Symbol captures can generate multiple path segments, so include /. - reserved_segment = '/' - reserved_fragment = '/?' - reserved_pchar = ':@&=+$,;%' - - safe_pchar = "#{URI::REGEXP::PATTERN::UNRESERVED}#{reserved_pchar}" - safe_segment = "#{safe_pchar}#{reserved_segment}" - safe_fragment = "#{safe_pchar}#{reserved_fragment}" - UNSAFE_SEGMENT = Regexp.new("[^#{safe_segment}]", false).freeze - UNSAFE_FRAGMENT = Regexp.new("[^#{safe_fragment}]", false).freeze + class UriEncoder # :nodoc: + ENCODE = "%%%02X".freeze + ENCODING = Encoding::US_ASCII + EMPTY = "".force_encoding(ENCODING).freeze + DEC2HEX = (0..255).to_a.map{ |i| ENCODE % i }.map{ |s| s.force_encoding(ENCODING) } + + ALPHA = "a-zA-Z".freeze + DIGIT = "0-9".freeze + UNRESERVED = "#{ALPHA}#{DIGIT}\\-\\._~".freeze + SUB_DELIMS = "!\\$&'\\(\\)\\*\\+,;=".freeze + + ESCAPED = /%[a-zA-Z0-9]{2}/.freeze + + FRAGMENT = /[^#{UNRESERVED}#{SUB_DELIMS}:@\/\?]/.freeze + SEGMENT = /[^#{UNRESERVED}#{SUB_DELIMS}:@]/.freeze + PATH = /[^#{UNRESERVED}#{SUB_DELIMS}:@\/]/.freeze + + def escape_fragment(fragment) + escape(fragment, FRAGMENT) + end + + def escape_path(path) + escape(path, PATH) + end + + def escape_segment(segment) + escape(segment, SEGMENT) + end + + def unescape_uri(uri) + uri.gsub(ESCAPED) { [$&[1, 2].hex].pack('C') }.force_encoding(uri.encoding) + end + + protected + def escape(component, pattern) + component.gsub(pattern){ |unsafe| percent_encode(unsafe) }.force_encoding(ENCODING) + end + + def percent_encode(unsafe) + safe = EMPTY.dup + unsafe.each_byte { |b| safe << DEC2HEX[b] } + safe + end end - Parser = URI.const_defined?(:Parser) ? URI::Parser.new : URI + ENCODER = UriEncoder.new def self.escape_path(path) - Parser.escape(path.to_s, UriEscape::UNSAFE_SEGMENT) + ENCODER.escape_path(path.to_s) + end + + def self.escape_segment(segment) + ENCODER.escape_segment(segment.to_s) end def self.escape_fragment(fragment) - Parser.escape(fragment.to_s, UriEscape::UNSAFE_FRAGMENT) + ENCODER.escape_fragment(fragment.to_s) end def self.unescape_uri(uri) - Parser.unescape(uri) + ENCODER.unescape_uri(uri) end end end diff --git a/actionpack/lib/action_dispatch/journey/routes.rb b/actionpack/lib/action_dispatch/journey/routes.rb index a99d6d0d6a..80e3818ccd 100644 --- a/actionpack/lib/action_dispatch/journey/routes.rb +++ b/actionpack/lib/action_dispatch/journey/routes.rb @@ -30,6 +30,7 @@ module ActionDispatch def clear routes.clear + named_routes.clear end def partitioned_routes diff --git a/actionpack/lib/action_dispatch/journey/visitors.rb b/actionpack/lib/action_dispatch/journey/visitors.rb index 0a8cb1b4d4..b635576b6a 100644 --- a/actionpack/lib/action_dispatch/journey/visitors.rb +++ b/actionpack/lib/action_dispatch/journey/visitors.rb @@ -1,11 +1,56 @@ # encoding: utf-8 + module ActionDispatch module Journey # :nodoc: + class Format + ESCAPE_PATH = ->(value) { Router::Utils.escape_path(value) } + ESCAPE_SEGMENT = ->(value) { Router::Utils.escape_segment(value) } + + class Parameter < Struct.new(:name, :escaper) + def escape(value); escaper.call value; end + end + + def self.required_path(symbol) + Parameter.new symbol, ESCAPE_PATH + end + + def self.required_segment(symbol) + Parameter.new symbol, ESCAPE_SEGMENT + end + + def initialize(parts) + @parts = parts + @children = [] + @parameters = [] + + parts.each_with_index do |object,i| + case object + when Journey::Format + @children << i + when Parameter + @parameters << i + end + end + end + + def evaluate(hash) + parts = @parts.dup + + @parameters.each do |index| + param = parts[index] + value = hash.fetch(param.name) { return ''.freeze } + parts[index] = param.escape value + end + + @children.each { |index| parts[index] = parts[index].evaluate(hash) } + + parts.join + end + end + module Visitors # :nodoc: class Visitor # :nodoc: - DISPATCH_CACHE = Hash.new { |h,k| - h[k] = :"visit_#{k}" - } + DISPATCH_CACHE = {} def accept(node) visit(node) @@ -35,11 +80,41 @@ module ActionDispatch def visit_STAR(n); unary(n); end def terminal(node); end - %w{ LITERAL SYMBOL SLASH DOT }.each do |t| - class_eval %{ def visit_#{t}(n); terminal(n); end }, __FILE__, __LINE__ + def visit_LITERAL(n); terminal(n); end + def visit_SYMBOL(n); terminal(n); end + def visit_SLASH(n); terminal(n); end + def visit_DOT(n); terminal(n); end + + private_instance_methods(false).each do |pim| + next unless pim =~ /^visit_(.*)$/ + DISPATCH_CACHE[$1.to_sym] = pim end end + class FormatBuilder < Visitor # :nodoc: + def accept(node); Journey::Format.new(super); end + def terminal(node); [node.left]; end + + def binary(node) + visit(node.left) + visit(node.right) + end + + def visit_GROUP(n); [Journey::Format.new(unary(n))]; end + + def visit_STAR(n) + [Journey::Format.required_path(n.left.to_sym)] + end + + def visit_SYMBOL(n) + symbol = n.to_sym + if symbol == :controller + [Journey::Format.required_path(symbol)] + else + [Journey::Format.required_segment(symbol)] + end + end + end + # Loop through the requirements AST class Each < Visitor # :nodoc: attr_reader :block @@ -49,8 +124,8 @@ module ActionDispatch end def visit(node) - super block.call(node) + super end end @@ -74,55 +149,31 @@ module ActionDispatch end end - class OptimizedPath < String # :nodoc: - private - - def visit_GROUP(node) - "" - end - end - - # Used for formatting urls (url_for) - class Formatter < Visitor # :nodoc: - attr_reader :options, :consumed - - def initialize(options) - @options = options - @consumed = {} + class OptimizedPath < Visitor # :nodoc: + def accept(node) + Array(visit(node)) end private - def visit_GROUP(node) - if consumed == options - nil - else - route = visit(node.left) - route.include?("\0") ? nil : route - end + def visit_CAT(node) + [visit(node.left), visit(node.right)].flatten end - def terminal(node) - node.left + def visit_SYMBOL(node) + node.left[1..-1].to_sym end - def binary(node) - [visit(node.left), visit(node.right)].join + def visit_STAR(node) + visit(node.left) end - def nary(node) - node.children.map { |c| visit(c) }.join + def visit_GROUP(node) + [] end - def visit_SYMBOL(node) - key = node.to_sym - - if value = options[key] - consumed[key] = value - Router::Utils.escape_path(value) - else - "\0" - end + %w{ LITERAL SLASH DOT }.each do |t| + class_eval %{ def visit_#{t}(n); n.left; end }, __FILE__, __LINE__ end end diff --git a/actionpack/lib/action_dispatch/middleware/callbacks.rb b/actionpack/lib/action_dispatch/middleware/callbacks.rb index 852f1cf6f5..baf9d5779e 100644 --- a/actionpack/lib/action_dispatch/middleware/callbacks.rb +++ b/actionpack/lib/action_dispatch/middleware/callbacks.rb @@ -8,14 +8,14 @@ module ActionDispatch class << self delegate :to_prepare, :to_cleanup, :to => "ActionDispatch::Reloader" - end - def self.before(*args, &block) - set_callback(:call, :before, *args, &block) - end + def before(*args, &block) + set_callback(:call, :before, *args, &block) + end - def self.after(*args, &block) - set_callback(:call, :after, *args, &block) + def after(*args, &block) + set_callback(:call, :after, *args, &block) + end end def initialize(app) diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index 3ccd0c9ee8..22b16b628d 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -23,15 +23,15 @@ module ActionDispatch # # This cookie will be deleted when the user's browser is closed. # cookies[:user_name] = "david" # - # # Assign an array of values to a cookie. - # cookies[:lat_lon] = [47.68, -122.37] + # # Cookie values are String based. Other data types need to be serialized. + # cookies[:lat_lon] = JSON.generate([47.68, -122.37]) # # # Sets a cookie that expires in 1 hour. # cookies[:login] = { value: "XJ-122", expires: 1.hour.from_now } # # # Sets a signed cookie, which prevents users from tampering with its value. - # # The cookie is signed by your app's <tt>config.secret_key_base</tt> value. - # # It can be read using the signed method <tt>cookies.signed[:name]</tt> + # # The cookie is signed by your app's `secrets.secret_key_base` value. + # # It can be read using the signed method `cookies.signed[:name]` # cookies.signed[:user_id] = current_user.id # # # Sets a "permanent" cookie (which expires in 20 years from now). @@ -42,10 +42,10 @@ module ActionDispatch # # Examples of reading: # - # cookies[:user_name] # => "david" - # cookies.size # => 2 - # cookies[:lat_lon] # => [47.68, -122.37] - # cookies.signed[:login] # => "XJ-122" + # cookies[:user_name] # => "david" + # cookies.size # => 2 + # JSON.parse(cookies[:lat_lon]) # => [47.68, -122.37] + # cookies.signed[:login] # => "XJ-122" # # Example for deleting: # @@ -63,7 +63,7 @@ module ActionDispatch # # The option symbols for setting cookies are: # - # * <tt>:value</tt> - The cookie's value or list of values (as an array). + # * <tt>:value</tt> - The cookie's value. # * <tt>:path</tt> - The path for which this cookie applies. Defaults to the root # of the application. # * <tt>:domain</tt> - The domain for which this cookie applies so you can @@ -74,7 +74,7 @@ module ActionDispatch # # domain: nil # Does not sets cookie domain. (default) # domain: :all # Allow the cookie for the top most level - # domain and subdomains. + # # domain and subdomains. # # * <tt>:expires</tt> - The time at which this cookie expires, as a \Time object. # * <tt>:secure</tt> - Whether this cookie is only transmitted to HTTPS servers. @@ -89,6 +89,7 @@ module ActionDispatch ENCRYPTED_SIGNED_COOKIE_SALT = "action_dispatch.encrypted_signed_cookie_salt".freeze SECRET_TOKEN = "action_dispatch.secret_token".freeze SECRET_KEY_BASE = "action_dispatch.secret_key_base".freeze + COOKIES_SERIALIZER = "action_dispatch.cookies_serializer".freeze # Cookies can typically store 4096 bytes. MAX_COOKIE_SIZE = 4096 @@ -117,10 +118,10 @@ module ActionDispatch # the cookie again. This is useful for creating cookies with values that the user is not supposed to change. If a signed # cookie was tampered with by the user (or a 3rd party), nil will be returned. # - # If +config.secret_key_base+ and +config.secret_token+ (deprecated) are both set, + # If +secrets.secret_key_base+ and +config.secret_token+ (deprecated) are both set, # legacy cookies signed with the old key generator will be transparently upgraded. # - # This jar requires that you set a suitable secret for the verification on your app's +config.secret_key_base+. + # This jar requires that you set a suitable secret for the verification on your app's +secrets.secret_key_base+. # # Example: # @@ -140,10 +141,10 @@ module ActionDispatch # Returns a jar that'll automatically encrypt cookie values before sending them to the client and will decrypt them for read. # If the cookie was tampered with by the user (or a 3rd party), nil will be returned. # - # If +config.secret_key_base+ and +config.secret_token+ (deprecated) are both set, + # If +secrets.secret_key_base+ and +config.secret_token+ (deprecated) are both set, # legacy cookies signed with the old key generator will be transparently upgraded. # - # This jar requires that you set a suitable secret for the verification on your app's +config.secret_key_base+. + # This jar requires that you set a suitable secret for the verification on your app's +secrets.secret_key_base+. # # Example: # @@ -175,12 +176,12 @@ module ActionDispatch module VerifyAndUpgradeLegacySignedMessage def initialize(*args) super - @legacy_verifier = ActiveSupport::MessageVerifier.new(@options[:secret_token]) + @legacy_verifier = ActiveSupport::MessageVerifier.new(@options[:secret_token], serializer: NullSerializer) end def verify_and_upgrade_legacy_signed_message(name, signed_message) - @legacy_verifier.verify(signed_message).tap do |value| - self[name] = value + deserialize(name, @legacy_verifier.verify(signed_message)).tap do |value| + self[name] = { value: value } end rescue ActiveSupport::MessageVerifier::InvalidSignature nil @@ -210,7 +211,8 @@ module ActionDispatch encrypted_signed_cookie_salt: env[ENCRYPTED_SIGNED_COOKIE_SALT] || '', secret_token: env[SECRET_TOKEN], secret_key_base: env[SECRET_KEY_BASE], - upgrade_legacy_signed_cookies: env[SECRET_TOKEN].present? && env[SECRET_KEY_BASE].present? + upgrade_legacy_signed_cookies: env[SECRET_TOKEN].present? && env[SECRET_KEY_BASE].present?, + serializer: env[COOKIES_SERIALIZER] } end @@ -235,6 +237,15 @@ module ActionDispatch @secure = secure @options = options @cookies = {} + @committed = false + end + + def committed?; @committed; end + + def commit! + @committed = true + @set_cookies.freeze + @delete_cookies.freeze end def each(&block) @@ -334,8 +345,8 @@ module ActionDispatch end def recycle! #:nodoc: - @set_cookies.clear - @delete_cookies.clear + @set_cookies = {} + @delete_cookies = {} end mattr_accessor :always_write_cookie @@ -372,28 +383,89 @@ module ActionDispatch end end + class JsonSerializer + def self.load(value) + JSON.parse(value, quirks_mode: true) + end + + def self.dump(value) + JSON.generate(value, quirks_mode: true) + end + end + + # Passing the NullSerializer downstream to the Message{Encryptor,Verifier} + # allows us to handle the (de)serialization step within the cookie jar, + # which gives us the opportunity to detect and migrate legacy cookies. + class NullSerializer + def self.load(value) + value + end + + def self.dump(value) + value + end + end + + module SerializedCookieJars + MARSHAL_SIGNATURE = "\x04\x08".freeze + + protected + def needs_migration?(value) + @options[:serializer] == :hybrid && value.start_with?(MARSHAL_SIGNATURE) + end + + def serialize(name, value) + serializer.dump(value) + end + + def deserialize(name, value) + if value + if needs_migration?(value) + Marshal.load(value).tap do |v| + self[name] = { value: v } + end + else + serializer.load(value) + end + end + end + + def serializer + serializer = @options[:serializer] || :marshal + case serializer + when :marshal + Marshal + when :json, :hybrid + JsonSerializer + else + serializer + end + end + end + class SignedCookieJar #:nodoc: include ChainedCookieJars + include SerializedCookieJars def initialize(parent_jar, key_generator, options = {}) @parent_jar = parent_jar @options = options secret = key_generator.generate_key(@options[:signed_cookie_salt]) - @verifier = ActiveSupport::MessageVerifier.new(secret) + @verifier = ActiveSupport::MessageVerifier.new(secret, serializer: NullSerializer) end def [](name) if signed_message = @parent_jar[name] - verify(signed_message) + deserialize name, verify(signed_message) end end def []=(name, options) if options.is_a?(Hash) options.symbolize_keys! - options[:value] = @verifier.generate(options[:value]) + options[:value] = @verifier.generate(serialize(name, options[:value])) else - options = { :value => @verifier.generate(options) } + options = { :value => @verifier.generate(serialize(name, options)) } end raise CookieOverflow if options[:value].size > MAX_COOKIE_SIZE @@ -409,7 +481,7 @@ module ActionDispatch end # UpgradeLegacySignedCookieJar is used instead of SignedCookieJar if - # config.secret_token and config.secret_key_base are both set. It reads + # config.secret_token and secrets.secret_key_base are both set. It reads # legacy cookies signed with the old dummy key generator and re-saves # them using the new key generator to provide a smooth upgrade path. class UpgradeLegacySignedCookieJar < SignedCookieJar #:nodoc: @@ -417,17 +489,18 @@ module ActionDispatch def [](name) if signed_message = @parent_jar[name] - verify(signed_message) || verify_and_upgrade_legacy_signed_message(name, signed_message) + deserialize(name, verify(signed_message)) || verify_and_upgrade_legacy_signed_message(name, signed_message) end end end class EncryptedCookieJar #:nodoc: include ChainedCookieJars + include SerializedCookieJars def initialize(parent_jar, key_generator, options = {}) if ActiveSupport::LegacyKeyGenerator === key_generator - raise "You didn't set config.secret_key_base, which is required for this cookie jar. " + + raise "You didn't set secrets.secret_key_base, which is required for this cookie jar. " + "Read the upgrade documentation to learn more about this new config option." end @@ -435,12 +508,12 @@ module ActionDispatch @options = options secret = key_generator.generate_key(@options[:encrypted_cookie_salt]) sign_secret = key_generator.generate_key(@options[:encrypted_signed_cookie_salt]) - @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret) + @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, serializer: NullSerializer) end def [](name) if encrypted_message = @parent_jar[name] - decrypt_and_verify(encrypted_message) + deserialize name, decrypt_and_verify(encrypted_message) end end @@ -450,7 +523,8 @@ module ActionDispatch else options = { :value => options } end - options[:value] = @encryptor.encrypt_and_sign(options[:value]) + + options[:value] = @encryptor.encrypt_and_sign(serialize(name, options[:value])) raise CookieOverflow if options[:value].size > MAX_COOKIE_SIZE @parent_jar[name] = options @@ -465,7 +539,7 @@ module ActionDispatch end # UpgradeLegacyEncryptedCookieJar is used by ActionDispatch::Session::CookieStore - # instead of EncryptedCookieJar if config.secret_token and config.secret_key_base + # instead of EncryptedCookieJar if config.secret_token and secrets.secret_key_base # are both set. It reads legacy cookies signed with the old dummy key generator and # encrypts and re-saves them using the new key generator to provide a smooth upgrade path. class UpgradeLegacyEncryptedCookieJar < EncryptedCookieJar #:nodoc: @@ -473,7 +547,7 @@ module ActionDispatch def [](name) if encrypted_or_signed_message = @parent_jar[name] - decrypt_and_verify(encrypted_or_signed_message) || verify_and_upgrade_legacy_signed_message(name, encrypted_or_signed_message) + deserialize(name, decrypt_and_verify(encrypted_or_signed_message)) || verify_and_upgrade_legacy_signed_message(name, encrypted_or_signed_message) end end end @@ -486,9 +560,11 @@ module ActionDispatch status, headers, body = @app.call(env) if cookie_jar = env['action_dispatch.cookies'] - cookie_jar.write(headers) - if headers[HTTP_HEADER].respond_to?(:join) - headers[HTTP_HEADER] = headers[HTTP_HEADER].join("\n") + unless cookie_jar.committed? + cookie_jar.write(headers) + if headers[HTTP_HEADER].respond_to?(:join) + headers[HTTP_HEADER] = headers[HTTP_HEADER].join("\n") + end end end diff --git a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb index 64230ff1ae..0ca1a87645 100644 --- a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb @@ -34,27 +34,35 @@ module ActionDispatch log_error(env, wrapper) if env['action_dispatch.show_detailed_exceptions'] + request = Request.new(env) 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, - :routes_inspector => routes_inspector(exception), - :source_extract => wrapper.source_extract, - :line_number => wrapper.line_number, - :file => wrapper.file + request: request, + exception: wrapper.exception, + application_trace: wrapper.application_trace, + framework_trace: wrapper.framework_trace, + full_trace: wrapper.full_trace, + routes_inspector: routes_inspector(exception), + source_extract: wrapper.source_extract, + line_number: wrapper.line_number, + file: wrapper.file ) file = "rescues/#{wrapper.rescue_template}" - body = template.render(:template => file, :layout => 'rescues/layout') - render(wrapper.status_code, body) + + if request.xhr? + body = template.render(template: file, layout: false, formats: [:text]) + format = "text/plain" + else + body = template.render(template: file, layout: 'rescues/layout') + format = "text/html" + end + render(wrapper.status_code, body, format) 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]] + def render(status, body, format) + [status, {'Content-Type' => "#{format}; charset=#{Response.default_charset}", 'Content-Length' => body.bytesize.to_s}, [body]] end def log_error(env, wrapper) diff --git a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb index 1de3d14530..2326bb043a 100644 --- a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb +++ b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb @@ -1,5 +1,5 @@ require 'action_controller/metal/exceptions' -require 'active_support/core_ext/class/attribute_accessors' +require 'active_support/core_ext/module/attribute_accessors' module ActionDispatch class ExceptionWrapper @@ -32,6 +32,8 @@ module ActionDispatch def initialize(env, exception) @env = env @exception = original_exception(exception) + + expand_backtrace if exception.is_a?(SyntaxError) || exception.try(:original_exception).try(:is_a?, SyntaxError) end def rescue_template @@ -96,7 +98,7 @@ module ActionDispatch def source_fragment(path, line) return unless Rails.respond_to?(:root) && Rails.root full_path = Rails.root.join(path) - if File.exists?(full_path) + if File.exist?(full_path) File.open(full_path, "r") do |file| start = [line - 3, 0].max lines = file.each_line.drop(start).take(6) @@ -104,5 +106,11 @@ module ActionDispatch end end end + + def expand_backtrace + @exception.backtrace.unshift( + @exception.to_s.split("\n") + ).flatten! + end end end diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb index 89003e7a5e..4821d2a899 100644 --- a/actionpack/lib/action_dispatch/middleware/flash.rb +++ b/actionpack/lib/action_dispatch/middleware/flash.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/hash/keys' + module ActionDispatch class Request < Rack::Request # Access the contents of the flash. Use <tt>flash["notice"]</tt> to @@ -50,13 +52,14 @@ module ActionDispatch end def []=(k, v) + k = k.to_s @flash[k] = v @flash.discard(k) v end def [](k) - @flash[k] + @flash[k.to_s] end # Convenience accessor for <tt>flash.now[:alert]=</tt>. @@ -92,8 +95,8 @@ module ActionDispatch end def initialize(flashes = {}, discard = []) #:nodoc: - @discard = Set.new(discard) - @flashes = flashes + @discard = Set.new(stringify_array(discard)) + @flashes = flashes.stringify_keys @now = nil end @@ -106,17 +109,18 @@ module ActionDispatch end def []=(k, v) + k = k.to_s @discard.delete k @flashes[k] = v end def [](k) - @flashes[k] + @flashes[k.to_s] end def update(h) #:nodoc: - @discard.subtract h.keys - @flashes.update h + @discard.subtract stringify_array(h.keys) + @flashes.update h.stringify_keys self end @@ -129,6 +133,7 @@ module ActionDispatch end def delete(key) + key = key.to_s @discard.delete key @flashes.delete key self @@ -155,7 +160,7 @@ module ActionDispatch def replace(h) #:nodoc: @discard.clear - @flashes.replace h + @flashes.replace h.stringify_keys self end @@ -186,6 +191,7 @@ module ActionDispatch # 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) + k = k.to_s if k @discard.subtract Array(k || keys) k ? self[k] : self end @@ -195,6 +201,7 @@ 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) + k = k.to_s if k @discard.merge Array(k || keys) k ? self[k] : self end @@ -231,6 +238,12 @@ module ActionDispatch def now_is_loaded? @now end + + def stringify_array(array) + array.map do |item| + item.kind_of?(Symbol) ? item.to_s : item + end + end end def initialize(app) diff --git a/actionpack/lib/action_dispatch/middleware/params_parser.rb b/actionpack/lib/action_dispatch/middleware/params_parser.rb index fb70b60ef6..b426183488 100644 --- a/actionpack/lib/action_dispatch/middleware/params_parser.rb +++ b/actionpack/lib/action_dispatch/middleware/params_parser.rb @@ -41,7 +41,7 @@ module ActionDispatch when Proc strategy.call(request.raw_post) when :json - data = ActiveSupport::JSON.decode(request.body) + data = ActiveSupport::JSON.decode(request.raw_post) data = {:_json => data} unless data.is_a?(Hash) Request::Utils.deep_munge(data).with_indifferent_access else diff --git a/actionpack/lib/action_dispatch/middleware/reloader.rb b/actionpack/lib/action_dispatch/middleware/reloader.rb index 2f6968eb2e..15b5a48535 100644 --- a/actionpack/lib/action_dispatch/middleware/reloader.rb +++ b/actionpack/lib/action_dispatch/middleware/reloader.rb @@ -1,3 +1,5 @@ +require 'active_support/deprecation/reporting' + module ActionDispatch # ActionDispatch::Reloader provides prepare and cleanup callbacks, # intended to assist with code reloading during development. @@ -25,19 +27,26 @@ module ActionDispatch # class Reloader include ActiveSupport::Callbacks + include ActiveSupport::Deprecation::Reporting - define_callbacks :prepare, :scope => :name - define_callbacks :cleanup, :scope => :name + define_callbacks :prepare + define_callbacks :cleanup # Add a prepare callback. Prepare callbacks are run before each request, prior # to ActionDispatch::Callback's before callbacks. def self.to_prepare(*args, &block) + unless block_given? + warn "to_prepare without a block is deprecated. Please use a block" + end set_callback(:prepare, *args, &block) end # Add a cleanup callback. Cleanup callbacks are run after each request is # complete (after #close is called on the response body). def self.to_cleanup(*args, &block) + unless block_given? + warn "to_cleanup without a block is deprecated. Please use a block" + end set_callback(:cleanup, *args, &block) end diff --git a/actionpack/lib/action_dispatch/middleware/remote_ip.rb b/actionpack/lib/action_dispatch/middleware/remote_ip.rb index 8879291dbd..cbb066b092 100644 --- a/actionpack/lib/action_dispatch/middleware/remote_ip.rb +++ b/actionpack/lib/action_dispatch/middleware/remote_ip.rb @@ -31,7 +31,7 @@ module ActionDispatch TRUSTED_PROXIES = %r{ ^127\.0\.0\.1$ | # localhost IPv4 ^::1$ | # localhost IPv6 - ^fc00: | # private IPv6 range fc00 + ^[fF][cCdD] | # private IPv6 range fc00::/7 ^10\. | # private IPv4 range 10.x.x.x ^172\.(1[6-9]|2[0-9]|3[0-1])\.| # private IPv4 range 172.16.0.0 .. 172.31.255.255 ^192\.168\. # private IPv4 range 192.168.x.x @@ -47,12 +47,12 @@ module ActionDispatch # clients (like WAP devices), or behind proxies that set headers in an # incorrect or confusing way (like AWS ELB). # - # The +custom_trusted+ argument can take a regex, which will be used + # The +custom_proxies+ argument can take a regex, which will be used # instead of +TRUSTED_PROXIES+, or a string, which will be used in addition # to +TRUSTED_PROXIES+. Any proxy setup will put the value you want in the # middle (or at the beginning) of the X-Forwarded-For list, with your proxy # servers after it. If your proxies aren't removed, pass them in via the - # +custom_trusted+ parameter. That way, the middleware will ignore those + # +custom_proxies+ parameter. That way, the middleware will ignore those # IP addresses, and return the one that you want. def initialize(app, check_ip_spoofing = true, custom_proxies = nil) @app = app @@ -143,7 +143,7 @@ module ActionDispatch # proxies with incompatible IP header conventions, and there is no way # for us to determine which header is the right one after the fact. # Since we have no idea, we give up and explode. - should_check_ip = @check_ip && client_ips.last + should_check_ip = @check_ip && client_ips.last && forwarded_ips.last if should_check_ip && !forwarded_ips.include?(client_ips.last) # We don't know which came from the proxy, and which from the user raise IpSpoofAttackError, "IP spoofing attack?! " + diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb index b9eb8036e9..0864e7ef2a 100644 --- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb @@ -15,8 +15,8 @@ module ActionDispatch # best possible option given your application's configuration. # # If you only have secret_token set, your cookies will be signed, but - # not encrypted. This means a user cannot alter his +user_id+ without - # knowing your app's secret key, but can easily read his +user_id+. This + # not encrypted. This means a user cannot alter their +user_id+ without + # knowing your app's secret key, but can easily read their +user_id+. This # was the default for Rails 3 apps. # # If you have secret_key_base set, your cookies will be encrypted. This @@ -29,11 +29,12 @@ module ActionDispatch # # Configure your session store in config/initializers/session_store.rb: # - # Myapp::Application.config.session_store :cookie_store, key: '_your_app_session' + # Rails.application.config.session_store :cookie_store, key: '_your_app_session' # - # Configure your secret key in config/initializers/secret_token.rb: + # Configure your secret key in config/secrets.yml: # - # Myapp::Application.config.secret_key_base 'secret key' + # development: + # secret_key_base: 'secret key' # # To generate a secret key for an existing application, run `rake secret`. # @@ -50,7 +51,7 @@ module ActionDispatch # decode signed cookies generated by your app in external applications or # Javascript before upgrading. # - # Note that changing digest or secret invalidates all existing sessions! + # Note that changing the secret key will invalidate all existing sessions! class CookieStore < Rack::Session::Abstract::ID include Compatibility include StaleSessionCheck diff --git a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb index fcc5bc12c4..1d4f0f89a6 100644 --- a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb @@ -29,8 +29,11 @@ module ActionDispatch def call(env) @app.call(env) rescue Exception => exception - raise exception if env['action_dispatch.show_exceptions'] == false - render_exception(env, exception) + if env['action_dispatch.show_exceptions'] == false + raise exception + else + render_exception(env, exception) + end end private diff --git a/actionpack/lib/action_dispatch/middleware/ssl.rb b/actionpack/lib/action_dispatch/middleware/ssl.rb index 999c022535..0c7caef25d 100644 --- a/actionpack/lib/action_dispatch/middleware/ssl.rb +++ b/actionpack/lib/action_dispatch/middleware/ssl.rb @@ -32,11 +32,14 @@ module ActionDispatch private def redirect_to_https(request) - url = URI(request.url) - url.scheme = "https" - url.host = @host if @host - url.port = @port if @port - headers = { 'Content-Type' => 'text/html', 'Location' => url.to_s } + host = @host || request.host + port = @port || request.port + + location = "https://#{host}" + location << ":#{port}" if port != 80 + location << request.fullpath + + headers = { 'Content-Type' => 'text/html', 'Location' => location } [301, headers, []] end diff --git a/actionpack/lib/action_dispatch/middleware/static.rb b/actionpack/lib/action_dispatch/middleware/static.rb index c6a7d9c415..2764584fe9 100644 --- a/actionpack/lib/action_dispatch/middleware/static.rb +++ b/actionpack/lib/action_dispatch/middleware/static.rb @@ -11,9 +11,10 @@ module ActionDispatch end def match?(path) - path = path.dup + path = unescape_path(path) + return false unless path.valid_encoding? - full_path = path.empty? ? @root : File.join(@root, escape_glob_chars(unescape_path(path))) + full_path = path.empty? ? @root : File.join(@root, escape_glob_chars(path)) paths = "#{full_path}#{ext}" matches = Dir[paths] @@ -40,7 +41,6 @@ module ActionDispatch end def escape_glob_chars(path) - path.force_encoding('binary') if path.respond_to? :force_encoding path.gsub(/[*?{}\[\]]/, "\\\\\\&") end end diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb index db219c8fa9..db219c8fa9 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb new file mode 100644 index 0000000000..396768ecee --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb @@ -0,0 +1,23 @@ +<% + clean_params = @request.filtered_parameters.clone + clean_params.delete("action") + clean_params.delete("controller") + + request_dump = clean_params.empty? ? 'None' : clean_params.inspect.gsub(',', ",\n") + + def debug_hash(object) + object.to_hash.sort_by { |k, _| k.to_s }.map { |k, v| "#{k}: #{v.inspect rescue $!.message}" }.join("\n") + end unless self.class.method_defined?(:debug_hash) +%> + +Request parameters +<%= request_dump %> + +Session dump +<%= debug_hash @request.session %> + +Env dump +<%= debug_hash @request.env.slice(*@request.class::ENV_METHODS) %> + +Response headers +<%= defined?(@response) ? @response.headers.inspect.gsub(',', ",\n") : 'None' %> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb index b181909bff..b181909bff 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb new file mode 100644 index 0000000000..d4af5c9b06 --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb @@ -0,0 +1,15 @@ +<% + traces = { "Application Trace" => @application_trace, + "Framework Trace" => @framework_trace, + "Full Trace" => @full_trace } +%> + +Rails.root: <%= defined?(Rails) && Rails.respond_to?(:root) ? Rails.root : "unset" %> + +<% traces.each do |name, trace| %> +<% if trace.any? %> +<%= name %> +<%= trace.join("\n") %> + +<% end %> +<% end %> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb index 57a2940802..f154021ae6 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb @@ -8,7 +8,7 @@ </header> <div id="container"> - <h2><%= @exception.message %></h2> + <h2><%= h @exception.message %></h2> <%= render template: "rescues/_source" %> <%= render template: "rescues/_trace" %> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb new file mode 100644 index 0000000000..603de54b8b --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb @@ -0,0 +1,9 @@ +<%= @exception.class.to_s %><% + if @request.parameters['controller'] +%> in <%= @request.parameters['controller'].camelize %>Controller<% if @request.parameters['action'] %>#<%= @request.parameters['action'] %><% end %> +<% end %> + +<%= @exception.message %> +<%= render template: "rescues/_source" %> +<%= render template: "rescues/_trace" %> +<%= render template: "rescues/_request_and_response" %> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb index ca14215946..5c016e544e 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb @@ -3,5 +3,5 @@ </header> <div id="container"> - <h2><%= @exception.message %></h2> + <h2><%= h @exception.message %></h2> </div> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.text.erb new file mode 100644 index 0000000000..ae62d9eb02 --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.text.erb @@ -0,0 +1,3 @@ +Template is missing + +<%= @exception.message %> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb index cd3daff065..7e9cedb95e 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb @@ -2,7 +2,7 @@ <h1>Routing Error</h1> </header> <div id="container"> - <h2><%= @exception.message %></h2> + <h2><%= h @exception.message %></h2> <% unless @exception.failures.empty? %> <p> <h2>Failure reasons:</h2> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.text.erb new file mode 100644 index 0000000000..f6e4dac1f3 --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.text.erb @@ -0,0 +1,11 @@ +Routing Error + +<%= @exception.message %> +<% unless @exception.failures.empty? %> +Failure reasons: +<% @exception.failures.each do |route, reason| %> + - <%= route.inspect.delete('\\') %></code> failed because <%= reason.downcase %> +<% end %> +<% end %> + +<%= render template: "rescues/_trace", format: :text %> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb index 31f46ee340..027a0f5b3e 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb @@ -10,7 +10,7 @@ <p> Showing <i><%= @exception.file_name %></i> where line <b>#<%= @exception.line_number %></b> raised: </p> - <pre><code><%= @exception.message %></code></pre> + <pre><code><%= h @exception.message %></code></pre> <div class="source"> <div class="info"> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb new file mode 100644 index 0000000000..5da21d9784 --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb @@ -0,0 +1,8 @@ +<% @source_extract = @exception.source_extract(0, :html) %> +<%= @exception.original_exception.class.to_s %> in <%= @request.parameters["controller"].camelize if @request.parameters["controller"] %>#<%= @request.parameters["action"] %> + +Showing <%= @exception.file_name %> where line #<%= @exception.line_number %> raised: +<%= @exception.message %> +<%= @exception.sub_template_message %> +<%= render template: "rescues/_trace", format: :text %> +<%= render template: "rescues/_request_and_response", format: :text %> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/unknown_action.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb index c1fbf67eed..259fb2bb3b 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/unknown_action.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb @@ -2,5 +2,5 @@ <h1>Unknown action</h1> </header> <div id="container"> - <h2><%= @exception.message %></h2> + <h2><%= h @exception.message %></h2> </div> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb new file mode 100644 index 0000000000..83973addcb --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb @@ -0,0 +1,3 @@ +Unknown action + +<%= @exception.message %> diff --git a/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb b/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb index 95461fa693..cce0d75af4 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb @@ -4,21 +4,41 @@ border-collapse: collapse; } - #route_table td { - padding: 0 30px; + #route_table thead tr { + border-bottom: 2px solid #ddd; + } + + #route_table thead tr.bottom { + border-bottom: none; } - #route_table tr.bottom th { - padding-bottom: 10px; + #route_table thead tr.bottom th { + padding: 10px 0; line-height: 15px; } - #route_table .matched_paths { + #route_table tbody tr { + border-bottom: 1px solid #ddd; + } + + #route_table tbody tr:nth-child(odd) { + background: #f2f2f2; + } + + #route_table tbody.exact_matches, + #route_table tbody.fuzzy_matches { background-color: LightGoldenRodYellow; + border-bottom: solid 2px SlateGrey; } - #route_table .matched_paths { - border-bottom: solid 3px SlateGrey; + #route_table tbody.exact_matches tr, + #route_table tbody.fuzzy_matches tr { + background: none; + border-bottom: none; + } + + #route_table td { + padding: 4px 30px; } #path_search { @@ -45,13 +65,15 @@ <th><%# HTTP Verb %> </th> <th><%# Path %> - <%= search_field(:path, nil, id: 'path_search', placeholder: "Path Match") %> + <%= search_field(:path, nil, id: 'search', placeholder: "Path Match") %> </th> <th><%# Controller#action %> </th> </tr> </thead> - <tbody class='matched_paths' id='matched_paths'> + <tbody class='exact_matches' id='exact_matches'> + </tbody> + <tbody class='fuzzy_matches' id='fuzzy_matches'> </tbody> <tbody> <%= yield %> @@ -59,6 +81,7 @@ </table> <script type='text/javascript'> + // Iterates each element through a function function each(elems, func) { if (!elems instanceof Array) { elems = [elems]; } for (var i = 0, len = elems.length; i < len; i++) { @@ -66,77 +89,110 @@ } } - function setValOn(elems, val) { - each(elems, function(elem) { - elem.innerHTML = val; - }); + // Sets innerHTML for an element + function setContent(elem, text) { + elem.innerHTML = text; } - function onClick(elems, func) { - each(elems, function(elem) { - elem.onclick = func; - }); - } + // Enables path search functionality + function setupMatchPaths() { + // Check if the user input (sanitized as a path) matches the regexp data attribute + function checkExactMatch(section, elem, value) { + var string = sanitizePath(value), + regexp = elem.getAttribute("data-regexp"); - // Enables functionality to toggle between `_path` and `_url` helper suffixes - function setupRouteToggleHelperLinks() { - var toggleLinks = document.querySelectorAll('#route_table [data-route-helper]'); - onClick(toggleLinks, function(){ - var helperTxt = this.getAttribute("data-route-helper"), - helperElems = document.querySelectorAll('[data-route-name] span.helper'); - setValOn(helperElems, helperTxt); - }); - } + showMatch(string, regexp, section, elem); + } - // takes an array of elements with a data-regexp attribute and - // passes their their parent <tr> into the callback function - // if the regexp matchs a given path - function eachElemsForPath(elems, path, func) { - each(elems, function(e){ - var reg = e.getAttribute("data-regexp"); - if (path.match(RegExp(reg))) { - func(e.parentNode.cloneNode(true)); - } - }) - } + // Check if the route path data attribute contains the user input + function checkFuzzyMatch(section, elem, value) { + var string = elem.getAttribute("data-route-path"), + regexp = value; - // Ensure path always starts with a slash "/" and remove params or fragments - function sanitizePath(path) { - var path = path.charAt(0) == '/' ? path : "/" + path; - return path.replace(/\#.*|\?.*/, ''); - } + showMatch(string, regexp, section, elem); + } - // Enables path search functionality - function setupMatchPaths() { - var regexpElems = document.querySelectorAll('#route_table [data-regexp]'), - pathElem = document.querySelector('#path_search'), - selectedSection = document.querySelector('#matched_paths'), - noMatchText = '<tr><th colspan="4">None</th></tr>'; + // Display the parent <tr> element in the appropriate section when there's a match + function showMatch(string, regexp, section, elem) { + if(string.match(RegExp(regexp))) { + section.appendChild(elem.parentNode.cloneNode(true)); + } + } + + // Check if there are any matched results in a section + function checkNoMatch(section, defaultText, noMatchText) { + if (section.innerHTML === defaultText) { + setContent(section, defaultText + noMatchText); + } + } + // Ensure path always starts with a slash "/" and remove params or fragments + function sanitizePath(path) { + var path = path.charAt(0) == '/' ? path : "/" + path; + return path.replace(/\#.*|\?.*/, ''); + } - // Remove matches if no path is present - pathElem.onblur = function(e) { - if (pathElem.value === "") selectedSection.innerHTML = ""; + var regexpElems = document.querySelectorAll('#route_table [data-regexp]'), + searchElem = document.querySelector('#search'), + exactMatches = document.querySelector('#exact_matches'), + fuzzyMatches = document.querySelector('#fuzzy_matches'); + + // Remove matches when no search value is present + searchElem.onblur = function(e) { + if (searchElem.value === "") { + setContent(exactMatches, ""); + setContent(fuzzyMatches, ""); + } } // On key press perform a search for matching paths - pathElem.onkeyup = function(e){ - var path = sanitizePath(pathElem.value), - defaultText = '<tr><th colspan="4">Paths Matching (' + path + '):</th></tr>'; + searchElem.onkeyup = function(e){ + var userInput = searchElem.value, + defaultExactMatch = '<tr><th colspan="4">Paths Matching (' + sanitizePath(userInput) +'):</th></tr>', + defaultFuzzyMatch = '<tr><th colspan="4">Paths Containing (' + userInput +'):</th></tr>', + noExactMatch = '<tr><th colspan="4">No Exact Matches Found</th></tr>', + noFuzzyMatch = '<tr><th colspan="4">No Fuzzy Matches Found</th></tr>'; // Clear out results section - selectedSection.innerHTML= defaultText; + setContent(exactMatches, defaultExactMatch); + setContent(fuzzyMatches, defaultFuzzyMatch); + + // Display exact matches and fuzzy matches + each(regexpElems, function(elem) { + checkExactMatch(exactMatches, elem, userInput); + checkFuzzyMatch(fuzzyMatches, elem, userInput); + }) + + // Display 'No Matches' message when no matches are found + checkNoMatch(exactMatches, defaultExactMatch, noExactMatch); + checkNoMatch(fuzzyMatches, defaultFuzzyMatch, noFuzzyMatch); + } + } - // Display matches if they exist - eachElemsForPath(regexpElems, path, function(e){ - selectedSection.appendChild(e); + // Enables functionality to toggle between `_path` and `_url` helper suffixes + function setupRouteToggleHelperLinks() { + + // Sets content for each element + function setValOn(elems, val) { + each(elems, function(elem) { + setContent(elem, val); }); + } - // If no match present, tell the user - if (selectedSection.innerHTML === defaultText) { - selectedSection.innerHTML = selectedSection.innerHTML + noMatchText; - } + // Sets onClick event for each element + function onClick(elems, func) { + each(elems, function(elem) { + elem.onclick = func; + }); } + + var toggleLinks = document.querySelectorAll('#route_table [data-route-helper]'); + onClick(toggleLinks, function(){ + var helperTxt = this.getAttribute("data-route-helper"), + helperElems = document.querySelectorAll('[data-route-name] span.helper'); + + setValOn(helperElems, helperTxt); + }); } setupMatchPaths(); diff --git a/actionpack/lib/action_dispatch/railtie.rb b/actionpack/lib/action_dispatch/railtie.rb index 2dfaab3587..ddeea24bb3 100644 --- a/actionpack/lib/action_dispatch/railtie.rb +++ b/actionpack/lib/action_dispatch/railtie.rb @@ -16,6 +16,7 @@ module ActionDispatch config.action_dispatch.signed_cookie_salt = 'signed cookie' config.action_dispatch.encrypted_cookie_salt = 'encrypted cookie' config.action_dispatch.encrypted_signed_cookie_salt = 'signed encrypted cookie' + config.action_dispatch.perform_deep_munge = true config.action_dispatch.default_headers = { 'X-Frame-Options' => 'SAMEORIGIN', @@ -28,6 +29,7 @@ module ActionDispatch 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::Request::Utils.perform_deep_munge = app.config.action_dispatch.perform_deep_munge ActionDispatch::Response.default_charset = app.config.action_dispatch.default_charset || app.config.encoding ActionDispatch::Response.default_headers = app.config.action_dispatch.default_headers diff --git a/actionpack/lib/action_dispatch/request/session.rb b/actionpack/lib/action_dispatch/request/session.rb index 7bc812fd22..973627f106 100644 --- a/actionpack/lib/action_dispatch/request/session.rb +++ b/actionpack/lib/action_dispatch/request/session.rb @@ -7,6 +7,9 @@ module ActionDispatch ENV_SESSION_KEY = Rack::Session::Abstract::ENV_SESSION_KEY # :nodoc: ENV_SESSION_OPTIONS_KEY = Rack::Session::Abstract::ENV_SESSION_OPTIONS_KEY # :nodoc: + # Singleton object used to determine if an optional param wasn't specified + Unspecified = Object.new + def self.create(store, env, default_options) session_was = find env session = Request::Session.new(store, env) @@ -127,6 +130,15 @@ module ActionDispatch @delegate.delete key.to_s end + def fetch(key, default=Unspecified, &block) + load_for_read! + if default == Unspecified + @delegate.fetch(key.to_s, &block) + else + @delegate.fetch(key.to_s, default, &block) + end + end + def inspect if loaded? super diff --git a/actionpack/lib/action_dispatch/request/utils.rb b/actionpack/lib/action_dispatch/request/utils.rb index 8b43cdada8..9d4f1aa3c5 100644 --- a/actionpack/lib/action_dispatch/request/utils.rb +++ b/actionpack/lib/action_dispatch/request/utils.rb @@ -1,18 +1,29 @@ module ActionDispatch class Request < Rack::Request class Utils # :nodoc: + + mattr_accessor :perform_deep_munge + self.perform_deep_munge = true + class << self # Remove nils from the params hash - def deep_munge(hash) + def deep_munge(hash, keys = []) + return hash unless perform_deep_munge + hash.each do |k, v| + keys << k case v when Array - v.grep(Hash) { |x| deep_munge(x) } + v.grep(Hash) { |x| deep_munge(x, keys) } v.compact! - hash[k] = nil if v.empty? + if v.empty? + hash[k] = nil + ActiveSupport::Notifications.instrument("deep_munge.action_controller", keys: keys) + end when Hash - deep_munge(v) + deep_munge(v, keys) end + keys.pop end hash diff --git a/actionpack/lib/action_dispatch/routing.rb b/actionpack/lib/action_dispatch/routing.rb index a9ac2bce1d..ce03164ca9 100644 --- a/actionpack/lib/action_dispatch/routing.rb +++ b/actionpack/lib/action_dispatch/routing.rb @@ -1,6 +1,7 @@ # encoding: UTF-8 require 'active_support/core_ext/object/to_param' require 'active_support/core_ext/regexp' +require 'active_support/dependencies/autoload' module ActionDispatch # The routing module provides URL rewriting in native Ruby. It's a way to @@ -11,7 +12,7 @@ module ActionDispatch # Think of creating routes as drawing a map for your requests. The map tells # them where to go based on some predefined pattern: # - # AppName::Application.routes.draw do + # Rails.application.routes.draw do # Pattern 1 tells some request to go to one place # Pattern 2 tell them to go to another # ... diff --git a/actionpack/lib/action_dispatch/routing/inspector.rb b/actionpack/lib/action_dispatch/routing/inspector.rb index cffb814e1e..71a0c5e826 100644 --- a/actionpack/lib/action_dispatch/routing/inspector.rb +++ b/actionpack/lib/action_dispatch/routing/inspector.rb @@ -69,7 +69,7 @@ module ActionDispatch end def internal? - controller.to_s =~ %r{\Arails/(info|welcome)} || path =~ %r{\A#{Rails.application.config.assets.prefix}} + controller.to_s =~ %r{\Arails/(info|mailers|welcome)} || path =~ %r{\A#{Rails.application.config.assets.prefix}\z} end def engine? @@ -179,7 +179,8 @@ module ActionDispatch private def draw_section(routes) - name_width, verb_width, path_width = widths(routes) + header_lengths = ['Prefix', 'Verb', 'URI Pattern'].map(&:length) + name_width, verb_width, path_width = widths(routes).zip(header_lengths).map(&:max) routes.map do |r| "#{r[:name].rjust(name_width)} #{r[:verb].ljust(verb_width)} #{r[:path].ljust(path_width)} #{r[:reqs]}" @@ -193,9 +194,9 @@ module ActionDispatch end def widths(routes) - [routes.map { |r| r[:name].length }.max, - routes.map { |r| r[:verb].length }.max, - routes.map { |r| r[:path].length }.max] + [routes.map { |r| r[:name].length }.max || 0, + routes.map { |r| r[:verb].length }.max || 0, + routes.map { |r| r[:path].length }.max || 0] end end diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 288ce3e867..58c7f5330e 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -3,6 +3,7 @@ require 'active_support/core_ext/hash/reverse_merge' require 'active_support/core_ext/hash/slice' require 'active_support/core_ext/enumerable' require 'active_support/core_ext/array/extract_options' +require 'active_support/core_ext/module/remove_method' require 'active_support/inflector' require 'action_dispatch/routing/redirection' @@ -147,15 +148,19 @@ module ActionDispatch @defaults.merge!(options[:defaults]) if options[:defaults] options.each do |key, default| - next if Regexp === default || IGNORE_OPTIONS.include?(key) - @defaults[key] = default + unless Regexp === default || IGNORE_OPTIONS.include?(key) + @defaults[key] = default + end end if options[:constraints].is_a?(Hash) options[:constraints].each do |key, default| - next unless URL_OPTIONS.include?(key) && (String === default || Fixnum === default) - @defaults[key] ||= default + if URL_OPTIONS.include?(key) && (String === default || Fixnum === default) + @defaults[key] ||= default + end end + elsif options[:constraints] + verify_callable_constraint(options[:constraints]) end if Regexp === options[:format] @@ -165,20 +170,28 @@ module ActionDispatch end end + def verify_callable_constraint(callable_constraint) + unless callable_constraint.respond_to?(:call) || callable_constraint.respond_to?(:matches?) + raise ArgumentError, "Invalid constraint: #{callable_constraint.inspect} must respond to :call or :matches?" + end + end + def normalize_conditions! - @conditions.merge!(:path_info => path) + @conditions[:path_info] = path constraints.each do |key, condition| - next if segment_keys.include?(key) || key == :controller - @conditions[key] = condition + unless segment_keys.include?(key) || key == :controller + @conditions[key] = condition + end end - @conditions[:required_defaults] = [] + required_defaults = [] options.each do |key, required_default| - next if segment_keys.include?(key) || IGNORE_OPTIONS.include?(key) - next if Regexp === required_default - @conditions[:required_defaults] << key + unless segment_keys.include?(key) || IGNORE_OPTIONS.include?(key) || Regexp === required_default + required_defaults << key + end end + @conditions[:required_defaults] = required_defaults via_all = options.delete(:via) if options[:via] == :all @@ -192,8 +205,7 @@ module ActionDispatch end if via = options[:via] - list = Array(via).map { |m| m.to_s.dasherize.upcase } - @conditions.merge!(:request_method => list) + @conditions[:request_method] = Array(via).map { |m| m.to_s.dasherize.upcase } end end @@ -214,8 +226,12 @@ module ActionDispatch controller ||= default_controller action ||= default_action - unless controller.is_a?(Regexp) - controller = [@scope[:module], controller].compact.join("/").presence + if @scope[:module] && !controller.is_a?(Regexp) + if controller =~ %r{\A/} + controller = controller[1..-1] + else + controller = [@scope[:module], controller].compact.join("/").presence + end end if controller.is_a?(String) && controller =~ %r{\A/} @@ -226,11 +242,13 @@ module ActionDispatch action = action.to_s unless action.is_a?(Regexp) if controller.blank? && segment_keys.exclude?(:controller) - raise ArgumentError, "missing :controller" + message = "Missing :controller key on routes definition, please check your routes." + raise ArgumentError, message end if action.blank? && segment_keys.exclude?(:action) - raise ArgumentError, "missing :action" + message = "Missing :action key on routes definition, please check your routes." + raise ArgumentError, message end if controller.is_a?(String) && controller !~ /\A[a-z_0-9\/]*\z/ @@ -330,40 +348,61 @@ module ActionDispatch match '/', { :as => :root, :via => :get }.merge!(options) end - # Matches a url pattern to one or more routes. Any symbols in a pattern - # are interpreted as url query parameters and thus available as +params+ - # in an action: + # Matches a url pattern to one or more routes. + # + # You should not use the `match` method in your router + # without specifying an HTTP method. + # + # If you want to expose your action to both GET and POST, use: # # # sets :controller, :action and :id in params - # match ':controller/:action/:id' + # match ':controller/:action/:id', via: [:get, :post] + # + # Note that +:controller+, +:action+ and +:id+ are interpreted as url + # query parameters and thus available through +params+ in an action. + # + # If you want to expose your action to GET, use `get` in the router: + # + # Instead of: + # + # match ":controller/:action/:id" + # + # Do: + # + # get ":controller/:action/:id" # # Two of these symbols are special, +:controller+ maps to the controller # and +:action+ to the controller's action. A pattern can also map # wildcard segments (globs) to params: # - # match 'songs/*category/:title', to: 'songs#show' + # get 'songs/*category/:title', to: 'songs#show' # # # 'songs/rock/classic/stairway-to-heaven' sets # # params[:category] = 'rock/classic' # # params[:title] = 'stairway-to-heaven' # + # To match a wildcard parameter, it must have a name assigned to it. + # Without a variable name to attach the glob parameter to, the route + # can't be parsed. + # # When a pattern points to an internal route, the route's +:action+ and # +:controller+ should be set in options or hash shorthand. Examples: # - # match 'photos/:id' => 'photos#show' - # match 'photos/:id', to: 'photos#show' - # match 'photos/:id', controller: 'photos', action: 'show' + # match 'photos/:id' => 'photos#show', via: :get + # match 'photos/:id', to: 'photos#show', via: :get + # match 'photos/:id', controller: 'photos', action: 'show', via: :get # # A pattern can also point to a +Rack+ endpoint i.e. anything that # responds to +call+: # - # match 'photos/:id', to: lambda {|hash| [200, {}, ["Coming soon"]] } - # match 'photos/:id', to: PhotoRackApp + # match 'photos/:id', to: lambda {|hash| [200, {}, ["Coming soon"]] }, via: :get + # match 'photos/:id', to: PhotoRackApp, via: :get # # Yes, controller actions are just rack endpoints - # match 'photos/:id', to: PhotosController.action(:show) + # match 'photos/:id', to: PhotosController.action(:show), via: :get # - # Because request various HTTP verbs with a single action has security - # implications, is recommendable use HttpHelpers[rdoc-ref:HttpHelpers] + # Because requesting various HTTP verbs with a single action has security + # implications, you must either specify the actions in + # the via options or use one of the HtttpHelpers[rdoc-ref:HttpHelpers] # instead +match+ # # === Options @@ -382,8 +421,8 @@ module ActionDispatch # [:module] # The namespace for :controller. # - # match 'path', to: 'c#a', module: 'sekret', controller: 'posts' - # #=> Sekret::PostsController + # match 'path', to: 'c#a', module: 'sekret', controller: 'posts', via: :get + # # => Sekret::PostsController # # See <tt>Scoping#namespace</tt> for its scope equivalent. # @@ -401,9 +440,9 @@ module ActionDispatch # Points to a +Rack+ endpoint. Can be an object that responds to # +call+ or a string representing a controller's action. # - # match 'path', to: 'controller#action' - # match 'path', to: lambda { |env| [200, {}, ["Success!"]] } - # match 'path', to: RackApp + # match 'path', to: 'controller#action', via: :get + # match 'path', to: lambda { |env| [200, {}, ["Success!"]] }, via: :get + # match 'path', to: RackApp, via: :get # # [:on] # Shorthand for wrapping routes in a specific RESTful context. Valid @@ -428,14 +467,14 @@ module ActionDispatch # other than path can also be specified with any object # that responds to <tt>===</tt> (eg. String, Array, Range, etc.). # - # match 'path/:id', constraints: { id: /[A-Z]\d{5}/ } + # match 'path/:id', constraints: { id: /[A-Z]\d{5}/ }, via: :get # - # match 'json_only', constraints: { format: 'json' } + # match 'json_only', constraints: { format: 'json' }, via: :get # - # class Blacklist + # class Whitelist # def matches?(request) request.remote_ip == '1.2.3.4' end # end - # match 'path', to: 'c#a', constraints: Blacklist.new + # match 'path', to: 'c#a', constraints: Whitelist.new, via: :get # # See <tt>Scoping#constraints</tt> for more examples with its scope # equivalent. @@ -444,7 +483,7 @@ module ActionDispatch # Sets defaults for parameters # # # Sets params[:format] to 'jpg' by default - # match 'path', to: 'c#a', defaults: { format: 'jpg' } + # match 'path', to: 'c#a', defaults: { format: 'jpg' }, via: :get # # See <tt>Scoping#defaults</tt> for its scope equivalent. # @@ -453,7 +492,7 @@ module ActionDispatch # false, the pattern matches any request prefixed with the given path. # # # Matches any request starting with 'path' - # match 'path', to: 'c#a', anchor: false + # match 'path', to: 'c#a', anchor: false, via: :get # # [:format] # Allows you to specify the default value for optional +format+ @@ -496,11 +535,12 @@ module ActionDispatch raise "A rack application must be specified" unless path options[:as] ||= app_name(app) + target_as = name_for_action(options[:as], path) options[:via] ||= :all match(path, options.merge(:to => app, :anchor => false, :format => false)) - define_generate_prefix(app, options[:as]) + define_generate_prefix(app, target_as) self end @@ -538,18 +578,17 @@ module ActionDispatch _route = @set.named_routes.routes[name.to_sym] _routes = @set app.routes.define_mounted_helper(name) - app.routes.singleton_class.class_eval do - define_method :mounted? do - true - end - - define_method :_generate_prefix do |options| + app.routes.extend Module.new { + def mounted?; true; end + define_method :find_script_name do |options| + super(options) || begin prefix_options = options.slice(*_route.segment_keys) # we must actually delete prefix segment keys to avoid passing them to next url_for _route.segment_keys.each { |k| options.delete(k) } _routes.url_helpers.send("#{name}_path", prefix_options) + end end - end + } end end @@ -695,6 +734,11 @@ module ActionDispatch options[:path] = args.flatten.join('/') if args.any? options[:constraints] ||= {} + unless nested_scope? + options[:shallow_path] ||= options[:path] if options.key?(:path) + options[:shallow_prefix] ||= options[:as] if options.key?(:as) + end + if options[:constraints].is_a?(Hash) defaults = options[:constraints].select do |k, v| URL_OPTIONS.include?(k) && (v.is_a?(String) || v.is_a?(Fixnum)) @@ -776,9 +820,16 @@ module ActionDispatch # end def namespace(path, options = {}) path = path.to_s - options = { :path => path, :as => path, :module => path, - :shallow_path => path, :shallow_prefix => path }.merge!(options) - scope(options) { yield } + + defaults = { + module: path, + path: options.fetch(:path, path), + as: options.fetch(:as, path), + shallow_path: options.fetch(:path, path), + shallow_prefix: options.fetch(:as, path) + } + + scope(defaults.merge!(options)) { yield } end # === Parameter Restriction @@ -967,6 +1018,7 @@ module ActionDispatch @as = options[:as] @param = (options[:param] || :id).to_sym @options = options + @shallow = false end def default_actions @@ -1027,6 +1079,13 @@ module ActionDispatch "#{path}/:#{nested_param}" end + def shallow=(value) + @shallow = value + end + + def shallow? + @shallow + end end class SingletonResource < Resource #:nodoc: @@ -1066,18 +1125,18 @@ module ActionDispatch # a singular resource to map /profile (rather than /profile/:id) to # the show action: # - # resource :geocoder + # resource :profile # # creates six different routes in your application, all mapping to - # the +GeoCoders+ controller (note that the controller is named after + # the +Profiles+ controller (note that the controller is named after # the plural): # - # GET /geocoder/new - # POST /geocoder - # GET /geocoder - # GET /geocoder/edit - # PATCH/PUT /geocoder - # DELETE /geocoder + # GET /profile/new + # POST /profile + # GET /profile + # GET /profile/edit + # PATCH/PUT /profile + # DELETE /profile # # === Options # Takes same options as +resources+. @@ -1307,8 +1366,10 @@ module ActionDispatch end with_scope_level(:member) do - scope(parent_resource.member_scope) do - yield + if shallow? + shallow_scope(parent_resource.member_scope) { yield } + else + scope(parent_resource.member_scope) { yield } end end end @@ -1331,16 +1392,8 @@ module ActionDispatch end with_scope_level(:nested) do - if shallow? - with_exclusive_scope do - if @scope[:shallow_path].blank? - scope(parent_resource.nested_scope, nested_options) { yield } - else - scope(@scope[:shallow_path], :as => @scope[:shallow_prefix]) do - scope(parent_resource.nested_scope, nested_options) { yield } - end - end - end + if shallow? && shallow_nesting_depth > 1 + shallow_scope(parent_resource.nested_scope, nested_options) { yield } else scope(parent_resource.nested_scope, nested_options) { yield } end @@ -1357,7 +1410,7 @@ module ActionDispatch end def shallow - scope(:shallow => true, :shallow_path => @scope[:path]) do + scope(:shallow => true) do yield end end @@ -1398,6 +1451,7 @@ module ActionDispatch path_without_format = _path.to_s.sub(/\(\.:format\)$/, '') if using_match_shorthand?(path_without_format, route_options) route_options[:to] ||= path_without_format.gsub(%r{^/}, "").sub(%r{/([^/]*)$}, '#\1') + route_options[:to].tr!("-", "_") end decomposed_match(_path, route_options) @@ -1428,8 +1482,8 @@ module ActionDispatch path = path_for_action(action, options.delete(:path)) action = action.to_s.dup - if action =~ /^[\w\/]+$/ - options[:action] ||= action unless action.include?("/") + if action =~ /^[\w\-\/]+$/ + options[:action] ||= action.tr('-', '_') unless action.include?("/") else action = nil end @@ -1477,6 +1531,13 @@ module ActionDispatch return true end + if options.delete(:shallow) + shallow do + send(method, resources.pop, options, &block) + end + return true + end + if resource_scope? nested { send(method, resources.pop, options, &block) } return true @@ -1521,6 +1582,10 @@ module ActionDispatch RESOURCE_METHOD_SCOPES.include? @scope[:scope_level] end + def nested_scope? #:nodoc: + @scope[:scope_level] == :nested + end + def with_exclusive_scope begin old_name_prefix, old_path = @scope[:as], @scope[:path] @@ -1534,21 +1599,24 @@ module ActionDispatch end end - def with_scope_level(kind, resource = parent_resource) + def with_scope_level(kind) old, @scope[:scope_level] = @scope[:scope_level], kind - old_resource, @scope[:scope_level_resource] = @scope[:scope_level_resource], resource yield ensure @scope[:scope_level] = old - @scope[:scope_level_resource] = old_resource end def resource_scope(kind, resource) #:nodoc: - with_scope_level(kind, resource) do - scope(parent_resource.resource_scope) do - yield - end + resource.shallow = @scope[:shallow] + old_resource, @scope[:scope_level_resource] = @scope[:scope_level_resource], resource + @nesting.push(resource) + + with_scope_level(kind) do + scope(parent_resource.resource_scope) { yield } end + ensure + @nesting.pop + @scope[:scope_level_resource] = old_resource end def nested_options #:nodoc: @@ -1560,6 +1628,14 @@ module ActionDispatch options end + def nesting_depth #:nodoc: + @nesting.size + end + + def shallow_nesting_depth #:nodoc: + @nesting.select(&:shallow?).size + end + def param_constraint? #:nodoc: @scope[:constraints] && @scope[:constraints][parent_resource.param].is_a?(Regexp) end @@ -1572,18 +1648,20 @@ module ActionDispatch flag && resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s) end - def shallow_scoping? #:nodoc: - shallow? && @scope[:scope_level] == :member + def shallow_scope(path, options = {}) #:nodoc: + old_name_prefix, old_path = @scope[:as], @scope[:path] + @scope[:as], @scope[:path] = @scope[:shallow_prefix], @scope[:shallow_path] + + scope(path, options) { yield } + ensure + @scope[:as], @scope[:path] = old_name_prefix, old_path end def path_for_action(action, path) #:nodoc: - prefix = shallow_scoping? ? - "#{@scope[:shallow_path]}/#{parent_resource.shallow_scope}" : @scope[:path] - if canonical_action?(action, path.blank?) - prefix.to_s + @scope[:path].to_s else - "#{prefix}/#{action_path(action, path)}" + "#{@scope[:path]}/#{action_path(action, path)}" end end @@ -1594,10 +1672,11 @@ module ActionDispatch def prefix_name_for_action(as, action) #:nodoc: if as - as.to_s + prefix = as elsif !canonical_action?(action, @scope[:scope_level]) - action.to_s + prefix = action end + prefix.to_s.tr('-', '_') if prefix end def name_for_action(as, action) #:nodoc: @@ -1620,7 +1699,7 @@ module ActionDispatch when :new [prefix, :new, name_prefix, member_name] when :member - [prefix, shallow_scoping? ? @scope[:shallow_prefix] : name_prefix, member_name] + [prefix, name_prefix, member_name] when :root [name_prefix, collection_name, prefix] else @@ -1761,6 +1840,7 @@ module ActionDispatch @set = set @scope = { :path_names => @set.resources_path_names } @concerns = {} + @nesting = [] end include Base diff --git a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb index 6d3f8da932..bd3696cda1 100644 --- a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb +++ b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb @@ -74,6 +74,19 @@ module ActionDispatch # * <tt>:routing_type</tt> - Allowed values are <tt>:path</tt> or <tt>:url</tt>. # Default is <tt>:url</tt>. # + # Also includes all the options from <tt>url_for</tt>. These include such + # things as <tt>:anchor</tt> or <tt>:trailing_slash</tt>. Example usage + # is given below: + # + # polymorphic_url([blog, post], anchor: 'my_anchor') + # # => "http://example.com/blogs/1/posts/1#my_anchor" + # polymorphic_url([blog, post], anchor: 'my_anchor', script_name: "/my_app") + # # => "http://example.com/my_app/blogs/1/posts/1#my_anchor" + # + # For all of these options, see the documentation for <tt>url_for</tt>. + # + # ==== Functionality + # # # an Article record # polymorphic_url(record) # same as article_url(record) # @@ -88,53 +101,45 @@ module ActionDispatch # polymorphic_url(Comment) # same as comments_url() # def polymorphic_url(record_or_hash_or_array, options = {}) - if record_or_hash_or_array.kind_of?(Array) - record_or_hash_or_array = record_or_hash_or_array.compact - if record_or_hash_or_array.first.is_a?(ActionDispatch::Routing::RoutesProxy) - proxy = record_or_hash_or_array.shift - end - record_or_hash_or_array = record_or_hash_or_array[0] if record_or_hash_or_array.size == 1 + if Hash === record_or_hash_or_array + options = record_or_hash_or_array.merge(options) + record = options.delete :id + return polymorphic_url record, options end - record = extract_record(record_or_hash_or_array) - record = convert_to_model(record) + opts = options.dup + action = opts.delete :action + type = opts.delete(:routing_type) || :url - args = Array === record_or_hash_or_array ? - record_or_hash_or_array.dup : - [ record_or_hash_or_array ] + HelperMethodBuilder.polymorphic_method self, + record_or_hash_or_array, + action, + type, + opts - inflection = if options[:action] && options[:action].to_s == "new" - args.pop - :singular - elsif (record.respond_to?(:persisted?) && !record.persisted?) - args.pop - :plural - elsif record.is_a?(Class) - args.pop - :plural - else - :singular - end - - args.delete_if {|arg| arg.is_a?(Symbol) || arg.is_a?(String)} - named_route = build_named_route_call(record_or_hash_or_array, inflection, options) - - url_options = options.except(:action, :routing_type) - unless url_options.empty? - args.last.kind_of?(Hash) ? args.last.merge!(url_options) : args << url_options - end - - args.collect! { |a| convert_to_model(a) } - - (proxy || self).send(named_route, *args) end # Returns the path component of a URL for the given record. It uses # <tt>polymorphic_url</tt> with <tt>routing_type: :path</tt>. def polymorphic_path(record_or_hash_or_array, options = {}) - polymorphic_url(record_or_hash_or_array, options.merge(:routing_type => :path)) + if Hash === record_or_hash_or_array + options = record_or_hash_or_array.merge(options) + record = options.delete :id + return polymorphic_path record, options + end + + opts = options.dup + action = opts.delete :action + type = :path + + HelperMethodBuilder.polymorphic_method self, + record_or_hash_or_array, + action, + type, + opts end + %w(edit new).each do |action| module_eval <<-EOT, __FILE__, __LINE__ + 1 def #{action}_polymorphic_url(record_or_hash, options = {}) # def edit_polymorphic_url(record_or_hash, options = {}) @@ -152,54 +157,169 @@ module ActionDispatch end private - def action_prefix(options) - options[:action] ? "#{options[:action]}_" : '' + + class HelperMethodBuilder # :nodoc: + CACHE = { 'path' => {}, 'url' => {} } + + def self.get(action, type) + type = type.to_s + CACHE[type].fetch(action) { build action, type } end - def routing_type(options) - options[:routing_type] || :url + def self.url; CACHE['url'.freeze][nil]; end + def self.path; CACHE['path'.freeze][nil]; end + + def self.build(action, type) + prefix = action ? "#{action}_" : "" + suffix = type + if action.to_s == 'new' + HelperMethodBuilder.singular prefix, suffix + else + HelperMethodBuilder.plural prefix, suffix + end + end + + def self.singular(prefix, suffix) + new(->(name) { name.singular_route_key }, prefix, suffix) end - def build_named_route_call(records, inflection, options = {}) - if records.is_a?(Array) - record = records.pop - route = records.map do |parent| - if parent.is_a?(Symbol) || parent.is_a?(String) - parent - else - model_name_from_record_or_class(parent).singular_route_key - end + def self.plural(prefix, suffix) + new(->(name) { name.route_key }, prefix, suffix) + end + + def self.polymorphic_method(recipient, record_or_hash_or_array, action, type, options) + builder = get action, type + + case record_or_hash_or_array + when Array + if record_or_hash_or_array.empty? || record_or_hash_or_array.include?(nil) + raise ArgumentError, "Nil location provided. Can't build URI." end + if record_or_hash_or_array.first.is_a?(ActionDispatch::Routing::RoutesProxy) + recipient = record_or_hash_or_array.shift + end + + method, args = builder.handle_list record_or_hash_or_array + when String, Symbol + method, args = builder.handle_string record_or_hash_or_array + when Class + method, args = builder.handle_class record_or_hash_or_array + + when nil + raise ArgumentError, "Nil location provided. Can't build URI." + else + method, args = builder.handle_model record_or_hash_or_array + end + + + if options.empty? + recipient.send(method, *args) else - record = extract_record(records) - route = [] + recipient.send(method, *args, options) end + end + + attr_reader :suffix, :prefix + + def initialize(key_strategy, prefix, suffix) + @key_strategy = key_strategy + @prefix = prefix + @suffix = suffix + end + + def handle_string(record) + [get_method_for_string(record), []] + end + + def handle_string_call(target, str) + target.send get_method_for_string str + end + + def handle_class(klass) + [get_method_for_class(klass), []] + end + + def handle_class_call(target, klass) + target.send get_method_for_class klass + end + + def handle_model(record) + args = [] + + model = record.to_model + name = if record.persisted? + args << model + model.class.model_name.singular_route_key + else + @key_strategy.call model.class.model_name + end + + named_route = prefix + "#{name}_#{suffix}" + + [named_route, args] + end - if record.is_a?(Symbol) || record.is_a?(String) - route << record - elsif record - if inflection == :singular - route << model_name_from_record_or_class(record).singular_route_key + def handle_model_call(target, model) + method, args = handle_model model + target.send(method, *args) + end + + def handle_list(list) + record_list = list.dup + record = record_list.pop + + args = [] + + route = record_list.map { |parent| + case parent + when Symbol, String + parent.to_s + when Class + args << parent + parent.model_name.singular_route_key else - route << model_name_from_record_or_class(record).route_key + args << parent.to_model + parent.to_model.class.model_name.singular_route_key end + } + + route << + case record + when Symbol, String + record.to_s + when Class + @key_strategy.call record.model_name else - raise ArgumentError, "Nil location provided. Can't build URI." + if record.persisted? + args << record.to_model + record.to_model.class.model_name.singular_route_key + else + @key_strategy.call record.to_model.class.model_name + end end - route << routing_type(options) + route << suffix - action_prefix(options) + route.join("_") + named_route = prefix + route.join("_") + [named_route, args] end - def extract_record(record_or_hash_or_array) - case record_or_hash_or_array - when Array; record_or_hash_or_array.last - when Hash; record_or_hash_or_array[:id] - else record_or_hash_or_array - end + private + + def get_method_for_class(klass) + name = @key_strategy.call klass.model_name + prefix + "#{name}_#{suffix}" + end + + def get_method_for_string(str) + prefix + "#{str}_#{suffix}" end + + [nil, 'new', 'edit'].each do |action| + CACHE['url'][action] = build action, 'url' + CACHE['path'][action] = build action, 'path' + end + end end end end - diff --git a/actionpack/lib/action_dispatch/routing/redirection.rb b/actionpack/lib/action_dispatch/routing/redirection.rb index d751e04e6a..b08e62543b 100644 --- a/actionpack/lib/action_dispatch/routing/redirection.rb +++ b/actionpack/lib/action_dispatch/routing/redirection.rb @@ -17,7 +17,7 @@ module ActionDispatch def call(env) req = Request.new(env) - # If any of the path parameters has a invalid encoding then + # If any of the path parameters has an invalid encoding then # raise since it's likely to trigger errors further on. req.symbolized_path_parameters.each do |key, value| unless value.valid_encoding? @@ -26,6 +26,15 @@ module ActionDispatch end uri = URI.parse(path(req.symbolized_path_parameters, req)) + + unless uri.host + if relative_path?(uri.path) + uri.path = "#{req.script_name}/#{uri.path}" + elsif uri.path.empty? + uri.path = req.script_name.empty? ? "/" : req.script_name + end + end + uri.scheme ||= req.scheme uri.host ||= req.host uri.port ||= req.port unless req.standard_port? @@ -48,11 +57,38 @@ module ActionDispatch def inspect "redirect(#{status})" end + + private + def relative_path?(path) + path && !path.empty? && path[0] != '/' + end + + def escape(params) + Hash[params.map{ |k,v| [k, Rack::Utils.escape(v)] }] + end + + def escape_fragment(params) + Hash[params.map{ |k,v| [k, Journey::Router::Utils.escape_fragment(v)] }] + end + + def escape_path(params) + Hash[params.map{ |k,v| [k, Journey::Router::Utils.escape_path(v)] }] + end end class PathRedirect < Redirect + URL_PARTS = /\A([^?]+)?(\?[^#]+)?(#.+)?\z/ + def path(params, request) - (params.empty? || !block.match(/%\{\w*\}/)) ? block : (block % escape(params)) + if block.match(URL_PARTS) + path = interpolation_required?($1, params) ? $1 % escape_path(params) : $1 + query = interpolation_required?($2, params) ? $2 % escape(params) : $2 + fragment = interpolation_required?($3, params) ? $3 % escape_fragment(params) : $3 + + "#{path}#{query}#{fragment}" + else + interpolation_required?(block, params) ? block % escape(params) : block + end end def inspect @@ -60,8 +96,8 @@ module ActionDispatch end private - def escape(params) - Hash[params.map{ |k,v| [k, Rack::Utils.escape(v)] }] + def interpolation_required?(string, params) + !params.empty? && string && string.match(/%\{\w*\}/) end end @@ -81,17 +117,22 @@ module ActionDispatch url_options[:path] = (url_options[:path] % escape_path(params)) end + unless options[:host] || options[:domain] + if relative_path?(url_options[:path]) + url_options[:path] = "/#{url_options[:path]}" + url_options[:script_name] = request.script_name + elsif url_options[:path].empty? + url_options[:path] = request.script_name.empty? ? "/" : "" + url_options[:script_name] = request.script_name + end + end + ActionDispatch::Http::URL.url_for url_options end def inspect "redirect(#{status}, #{options.map{ |k,v| "#{k}: #{v}" }.join(', ')})" end - - private - def escape_path(params) - Hash[params.map{ |k,v| [k, URI.parser.escape(v)] }] - end end module Redirection @@ -104,6 +145,10 @@ module ActionDispatch # # get 'docs/:article', to: redirect('/wiki/%{article}') # + # Note that if you return a path without a leading slash then the url is prefixed with the + # current SCRIPT_NAME environment variable. This is typically '/' but may be different in + # a mounted engine or where the application is deployed to a subdirectory of a website. + # # Alternatively you can use one of the other syntaxes: # # The block version of redirect allows for the easy encapsulation of any logic associated with diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 3ae9f92c0b..e699419f23 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -1,11 +1,13 @@ require 'action_dispatch/journey' require 'forwardable' require 'thread_safe' +require 'active_support/concern' require 'active_support/core_ext/object/to_query' require 'active_support/core_ext/hash/slice' require 'active_support/core_ext/module/remove_method' require 'active_support/core_ext/array/extract_options' require 'action_controller/metal/exceptions' +require 'action_dispatch/http/request' module ActionDispatch module Routing @@ -28,7 +30,7 @@ module ActionDispatch def call(env) params = env[PARAMETERS_KEY] - # If any of the path parameters has a invalid encoding then + # If any of the path parameters has an invalid encoding then # raise since it's likely to trigger errors further on. params.each do |key, value| next unless value.respond_to?(:valid_encoding?) @@ -155,7 +157,7 @@ module ActionDispatch end def self.optimize_helper?(route) - route.requirements.except(:controller, :action).empty? + !route.glob? && route.path.requirements.empty? end class OptimizedUrlHelper < UrlHelper # :nodoc: @@ -163,15 +165,15 @@ module ActionDispatch def initialize(route, options) super - @path_parts = @route.required_parts - @arg_size = @path_parts.size - @string_route = @route.optimized_path + @klass = Journey::Router::Utils + @required_parts = @route.required_parts + @arg_size = @required_parts.size + @optimized_path = @route.optimized_path end def call(t, args) if args.size == arg_size && !args.last.is_a?(Hash) && optimize_routes_generation?(t) - options = @options.dup - options.merge!(t.url_options) if t.respond_to?(:url_options) + options = t.url_options.merge @options options[:path] = optimized_helper(args) ActionDispatch::Http::URL.url_for(options) else @@ -182,41 +184,60 @@ module ActionDispatch private def optimized_helper(args) - path = @string_route.dup - klass = Journey::Router::Utils - - @path_parts.zip(args) do |part, arg| - # Replace each route parameter - # e.g. :id for regular parameter or *path for globbing - # with ruby string interpolation code - path.gsub!(/(\*|:)#{part}/, klass.escape_fragment(arg.to_param)) + params = Hash[parameterize_args(args)] + missing_keys = missing_keys(params) + + unless missing_keys.empty? + raise_generation_error(params, missing_keys) end - path + + @optimized_path.map{ |segment| replace_segment(params, segment) }.join + end + + def replace_segment(params, segment) + Symbol === segment ? @klass.escape_segment(params[segment]) : segment end def optimize_routes_generation?(t) t.send(:optimize_routes_generation?) end + + def parameterize_args(args) + @required_parts.zip(args.map(&:to_param)) + end + + def missing_keys(args) + args.select{ |part, arg| arg.nil? || arg.empty? }.keys + end + + def raise_generation_error(args, missing_keys) + constraints = Hash[@route.requirements.merge(args).sort] + message = "No route matches #{constraints.inspect}" + message << " missing required keys: #{missing_keys.sort.inspect}" + + raise ActionController::UrlGenerationError, message + end end def initialize(route, options) @options = options - @segment_keys = route.segment_keys + @segment_keys = route.segment_keys.uniq @route = route end def call(t, args) - t.url_for(handle_positional_args(t, args, @options, @segment_keys)) + options = t.url_options.merge @options + hash = handle_positional_args(t, args, options, @segment_keys) + t._routes.url_for(hash) end - def handle_positional_args(t, args, options, keys) + def handle_positional_args(t, args, result, keys) inner_options = args.extract_options! - result = options.dup if args.size > 0 if args.size < keys.size - 1 # take format into account - keys -= t.url_options.keys if t.respond_to?(:url_options) - keys -= options.keys + keys -= t.url_options.keys + keys -= result.keys end keys -= inner_options.keys result.merge!(Hash[keys.zip(args)]) @@ -336,7 +357,7 @@ module ActionDispatch include UrlFor end - # Contains all the mounted helpers accross different + # Contains all the mounted helpers across 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. @@ -374,6 +395,8 @@ module ActionDispatch @_routes = routes class << self delegate :url_for, :optimize_routes_generation?, :to => '@_routes' + attr_reader :_routes + def url_options; {}; end end # Make named_routes available in the module singleton @@ -489,11 +512,12 @@ module ActionDispatch @recall = recall.dup @set = set + normalize_recall! normalize_options! normalize_controller_action_id! use_relative_controller! normalize_controller! - handle_nil_action! + normalize_action! end def controller @@ -512,6 +536,11 @@ module ActionDispatch end end + # Set 'index' as default action for recall + def normalize_recall! + @recall[:action] ||= 'index' + end + def normalize_options! # If an explicit :controller was given, always make :action explicit # too, so that action expiry works as expected for things like @@ -527,8 +556,8 @@ module ActionDispatch options[:controller] = options[:controller].to_s end - if options[:action] - options[:action] = options[:action].to_s + if options.key?(:action) + options[:action] = (options[:action] || 'index').to_s end end @@ -538,8 +567,6 @@ module ActionDispatch # :controller, :action or :id is not found, don't pull any # more keys from the recall. def normalize_controller_action_id! - @recall[:action] ||= 'index' if current_controller - use_recall_for(:controller) or return use_recall_for(:action) or return use_recall_for(:id) @@ -561,13 +588,11 @@ module ActionDispatch @options[:controller] = controller.sub(%r{^/}, '') if controller end - # This handles the case of action: nil being explicitly passed. - # It is identical to action: "index" - def handle_nil_action! - if options.has_key?(:action) && options[:action].nil? - options[:action] = 'index' + # Move 'index' action from options to recall + def normalize_action! + if @options[:action] == 'index' + @recall[:action] = @options.delete(:action) end - recall[:action] = options.delete(:action) if options[:action] == 'index' end # Generates a path from routes, returns [path, params]. @@ -618,28 +643,34 @@ module ActionDispatch !mounted? && default_url_options.empty? end - def _generate_prefix(options = {}) - nil + def find_script_name(options) + options.delete :script_name end - # The +options+ argument must be +nil+ or a hash whose keys are *symbols*. + # The +options+ argument must be a hash whose keys are *symbols*. def url_for(options) - options = default_url_options.merge(options || {}) + options = default_url_options.merge options + + user = password = nil + + if options[:user] && options[:password] + user = options.delete :user + password = options.delete :password + end - user, password = extract_authentication(options) - recall = options.delete(:_recall) + recall = options.delete(:_recall) { {} } - original_script_name = options.delete(:original_script_name).presence - script_name = options.delete(:script_name).presence || _generate_prefix(options) + original_script_name = options.delete(:original_script_name) + script_name = find_script_name options if script_name && original_script_name script_name = original_script_name + script_name end - path_options = options.except(*RESERVED_OPTIONS) - path_options = yield(path_options) if block_given? + path_options = options.dup + RESERVED_OPTIONS.each { |ro| path_options.delete ro } - path, params = generate(path_options, recall || {}) + path, params = generate(path_options, recall) params.merge!(options[:params] || {}) ActionDispatch::Http::URL.url_for(options.merge!({ @@ -694,17 +725,6 @@ module ActionDispatch raise ActionController::RoutingError, "No route matches #{path.inspect}" end - - private - - def extract_authentication(options) - if options[:user] && options[:password] - [options.delete(:user), options.delete(:password)] - else - nil - end - end - end end end diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb index 8e19025722..e624fe3c4a 100644 --- a/actionpack/lib/action_dispatch/routing/url_for.rb +++ b/actionpack/lib/action_dispatch/routing/url_for.rb @@ -20,7 +20,7 @@ module ActionDispatch # # <%= link_to('Click here', controller: 'users', # action: 'new', message: 'Welcome!') %> - # # => "/users/new?message=Welcome%21" + # # => <a href="/users/new?message=Welcome%21">Click here</a> # # link_to, and all other functions that require URL generation functionality, # actually use ActionController::UrlFor under the hood. And in particular, @@ -155,8 +155,14 @@ module ActionDispatch _routes.url_for(options.symbolize_keys.reverse_merge!(url_options)) when String options + when Symbol + HelperMethodBuilder.url.handle_string_call self, options + when Array + polymorphic_url(options, options.extract_options!) + when Class + HelperMethodBuilder.url.handle_class_call self, options else - polymorphic_url(options) + HelperMethodBuilder.url.handle_model_call self, options end end diff --git a/actionpack/lib/action_dispatch/testing/assertions/dom.rb b/actionpack/lib/action_dispatch/testing/assertions/dom.rb index 8f90a1223e..241a39393a 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/dom.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/dom.rb @@ -7,20 +7,20 @@ module ActionDispatch # # # assert that the referenced method generates the appropriate HTML string # assert_dom_equal '<a href="http://www.example.com">Apples</a>', link_to("Apples", "http://www.example.com") - def assert_dom_equal(expected, actual, message = "") + def assert_dom_equal(expected, actual, message = nil) expected_dom = HTML::Document.new(expected).root actual_dom = HTML::Document.new(actual).root - assert_equal expected_dom, actual_dom + assert_equal expected_dom, actual_dom, message end # The negated form of +assert_dom_equivalent+. # # # assert that the referenced method does not generate the specified HTML string # assert_dom_not_equal '<a href="http://www.example.com">Apples</a>', link_to("Oranges", "http://www.example.com") - def assert_dom_not_equal(expected, actual, message = "") + def assert_dom_not_equal(expected, actual, message = nil) expected_dom = HTML::Document.new(expected).root actual_dom = HTML::Document.new(actual).root - assert_not_equal expected_dom, actual_dom + assert_not_equal expected_dom, actual_dom, message end end end diff --git a/actionpack/lib/action_dispatch/testing/assertions/response.rb b/actionpack/lib/action_dispatch/testing/assertions/response.rb index 44ed0ac1f3..0adc6c84ff 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/response.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/response.rb @@ -27,6 +27,9 @@ module ActionDispatch assert @response.send("#{type}?"), message else code = Rack::Utils::SYMBOL_TO_STATUS_CODE[type] + if code.nil? + raise ArgumentError, "Invalid response type :#{type}" + end assert_equal code, @response.response_code, message end else @@ -67,21 +70,17 @@ module ActionDispatch end def normalize_argument_to_redirection(fragment) - normalized = case fragment - when Regexp - fragment - when %r{^\w[A-Za-z\d+.-]*:.*} - fragment - when String - @request.protocol + @request.host_with_port + fragment - when :back - raise RedirectBackError unless refer = @request.headers["Referer"] - refer - else - @controller.url_for(fragment) - end - - normalized.respond_to?(:delete) ? normalized.delete("\0\r\n") : normalized + if Regexp === fragment + fragment + else + handle = @controller || Class.new(ActionController::Metal) do + include ActionController::Redirecting + def initialize(request) + @_request = request + end + end.new(@request) + handle._compute_redirect_to_location(fragment) + 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 496682e8bd..f1f998d932 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/routing.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/routing.rb @@ -211,7 +211,7 @@ module ActionDispatch def fail_on(exception_class) yield rescue exception_class => e - raise MiniTest::Assertion, e.message + raise Minitest::Assertion, e.message end end end diff --git a/actionpack/lib/action_dispatch/testing/assertions/selector.rb b/actionpack/lib/action_dispatch/testing/assertions/selector.rb index 3253a3d424..12023e6f77 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/selector.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/selector.rb @@ -267,7 +267,7 @@ module ActionDispatch text.strip! unless NO_STRIP.include?(match.name) text.sub!(/\A\n/, '') if match.name == "textarea" unless match_with.is_a?(Regexp) ? (text =~ match_with) : (text == match_with.to_s) - content_mismatch ||= sprintf("<%s> expected but was\n<%s>.", match_with, text) + content_mismatch ||= sprintf("<%s> expected but was\n<%s>", match_with, text) true end end @@ -276,7 +276,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 ||= sprintf("<%s> expected but was\n<%s>.", match_with, html) + content_mismatch ||= sprintf("<%s> expected but was\n<%s>", match_with, html) true end end @@ -289,9 +289,9 @@ module ActionDispatch # 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}.) + message = message || %(Expected #{count_description(min, max, count)} matching "#{selector.to_s}", found #{matches.size}) if count - assert_equal matches.size, count, message + assert_equal count, matches.size, message else assert_operator matches.size, :>=, min, message if min assert_operator matches.size, :<=, max, message if max diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index 9beb30307b..cc6b763093 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -137,7 +137,7 @@ module ActionDispatch class Session DEFAULT_HOST = "www.example.com" - include MiniTest::Assertions + include Minitest::Assertions include TestProcess, RequestHelpers, Assertions %w( status status_message headers body redirect? ).each do |method| @@ -242,7 +242,7 @@ module ActionDispatch @https = flag end - # Return +true+ if the session is mimicking a secure HTTPS request. + # Returns +true+ if the session is mimicking a secure HTTPS request. # # if session.https? # ... diff --git a/actionpack/lib/action_dispatch/testing/test_request.rb b/actionpack/lib/action_dispatch/testing/test_request.rb index c63778f870..57c678843b 100644 --- a/actionpack/lib/action_dispatch/testing/test_request.rb +++ b/actionpack/lib/action_dispatch/testing/test_request.rb @@ -3,7 +3,11 @@ require 'rack/utils' module ActionDispatch class TestRequest < Request - DEFAULT_ENV = Rack::MockRequest.env_for('/') + DEFAULT_ENV = Rack::MockRequest.env_for('/', + 'HTTP_HOST' => 'test.host', + 'REMOTE_ADDR' => '0.0.0.0', + 'HTTP_USER_AGENT' => 'Rails Testing' + ) def self.new(env = {}) super @@ -12,10 +16,6 @@ module ActionDispatch def initialize(env = {}) env = Rails.application.env_config.merge(env) if defined?(Rails.application) && Rails.application super(default_env.merge(env)) - - self.host = 'test.host' - self.remote_addr = '0.0.0.0' - self.user_agent = 'Rails Testing' end def request_method=(method) diff --git a/actionpack/lib/action_pack.rb b/actionpack/lib/action_pack.rb index ad5acd8080..77f656d6f1 100644 --- a/actionpack/lib/action_pack.rb +++ b/actionpack/lib/action_pack.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2013 David Heinemeier Hansson +# Copyright (c) 2004-2014 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/gem_version.rb b/actionpack/lib/action_pack/gem_version.rb new file mode 100644 index 0000000000..beaf35d3da --- /dev/null +++ b/actionpack/lib/action_pack/gem_version.rb @@ -0,0 +1,15 @@ +module ActionPack + # Returns the version of the currently loaded ActionPack as a <tt>Gem::Version</tt> + def self.gem_version + Gem::Version.new VERSION::STRING + end + + module VERSION + MAJOR = 4 + MINOR = 2 + TINY = 0 + PRE = "alpha" + + STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") + end +end diff --git a/actionpack/lib/action_pack/version.rb b/actionpack/lib/action_pack/version.rb index fd08f392aa..7088cd2760 100644 --- a/actionpack/lib/action_pack/version.rb +++ b/actionpack/lib/action_pack/version.rb @@ -1,11 +1,8 @@ +require_relative 'gem_version' + module ActionPack - # Returns the version of the currently loaded ActionPack as a Gem::Version + # Returns the version of the currently loaded ActionPack as a <tt>Gem::Version</tt> def self.version - Gem::Version.new "4.1.0.beta" - end - - module VERSION #:nodoc: - MAJOR, MINOR, TINY, PRE = ActionPack.version.segments - STRING = ActionPack.version.to_s + gem_version end end |