diff options
Diffstat (limited to 'actionpack/lib')
61 files changed, 856 insertions, 362 deletions
diff --git a/actionpack/lib/abstract_controller/base.rb b/actionpack/lib/abstract_controller/base.rb index 4501202b8c..8edea0f52b 100644 --- a/actionpack/lib/abstract_controller/base.rb +++ b/actionpack/lib/abstract_controller/base.rb @@ -49,7 +49,7 @@ module AbstractController # instance methods on that abstract class. Public instance methods of # a controller would normally be considered action methods, so methods # declared on abstract classes are being removed. - # (ActionController::Metal and ActionController::Base are defined as abstract) + # (<tt>ActionController::Metal</tt> and ActionController::Base are defined as abstract) def internal_methods controller = self @@ -80,7 +80,7 @@ module AbstractController # action_methods are cached and there is sometimes need to refresh # them. ::clear_action_methods! allows you to do that, so next time - # you run action_methods, they will be recalculated + # you run action_methods, they will be recalculated. def clear_action_methods! @action_methods = nil end @@ -149,7 +149,7 @@ module AbstractController # ==== Parameters # * <tt>action_name</tt> - The name of an action to be tested def available_action?(action_name) - _find_action_name(action_name).present? + _find_action_name(action_name) end # Returns true if the given controller is capable of rendering diff --git a/actionpack/lib/abstract_controller/callbacks.rb b/actionpack/lib/abstract_controller/callbacks.rb index 13795f0dd8..d63ce9c1c3 100644 --- a/actionpack/lib/abstract_controller/callbacks.rb +++ b/actionpack/lib/abstract_controller/callbacks.rb @@ -39,8 +39,8 @@ module AbstractController # except: :index, if: -> { true } # the :except option will be ignored. # # ==== Options - # * <tt>only</tt> - The callback should be run only for this action - # * <tt>except</tt> - The callback should be run for all actions except this action + # * <tt>only</tt> - The callback should be run only for this action. + # * <tt>except</tt> - The callback should be run for all actions except this action. def _normalize_callback_options(options) _normalize_callback_option(options, :only, :if) _normalize_callback_option(options, :except, :unless) @@ -48,7 +48,8 @@ module AbstractController def _normalize_callback_option(options, from, to) # :nodoc: if from = options[from] - from = Array(from).map {|o| "action_name == '#{o}'"}.join(" || ") + _from = Array(from).map(&:to_s).to_set + from = proc {|c| _from.include? c.action_name } options[to] = Array(options[to]).unshift(from) end end @@ -59,9 +60,9 @@ module AbstractController # * <tt>names</tt> - A list of valid names that could be used for # callbacks. Note that skipping uses Ruby equality, so it's # impossible to skip a callback defined using an anonymous proc - # using #skip_action_callback + # using #skip_action_callback. def skip_action_callback(*names) - ActiveSupport::Deprecation.warn('`skip_action_callback` is deprecated and will be removed in the next major version of Rails. Please use skip_before_action, skip_after_action or skip_around_action instead.') + ActiveSupport::Deprecation.warn('`skip_action_callback` is deprecated and will be removed in Rails 5.1. Please use skip_before_action, skip_after_action or skip_around_action instead.') skip_before_action(*names, raise: false) skip_after_action(*names, raise: false) skip_around_action(*names, raise: false) @@ -82,8 +83,8 @@ module AbstractController # * <tt>block</tt> - A proc that should be added to the callbacks. # # ==== Block Parameters - # * <tt>name</tt> - The callback to be added - # * <tt>options</tt> - A hash of options to be used when adding the callback + # * <tt>name</tt> - The callback to be added. + # * <tt>options</tt> - A hash of options to be used when adding the callback. def _insert_callbacks(callbacks, block = nil) options = callbacks.extract_options! _normalize_callback_options(options) diff --git a/actionpack/lib/abstract_controller/rendering.rb b/actionpack/lib/abstract_controller/rendering.rb index a73f188623..e765d73ce4 100644 --- a/actionpack/lib/abstract_controller/rendering.rb +++ b/actionpack/lib/abstract_controller/rendering.rb @@ -82,7 +82,13 @@ module AbstractController # <tt>render :file => "foo/bar"</tt>. # :api: plugin def _normalize_args(action=nil, options={}) - if action.is_a? Hash + if action.respond_to?(:permitted?) + if action.permitted? + action + else + raise ArgumentError, "render parameters are not permitted" + end + elsif action.is_a?(Hash) action else options diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb index 3d3af555c9..40f33a9de0 100644 --- a/actionpack/lib/action_controller.rb +++ b/actionpack/lib/action_controller.rb @@ -41,6 +41,10 @@ module ActionController autoload :UrlFor end + autoload_under "api" do + autoload :ApiRendering + end + autoload :TestCase, 'action_controller/test_case' autoload :TemplateAssertions, 'action_controller/test_case' diff --git a/actionpack/lib/action_controller/api.rb b/actionpack/lib/action_controller/api.rb index 1a46d49a49..ff12705abe 100644 --- a/actionpack/lib/action_controller/api.rb +++ b/actionpack/lib/action_controller/api.rb @@ -112,7 +112,7 @@ module ActionController UrlFor, Redirecting, - Rendering, + ApiRendering, Renderers::All, ConditionalGet, BasicImplicitRender, diff --git a/actionpack/lib/action_controller/api/api_rendering.rb b/actionpack/lib/action_controller/api/api_rendering.rb new file mode 100644 index 0000000000..3a08d28c39 --- /dev/null +++ b/actionpack/lib/action_controller/api/api_rendering.rb @@ -0,0 +1,14 @@ +module ActionController + module ApiRendering + extend ActiveSupport::Concern + + included do + include Rendering + end + + def render_to_body(options = {}) + _process_options(options) + super + end + end +end diff --git a/actionpack/lib/action_controller/caching/fragments.rb b/actionpack/lib/action_controller/caching/fragments.rb index 2694d4c12f..b9ad51a9cf 100644 --- a/actionpack/lib/action_controller/caching/fragments.rb +++ b/actionpack/lib/action_controller/caching/fragments.rb @@ -14,12 +14,57 @@ module ActionController # # expire_fragment('name_of_cache') module Fragments + extend ActiveSupport::Concern + + included do + if respond_to?(:class_attribute) + class_attribute :fragment_cache_keys + else + mattr_writer :fragment_cache_keys + end + + self.fragment_cache_keys = [] + + helper_method :fragment_cache_key if respond_to?(:helper_method) + end + + module ClassMethods + # Allows you to specify controller-wide key prefixes for + # cache fragments. Pass either a constant +value+, or a block + # which computes a value each time a cache key is generated. + # + # For example, you may want to prefix all fragment cache keys + # with a global version identifier, so you can easily + # invalidate all caches. + # + # class ApplicationController + # fragment_cache_key "v1" + # end + # + # When it's time to invalidate all fragments, simply change + # the string constant. Or, progressively roll out the cache + # invalidation using a computed value: + # + # class ApplicationController + # fragment_cache_key do + # @account.id.odd? ? "v1" : "v2" + # end + # end + def fragment_cache_key(value = nil, &key) + self.fragment_cache_keys += [key || ->{ value }] + end + end + # Given a key (as described in +expire_fragment+), returns # a key suitable for use in reading, writing, or expiring a - # cached fragment. All keys are prefixed with <tt>views/</tt> and uses - # ActiveSupport::Cache.expand_cache_key for the expansion. + # cached fragment. All keys begin with <tt>views/</tt>, + # followed by any controller-wide key prefix values, ending + # with the specified +key+ value. The key is expanded using + # ActiveSupport::Cache.expand_cache_key. def fragment_cache_key(key) - ActiveSupport::Cache.expand_cache_key(key.is_a?(Hash) ? url_for(key).split("://").last : key, :views) + head = self.class.fragment_cache_keys.map { |k| instance_exec(&k) } + tail = key.is_a?(Hash) ? url_for(key).split("://").last : key + ActiveSupport::Cache.expand_cache_key([*head, *tail], :views) end # Writes +content+ to the location signified by diff --git a/actionpack/lib/action_controller/metal.rb b/actionpack/lib/action_controller/metal.rb index 94ec62ec6f..f6a93a8940 100644 --- a/actionpack/lib/action_controller/metal.rb +++ b/actionpack/lib/action_controller/metal.rb @@ -1,6 +1,5 @@ require 'active_support/core_ext/array/extract_options' require 'action_dispatch/middleware/stack' -require 'active_support/deprecation' require 'action_dispatch/http/request' require 'action_dispatch/http/response' @@ -167,7 +166,7 @@ module ActionController alias :response_code :status # :nodoc: - # Basic url_for that can be overridden for more robust functionality + # Basic url_for that can be overridden for more robust functionality. def url_for(string) string end diff --git a/actionpack/lib/action_controller/metal/conditional_get.rb b/actionpack/lib/action_controller/metal/conditional_get.rb index 89d589c486..f8e0d9cf6c 100644 --- a/actionpack/lib/action_controller/metal/conditional_get.rb +++ b/actionpack/lib/action_controller/metal/conditional_get.rb @@ -66,7 +66,7 @@ module ActionController # # You can also pass an object that responds to +maximum+, such as a # collection of active records. In this case +last_modified+ will be set by - # calling +maximum(:updated_at)+ on the collection (the timestamp of the + # calling <tt>maximum(:updated_at)</tt> on the collection (the timestamp of the # most recently updated record) and the +etag+ by passing the object itself. # # def index @@ -228,7 +228,7 @@ module ActionController expires_in 100.years, public: public yield if stale?(etag: "#{version}-#{request.fullpath}", - last_modified: Time.parse('2011-01-01').utc, + last_modified: Time.new(2011, 1, 1).utc, public: public) end diff --git a/actionpack/lib/action_controller/metal/exceptions.rb b/actionpack/lib/action_controller/metal/exceptions.rb index 18e003741d..5c0ada37be 100644 --- a/actionpack/lib/action_controller/metal/exceptions.rb +++ b/actionpack/lib/action_controller/metal/exceptions.rb @@ -3,14 +3,19 @@ module ActionController end class BadRequest < ActionControllerError #:nodoc: - attr_reader :original_exception + def initialize(msg = nil, e = nil) + if e + ActiveSupport::Deprecation.warn("Passing #original_exception is deprecated and has no effect. " \ + "Exceptions will automatically capture the original exception.", caller) + end - def initialize(type = nil, e = nil) - return super() unless type && e + super(msg) + set_backtrace $!.backtrace if $! + end - super("Invalid #{type} parameters: #{e.message}") - @original_exception = e - set_backtrace e.backtrace + def original_exception + ActiveSupport::Deprecation.warn("#original_exception is deprecated. Use #cause instead.", caller) + cause end end diff --git a/actionpack/lib/action_controller/metal/head.rb b/actionpack/lib/action_controller/metal/head.rb index b2110bf946..5e9832fd4e 100644 --- a/actionpack/lib/action_controller/metal/head.rb +++ b/actionpack/lib/action_controller/metal/head.rb @@ -50,7 +50,6 @@ module ActionController end private - # :nodoc: def include_content?(status) case status when 100..199 diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb index 0a36fecd27..35be6d9300 100644 --- a/actionpack/lib/action_controller/metal/http_authentication.rb +++ b/actionpack/lib/action_controller/metal/http_authentication.rb @@ -1,4 +1,5 @@ require 'base64' +require 'active_support/security_utils' module ActionController # Makes it dead easy to do HTTP Basic, Digest and Token authentication. @@ -68,7 +69,11 @@ module ActionController def http_basic_authenticate_with(options = {}) before_action(options.except(:name, :password, :realm)) do authenticate_or_request_with_http_basic(options[:realm] || "Application") do |name, password| - name == options[:name] && password == options[:password] + # This comparison uses & so that it doesn't short circuit and + # uses `variable_size_secure_compare` so that length information + # isn't leaked. + ActiveSupport::SecurityUtils.variable_size_secure_compare(name, options[:name]) & + ActiveSupport::SecurityUtils.variable_size_secure_compare(password, options[:password]) end end end @@ -397,7 +402,7 @@ module ActionController # RewriteRule ^(.*)$ dispatch.fcgi [E=X-HTTP_AUTHORIZATION:%{HTTP:Authorization},QSA,L] module Token TOKEN_KEY = 'token=' - TOKEN_REGEX = /^(Token|Bearer) / + TOKEN_REGEX = /^(Token|Bearer)\s+/ AUTHN_PAIR_DELIMITERS = /(?:,|;|\t+)/ extend self diff --git a/actionpack/lib/action_controller/metal/live.rb b/actionpack/lib/action_controller/metal/live.rb index 7db8d13e24..e3c540bf5f 100644 --- a/actionpack/lib/action_controller/metal/live.rb +++ b/actionpack/lib/action_controller/metal/live.rb @@ -37,7 +37,7 @@ module ActionController module ClassMethods def make_response!(request) - if request.env["HTTP_VERSION"] == "HTTP/1.0" + if request.get_header("HTTP_VERSION") == "HTTP/1.0" super else Live::Response.new.tap do |res| @@ -222,12 +222,6 @@ module ActionController jar.write self unless committed? end - def before_sending - super - request.cookie_jar.commit! - headers.freeze - end - def build_buffer(response, body) buf = Live::Buffer.new response body.each { |part| buf.write part } @@ -292,9 +286,5 @@ module ActionController super response.close if response end - - def set_response!(request) - @_response = self.class.make_response! request - end end end diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index 58df5c539e..173a14a1d2 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -9,6 +9,13 @@ module ActionController #:nodoc: # @people = Person.all # end # + # That action implicitly responds to all formats, but formats can also be whitelisted: + # + # def index + # @people = Person.all + # respond_to :html, :js + # end + # # Here's the same action, with web-service support baked in: # # def index @@ -16,11 +23,12 @@ module ActionController #:nodoc: # # respond_to do |format| # format.html + # format.js # format.xml { render xml: @people } # end # end # - # What that says is, "if the client wants HTML in response to this action, just respond as we + # What that says is, "if the client wants HTML or JS in response to this action, just respond as we # would have before, but if the client wants XML, return them the list of people in XML format." # (Rails determines the desired response format from the HTTP Accept header submitted by the client.) # @@ -91,11 +99,11 @@ module ActionController #:nodoc: # and accept Rails' defaults, life will be much easier. # # If you need to use a MIME type which isn't supported by default, you can register your own handlers in - # config/initializers/mime_types.rb as follows. + # +config/initializers/mime_types.rb+ as follows. # # Mime::Type.register "image/jpg", :jpg # - # Respond to also allows you to specify a common block for different formats by using any: + # Respond to also allows you to specify a common block for different formats by using +any+: # # def index # @people = Person.all @@ -151,7 +159,7 @@ module ActionController #:nodoc: # format.html.none { render "trash" } # end # - # Variants also support common `any`/`all` block that formats have. + # Variants also support common +any+/+all+ block that formats have. # # It works for both inline: # @@ -174,15 +182,12 @@ module ActionController #:nodoc: # 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: + # +: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 <tt>ActionController::MimeResponds.respond_to</tt> - # for more examples. def respond_to(*mimes) raise ArgumentError, "respond_to takes either types or a block, never both" if mimes.any? && block_given? diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb index 0febc905f1..b13ba06962 100644 --- a/actionpack/lib/action_controller/metal/redirecting.rb +++ b/actionpack/lib/action_controller/metal/redirecting.rb @@ -20,8 +20,6 @@ module ActionController # * <tt>String</tt> starting with <tt>protocol://</tt> (like <tt>http://</tt>) or a protocol relative reference (like <tt>//</tt>) - Is passed straight through as the target for redirection. # * <tt>String</tt> not containing a protocol - The current protocol and host is prepended to the string. # * <tt>Proc</tt> - A block that will be executed in the controller's context. Should return any option accepted by +redirect_to+. - # * <tt>:back</tt> - Back to the page that issued the request. Useful for forms that are triggered from multiple places. - # Short-hand for <tt>redirect_to(request.env["HTTP_REFERER"])</tt> # # === Examples: # @@ -30,7 +28,6 @@ module ActionController # redirect_to "http://www.rubyonrails.org" # redirect_to "/images/screenshot.jpg" # redirect_to articles_url - # redirect_to :back # redirect_to proc { edit_post_url(@post) } # # The redirection happens as a "302 Found" header unless otherwise specified using the <tt>:status</tt> option: @@ -61,13 +58,8 @@ module ActionController # redirect_to post_url(@post), status: 301, flash: { updated_post_id: @post.id } # redirect_to({ action: 'atom' }, alert: "Something serious happened") # - # When using <tt>redirect_to :back</tt>, if there is no referrer, - # <tt>ActionController::RedirectBackError</tt> will be raised. You - # may specify some fallback behavior for this case by rescuing - # <tt>ActionController::RedirectBackError</tt>. def redirect_to(options = {}, response_status = {}) #:doc: raise ActionControllerError.new("Cannot redirect to nil!") unless options - raise ActionControllerError.new("Cannot redirect to a parameter hash!") if options.is_a?(ActionController::Parameters) raise AbstractController::DoubleRenderError if response_body self.status = _extract_redirect_to_status(options, response_status) @@ -75,6 +67,32 @@ module ActionController self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(location)}\">redirected</a>.</body></html>" end + # Redirects the browser to the page that issued the request (the referrer) + # if possible, otherwise redirects to the provided default fallback + # location. + # + # The referrer information is pulled from the HTTP `Referer` (sic) header on + # the request. This is an optional header and its presence on the request is + # subject to browser security settings and user preferences. If the request + # is missing this header, the <tt>fallback_location</tt> will be used. + # + # redirect_back fallback_location: { action: "show", id: 5 } + # redirect_back fallback_location: post + # redirect_back fallback_location: "http://www.rubyonrails.org" + # redirect_back fallback_location: "/images/screenshot.jpg" + # redirect_back fallback_location: articles_url + # redirect_back fallback_location: proc { edit_post_url(@post) } + # + # All options that can be passed to <tt>redirect_to</tt> are accepted as + # options and the behavior is indetical. + def redirect_back(fallback_location:, **args) + if referer = request.headers["Referer"] + redirect_to referer, **args + else + redirect_to fallback_location, **args + end + end + def _compute_redirect_to_location(request, options) #:nodoc: case options # The scheme name consist of a letter followed by any combination of @@ -87,6 +105,12 @@ module ActionController when String request.protocol + request.host_with_port + options when :back + ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) + `redirect_to :back` is deprecated and will be removed from Rails 5.1. + Please use `redirect_back(fallback_location: fallback_location)` where + `fallback_location` represents the location to use if the request has + no HTTP referer information. + MESSAGE request.headers["Referer"] or raise RedirectBackError when Proc _compute_redirect_to_location request, options.call diff --git a/actionpack/lib/action_controller/metal/renderers.rb b/actionpack/lib/action_controller/metal/renderers.rb index 22e0bb5955..90fb34e386 100644 --- a/actionpack/lib/action_controller/metal/renderers.rb +++ b/actionpack/lib/action_controller/metal/renderers.rb @@ -11,6 +11,7 @@ module ActionController Renderers.remove(key) end + # See <tt>Responder#api_behavior</tt> class MissingRenderer < LoadError def initialize(format) super "No renderer defined for format: #{format}" @@ -20,40 +21,25 @@ module ActionController module Renderers extend ActiveSupport::Concern + # 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 + included do class_attribute :_renderers self._renderers = Set.new.freeze end - module ClassMethods - def use_renderers(*args) - renderers = _renderers + args - self._renderers = renderers.freeze - end - alias use_renderer use_renderers - end - - def render_to_body(options) - _render_to_body_with_renderer(options) || super - end + # Used in <tt>ActionController::Base</tt> + # and <tt>ActionController::API</tt> to include all + # renderers by default. + module All + extend ActiveSupport::Concern + include Renderers - def _render_to_body_with_renderer(options) - _renderers.each do |name| - if options.key?(name) - _process_options(options) - method_name = Renderers._render_with_renderer_method_name(name) - return send(method_name, options.delete(name), options) - end + included do + self._renderers = RENDERERS end - nil - end - - # 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 - - def self._render_with_renderer_method_name(key) - "_render_with_renderer_#{key}" end # Adds a new renderer to call within controller actions. @@ -103,13 +89,70 @@ module ActionController remove_method(method_name) if method_defined?(method_name) end - module All - extend ActiveSupport::Concern - include Renderers + def self._render_with_renderer_method_name(key) + "_render_with_renderer_#{key}" + end - included do - self._renderers = RENDERERS + module ClassMethods + + # Adds, by name, a renderer or renderers to the +_renderers+ available + # to call within controller actions. + # + # It is useful when rendering from an <tt>ActionController::Metal</tt> controller or + # otherwise to add an available renderer proc to a specific controller. + # + # Both <tt>ActionController::Base</tt> and <tt>ActionController::API</tt> + # include <tt>ActionController::Renderers::All</tt>, making all renderers + # avaialable in the controller. See <tt>Renderers::RENDERERS</tt> and <tt>Renderers.add</tt>. + # + # Since <tt>ActionController::Metal</tt> controllers cannot render, the controller + # must include <tt>AbstractController::Rendering</tt>, <tt>ActionController::Rendering</tt>, + # and <tt>ActionController::Renderers</tt>, and have at lest one renderer. + # + # Rather than including <tt>ActionController::Renderers::All</tt> and including all renderers, + # you may specify which renderers to include by passing the renderer name or names to + # +use_renderers+. For example, a controller that includes only the <tt>:json</tt> renderer + # (+_render_with_renderer_json+) might look like: + # + # class MetalRenderingController < ActionController::Metal + # include AbstractController::Rendering + # include ActionController::Rendering + # include ActionController::Renderers + # + # use_renderers :json + # + # def show + # render json: record + # end + # end + # + # You must specify a +use_renderer+, else the +controller.renderer+ and + # +controller._renderers+ will be <tt>nil</tt>, and the action will fail. + def use_renderers(*args) + renderers = _renderers + args + self._renderers = renderers.freeze end + alias use_renderer use_renderers + end + + # Called by +render+ in <tt>AbstractController::Rendering</tt> + # which sets the return value as the +response_body+. + # + # If no renderer is found, +super+ returns control to + # <tt>ActionView::Rendering.render_to_body</tt>, if present. + def render_to_body(options) + _render_to_body_with_renderer(options) || super + end + + def _render_to_body_with_renderer(options) + _renderers.each do |name| + if options.key?(name) + _process_options(options) + method_name = Renderers._render_with_renderer_method_name(name) + return send(method_name, options.delete(name), options) + end + end + nil end add :json do |json, options| diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb index 172fbdf954..cce6fe7787 100644 --- a/actionpack/lib/action_controller/metal/rendering.rb +++ b/actionpack/lib/action_controller/metal/rendering.rb @@ -1,4 +1,3 @@ -require 'active_support/deprecation' require 'active_support/core_ext/string/filters' module ActionController diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index 64f6f7cf51..91b3403ad5 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -77,6 +77,14 @@ module ActionController #:nodoc: config_accessor :log_warning_on_csrf_failure self.log_warning_on_csrf_failure = true + # Controls whether the Origin header is checked in addition to the CSRF token. + config_accessor :forgery_protection_origin_check + self.forgery_protection_origin_check = false + + # Controls whether form-action/method specific CSRF tokens are used. + config_accessor :per_form_csrf_tokens + self.per_form_csrf_tokens = false + helper_method :form_authenticity_token helper_method :protect_against_forgery? end @@ -98,13 +106,13 @@ module ActionController #:nodoc: # # Valid Options: # - # * <tt>:only/:except</tt> - Only apply forgery protection to a subset of actions. Like <tt>only: [ :create, :create_all ]</tt>. + # * <tt>:only/:except</tt> - Only apply forgery protection to a subset of actions. For example <tt>only: [ :create, :create_all ]</tt>. # * <tt>:if/:unless</tt> - Turn off the forgery protection entirely depending on the passed Proc or method reference. - # * <tt>:prepend</tt> - By default, the verification of the authentication token is added to the front of the - # callback chain. If you need to make the verification depend on other callbacks, like authentication methods - # (say cookies vs OAuth), this might not work for you. Pass <tt>prepend: false</tt> to just add the - # verification callback in the position of the protect_from_forgery call. This means any callbacks added - # before are run first. + # * <tt>:prepend</tt> - By default, the verification of the authentication token will be added at the position of the + # protect_from_forgery call in your application. This means any callbacks added before are run first. This is useful + # when you want your forgery protection to depend on other callbacks, like authentication methods (Oauth vs Cookie auth). + # + # If you need to add verification to the beginning of the callback chain, use <tt>prepend: true</tt>. # * <tt>:with</tt> - Set the method to handle unverified request. # # Valid unverified request handling methods are: @@ -112,7 +120,7 @@ module ActionController #:nodoc: # * <tt>:reset_session</tt> - Resets the session. # * <tt>:null_session</tt> - Provides an empty session during request but doesn't reset it completely. Used as default if <tt>:with</tt> option is not specified. def protect_from_forgery(options = {}) - options = options.reverse_merge(prepend: true) + options = options.reverse_merge(prepend: false) self.forgery_protection_strategy = protection_method_class(options[:with] || :null_session) self.request_forgery_protection_token ||= :authenticity_token @@ -257,21 +265,41 @@ module ActionController #:nodoc: # * Does the X-CSRF-Token header match the form_authenticity_token def verified_request? !protect_against_forgery? || request.get? || request.head? || - valid_authenticity_token?(session, form_authenticity_param) || - valid_authenticity_token?(session, request.headers['X-CSRF-Token']) + (valid_request_origin? && any_authenticity_token_valid?) + end + + # Checks if any of the authenticity tokens from the request are valid. + def any_authenticity_token_valid? + request_authenticity_tokens.any? do |token| + valid_authenticity_token?(session, token) + end + end + + # Possible authenticity tokens sent in the request. + def request_authenticity_tokens + [form_authenticity_param, request.x_csrf_token] end # Sets the token value for the current session. - def form_authenticity_token - masked_authenticity_token(session) + def form_authenticity_token(form_options: {}) + masked_authenticity_token(session, form_options: form_options) end # Creates a masked version of the authenticity token that varies # on each request. The masking is used to mitigate SSL attacks # like BREACH. - def masked_authenticity_token(session) + def masked_authenticity_token(session, form_options: {}) + action, method = form_options.values_at(:action, :method) + + raw_token = if per_form_csrf_tokens && action && method + action_path = normalize_action_path(action) + per_form_csrf_token(session, action_path, method) + else + real_csrf_token(session) + end + one_time_pad = SecureRandom.random_bytes(AUTHENTICITY_TOKEN_LENGTH) - encrypted_csrf_token = xor_byte_strings(one_time_pad, real_csrf_token(session)) + encrypted_csrf_token = xor_byte_strings(one_time_pad, raw_token) masked_token = one_time_pad + encrypted_csrf_token Base64.strict_encode64(masked_token) end @@ -301,28 +329,54 @@ module ActionController #:nodoc: compare_with_real_token masked_token, session elsif masked_token.length == AUTHENTICITY_TOKEN_LENGTH * 2 - # Split the token into the one-time pad and the encrypted - # value and decrypt it - one_time_pad = masked_token[0...AUTHENTICITY_TOKEN_LENGTH] - encrypted_csrf_token = masked_token[AUTHENTICITY_TOKEN_LENGTH..-1] - csrf_token = xor_byte_strings(one_time_pad, encrypted_csrf_token) - - compare_with_real_token csrf_token, session + csrf_token = unmask_token(masked_token) + compare_with_real_token(csrf_token, session) || + valid_per_form_csrf_token?(csrf_token, session) else false # Token is malformed end end + def unmask_token(masked_token) + # Split the token into the one-time pad and the encrypted + # value and decrypt it + one_time_pad = masked_token[0...AUTHENTICITY_TOKEN_LENGTH] + encrypted_csrf_token = masked_token[AUTHENTICITY_TOKEN_LENGTH..-1] + xor_byte_strings(one_time_pad, encrypted_csrf_token) + end + def compare_with_real_token(token, session) ActiveSupport::SecurityUtils.secure_compare(token, real_csrf_token(session)) end + def valid_per_form_csrf_token?(token, session) + if per_form_csrf_tokens + correct_token = per_form_csrf_token( + session, + normalize_action_path(request.fullpath), + request.request_method + ) + + ActiveSupport::SecurityUtils.secure_compare(token, correct_token) + else + false + end + end + def real_csrf_token(session) session[:_csrf_token] ||= SecureRandom.base64(AUTHENTICITY_TOKEN_LENGTH) Base64.strict_decode64(session[:_csrf_token]) end + def per_form_csrf_token(session, action_path, method) + OpenSSL::HMAC.digest( + OpenSSL::Digest::SHA256.new, + real_csrf_token(session), + [action_path, method.downcase].join("#") + ) + end + def xor_byte_strings(s1, s2) s1.bytes.zip(s2.bytes).map { |(c1,c2)| c1 ^ c2 }.pack('c*') end @@ -336,5 +390,20 @@ module ActionController #:nodoc: def protect_against_forgery? allow_forgery_protection end + + # Checks if the request originated from the same origin by looking at the + # Origin header. + def valid_request_origin? + if forgery_protection_origin_check + # We accept blank origin headers because some user agents don't send it. + request.origin.nil? || request.origin == request.base_url + else + true + end + end + + def normalize_action_path(action_path) + action_path.split('?').first.to_s.chomp('/') + end end end diff --git a/actionpack/lib/action_controller/metal/rescue.rb b/actionpack/lib/action_controller/metal/rescue.rb index 68cc9a9c9b..81b9a7b9ed 100644 --- a/actionpack/lib/action_controller/metal/rescue.rb +++ b/actionpack/lib/action_controller/metal/rescue.rb @@ -7,10 +7,8 @@ module ActionController #:nodoc: include ActiveSupport::Rescuable def rescue_with_handler(exception) - if (exception.respond_to?(:original_exception) && - (orig_exception = exception.original_exception) && - handler_for_rescue(orig_exception)) - exception = orig_exception + if exception.cause && handler_for_rescue(exception.cause) + exception = exception.cause end super(exception) end diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index 130ba61786..ba03430930 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -1,8 +1,10 @@ require 'active_support/core_ext/hash/indifferent_access' +require 'active_support/core_ext/hash/transform_values' require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/string/filters' require 'active_support/rescuable' require 'action_dispatch/http/upload' +require 'rack/test' require 'stringio' require 'set' @@ -107,7 +109,8 @@ module ActionController cattr_accessor :permit_all_parameters, instance_accessor: false cattr_accessor :action_on_unpermitted_parameters, instance_accessor: false - delegate :keys, :key?, :has_key?, :empty?, :inspect, to: :@parameters + delegate :keys, :key?, :has_key?, :values, :has_value?, :value?, :empty?, :include?, :inspect, + :as_json, to: :@parameters # By default, never raise an UnpermittedParameters exception if these # params are present. The default includes both 'controller' and 'action' @@ -157,12 +160,16 @@ module ActionController if other_hash.respond_to?(:permitted?) super else - @parameters == other_hash + if other_hash.is_a?(Hash) + @parameters == other_hash.with_indifferent_access + else + @parameters == other_hash + end end end - # Returns a safe +Hash+ representation of this parameter with all - # unpermitted keys removed. + # Returns a safe <tt>ActiveSupport::HashWithIndifferentAccess</tt> + # representation of this parameter with all unpermitted keys removed. # # params = ActionController::Parameters.new({ # name: 'Senjougahara Hitagi', @@ -174,15 +181,17 @@ module ActionController # safe_params.to_h # => {"name"=>"Senjougahara Hitagi"} def to_h if permitted? - @parameters.to_h + convert_parameters_to_hashes(@parameters, :to_h) else slice(*self.class.always_permitted_parameters).permit!.to_h end end - # Returns an unsafe, unfiltered +Hash+ representation of this parameter. + # Returns an unsafe, unfiltered + # <tt>ActiveSupport::HashWithIndifferentAccess</tt> representation of this + # parameter. def to_unsafe_h - @parameters.to_h + convert_parameters_to_hashes(@parameters, :to_unsafe_h) end alias_method :to_unsafe_hash, :to_unsafe_h @@ -415,7 +424,7 @@ module ActionController # params.fetch(:none) # => ActionController::ParameterMissing: param is missing or the value is empty: none # params.fetch(:none, 'Francesco') # => "Francesco" # params.fetch(:none) { 'Francesco' } # => "Francesco" - def fetch(key, *args, &block) + def fetch(key, *args) convert_value_to_parameters( @parameters.fetch(key) { if block_given? @@ -510,7 +519,7 @@ module ActionController # to key. If the key is not found, returns the default value. If the # optional code block is given and the key is not found, pass in the key # and return the result of block. - def delete(key, &block) + def delete(key) convert_value_to_parameters(@parameters.delete(key)) end @@ -575,6 +584,24 @@ module ActionController dup end + def method_missing(method_sym, *args, &block) + if @parameters.respond_to?(method_sym) + message = <<-DEPRECATE.squish + Method #{ method_sym } is deprecated and will be removed in Rails 5.1, + as `ActionController::Parameters` no longer inherits from + hash. Using this deprecated behavior exposes potential security + problems. If you continue to use this method you may be creating + a security vulunerability in your app that can be exploited. Instead, + consider using one of these documented methods which are not + deprecated: http://api.rubyonrails.org/v#{ActionPack.version}/classes/ActionController/Parameters.html + DEPRECATE + ActiveSupport::Deprecation.warn(message) + @parameters.public_send(method_sym, *args, &block) + else + super + end + end + protected def permitted=(new_permitted) @permitted = new_permitted @@ -591,6 +618,21 @@ module ActionController end end + def convert_parameters_to_hashes(value, using) + case value + when Array + value.map { |v| convert_parameters_to_hashes(v, using) } + when Hash + value.transform_values do |v| + convert_parameters_to_hashes(v, using) + end.with_indifferent_access + when Parameters + value.send(using) + else + value + end + end + def convert_hashes_to_parameters(key, value) converted = convert_value_to_parameters(value) @parameters[key] = converted unless converted.equal?(value) diff --git a/actionpack/lib/action_controller/metal/testing.rb b/actionpack/lib/action_controller/metal/testing.rb index b2b3b4283f..ac37b00010 100644 --- a/actionpack/lib/action_controller/metal/testing.rb +++ b/actionpack/lib/action_controller/metal/testing.rb @@ -4,9 +4,6 @@ module ActionController # Behavior specific to functional tests module Functional # :nodoc: - def set_response!(request) - end - def recycle! @_url_options = nil self.formats = nil diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index 380e9d29b4..b43bb9dc17 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -7,6 +7,13 @@ require 'action_controller/template_assertions' require 'rails-dom-testing' module ActionController + # :stopdoc: + class Metal + include Testing::Functional + end + + # ActionController::TestCase will be deprecated and moved to a gem in Rails 5.1. + # Please use ActionDispatch::IntegrationTest going forward. class TestRequest < ActionDispatch::TestRequest #:nodoc: DEFAULT_ENV = ActionDispatch::TestRequest::DEFAULT_ENV.dup DEFAULT_ENV.delete 'PATH_INFO' @@ -210,11 +217,11 @@ module ActionController # # Simulate a POST response with the given HTTP parameters. # post(:create, params: { book: { title: "Love Hina" }}) # - # # Assert that the controller tried to redirect us to + # # Asserts that the controller tried to redirect us to # # the created book's URI. # assert_response :found # - # # Assert that the controller really put the book in the database. + # # Asserts that the controller really put the book in the database. # assert_not_nil Book.find_by(title: "Love Hina") # end # end @@ -452,16 +459,16 @@ module ActionController parameters = nil end - if parameters.present? || session.present? || flash.present? + if parameters || session || flash non_kwarg_request_warning end end - if body.present? + if body @request.set_header 'RAW_POST_DATA', body end - if http_method.present? + if http_method http_method = http_method.to_s.upcase else http_method = "GET" @@ -469,16 +476,12 @@ module ActionController parameters ||= {} - if format.present? + if format parameters[:format] = format end @html_document = nil - unless @controller.respond_to?(:recycle!) - @controller.extend(Testing::Functional) - end - self.cookies.update @request.cookies self.cookies.update_cookies_from_jar @request.set_header 'HTTP_COOKIE', cookies.to_header @@ -509,20 +512,19 @@ module ActionController end end - @controller.request = @request - @controller.response = @response - @request.fetch_header("SCRIPT_NAME") do |k| @request.set_header k, @controller.config.relative_url_root end @controller.recycle! - @controller.process(action) + @controller.dispatch(action, @request, @response) + @request = @controller.request + @response = @controller.response @request.delete_header 'HTTP_COOKIE' if @request.have_cookie_jar? - unless @response.committed? + unless @request.cookie_jar.committed? @request.cookie_jar.write(@response) self.cookies.update(@request.cookie_jar.instance_variable_get(:@cookies)) end @@ -659,4 +661,5 @@ module ActionController include Behavior end + # :startdoc: end diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb index f6336c8c7a..1e4df07d6e 100644 --- a/actionpack/lib/action_dispatch.rb +++ b/actionpack/lib/action_dispatch.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2015 David Heinemeier Hansson +# Copyright (c) 2004-2016 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -95,6 +95,7 @@ module ActionDispatch autoload :TestProcess autoload :TestRequest autoload :TestResponse + autoload :AssertionResponse end end diff --git a/actionpack/lib/action_dispatch/http/cache.rb b/actionpack/lib/action_dispatch/http/cache.rb index 30ade14c26..0f7898a3f8 100644 --- a/actionpack/lib/action_dispatch/http/cache.rb +++ b/actionpack/lib/action_dispatch/http/cache.rb @@ -82,7 +82,7 @@ module ActionDispatch def etag=(etag) key = ActiveSupport::Cache.expand_cache_key(etag) - super %("#{Digest::MD5.hexdigest(key)}") + super %(W/"#{Digest::MD5.hexdigest(key)}") end def etag?; etag; end @@ -91,7 +91,7 @@ module ActionDispatch DATE = 'Date'.freeze LAST_MODIFIED = "Last-Modified".freeze - SPECIAL_KEYS = Set.new(%w[extras no-cache max-age public must-revalidate]) + SPECIAL_KEYS = Set.new(%w[extras no-cache max-age public private must-revalidate]) def cache_control_segments if cache_control = _cache_control diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb index 7acf91902d..e9b25339dc 100644 --- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb +++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb @@ -69,6 +69,8 @@ module ActionDispatch Array(Mime[parameters[:format]]) elsif use_accept_header && valid_accept_header accepts + elsif extension_format = format_from_path_extension + [extension_format] elsif xhr? [Mime[:js]] else @@ -160,6 +162,13 @@ module ActionDispatch def use_accept_header !self.class.ignore_accept_header end + + def format_from_path_extension + path = @env['action_dispatch.original_path'] || @env['PATH_INFO'] + if match = path && path.match(/\.(\w+)\z/) + Mime[match.captures.first] + end + end end end end diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb index 95094c25c0..6abbf9c754 100644 --- a/actionpack/lib/action_dispatch/http/mime_type.rb +++ b/actionpack/lib/action_dispatch/http/mime_type.rb @@ -1,7 +1,6 @@ require 'singleton' require 'active_support/core_ext/module/attribute_accessors' require 'active_support/core_ext/string/starts_ends_with' -require 'active_support/deprecation' module Mime class Mimes @@ -32,7 +31,7 @@ module Mime SET = Mimes.new EXTENSION_LOOKUP = {} - LOOKUP = Hash.new { |h, k| h[k] = Type.new(k) unless k.blank? } + LOOKUP = {} class << self def [](type) @@ -48,15 +47,10 @@ module Mime def const_missing(sym) ext = sym.downcase if Mime[ext] - ActiveSupport::Deprecation.warn <<-eow -Accessing mime types via constants is deprecated. Please change: - - `Mime::#{sym}` - -to: - - `Mime[:#{ext}]` - eow + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Accessing mime types via constants is deprecated. + Please change `Mime::#{sym}` to `Mime[:#{ext}]`. + MSG Mime[ext] else super @@ -66,15 +60,10 @@ to: def const_defined?(sym, inherit = true) ext = sym.downcase if Mime[ext] - ActiveSupport::Deprecation.warn <<-eow -Accessing mime types via constants is deprecated. Please change: - - `Mime.const_defined?(#{sym})` - -to: - - `Mime[:#{ext}]` - eow + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Accessing mime types via constants is deprecated. + Please change `Mime.const_defined?(#{sym})` to `Mime[:#{ext}]`. + MSG true else super @@ -188,7 +177,7 @@ to: end def lookup(string) - LOOKUP[string] + LOOKUP[string] || Type.new(string) end def lookup_by_extension(extension) @@ -266,9 +255,12 @@ to: end end + attr_reader :hash + def initialize(string, symbol = nil, synonyms = []) @symbol, @synonyms = symbol, synonyms @string = string + @hash = [@string, @synonyms, @symbol].hash end def to_s @@ -302,6 +294,13 @@ to: end end + def eql?(other) + super || (self.class == other.class && + @string == other.string && + @synonyms == other.synonyms && + @symbol == other.symbol) + end + def =~(mime_type) return false unless mime_type regexp = Regexp.new(Regexp.quote(mime_type.to_s)) @@ -314,6 +313,10 @@ to: def all?; false; end + protected + + attr_reader :string, :synonyms + private def to_ary; end diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb index 248ecfd676..cca7376ffa 100644 --- a/actionpack/lib/action_dispatch/http/parameters.rb +++ b/actionpack/lib/action_dispatch/http/parameters.rb @@ -43,7 +43,7 @@ module ActionDispatch # # {'action' => 'my_action', 'controller' => 'my_controller'} def path_parameters - get_header(PARAMETERS_KEY) || {} + get_header(PARAMETERS_KEY) || set_header(PARAMETERS_KEY, {}) end private @@ -55,11 +55,11 @@ module ActionDispatch begin strategy.call(raw_post) - rescue => e # JSON or Ruby code block errors + rescue # JSON or Ruby code block errors my_logger = logger || ActiveSupport::Logger.new($stderr) my_logger.debug "Error occurred while parsing request parameters.\nContents:\n\n#{raw_post}" - raise ParamsParser::ParseError.new(e.message, e) + raise ParamsParser::ParseError end end diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index bf20a33d36..5427425ef7 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -36,8 +36,8 @@ module ActionDispatch HTTP_ACCEPT HTTP_ACCEPT_CHARSET HTTP_ACCEPT_ENCODING HTTP_ACCEPT_LANGUAGE HTTP_CACHE_CONTROL HTTP_FROM HTTP_NEGOTIATE HTTP_PRAGMA HTTP_CLIENT_IP - HTTP_X_FORWARDED_FOR HTTP_VERSION - HTTP_X_REQUEST_ID HTTP_X_FORWARDED_HOST + HTTP_X_FORWARDED_FOR HTTP_ORIGIN HTTP_VERSION + HTTP_X_CSRF_TOKEN HTTP_X_REQUEST_ID HTTP_X_FORWARDED_HOST SERVER_ADDR ].freeze @@ -49,6 +49,10 @@ module ActionDispatch METHOD end + def self.empty + new({}) + end + def initialize(env) super @method = nil @@ -59,13 +63,16 @@ module ActionDispatch @ip = nil end + def commit_cookie_jar! # :nodoc: + end + def check_path_parameters! # If any of the path parameters has an invalid encoding then # raise since it's likely to trigger errors further on. path_parameters.each do |key, value| next unless value.respond_to?(:valid_encoding?) unless value.valid_encoding? - raise ActionController::BadRequest, "Invalid parameter: #{key} => #{value}" + raise ActionController::BadRequest, "Invalid parameter encoding: #{key} => #{value.inspect}" end end end @@ -252,7 +259,7 @@ module ActionDispatch end # Returns the IP address of client as a +String+, - # usually set by the RemoteIp middleware. + # usually set by the RemoteIp middleware. def remote_ip @remote_ip ||= (get_header("action_dispatch.remote_ip") || ip).to_s end @@ -306,10 +313,16 @@ module ActionDispatch end end - # Returns true if the request's content MIME type is - # +application/x-www-form-urlencoded+ or +multipart/form-data+. + # Determine whether the request body contains form-data by checking + # the request Content-Type for one of the media-types: + # "application/x-www-form-urlencoded" or "multipart/form-data". The + # list of form-data media types can be modified through the + # +FORM_DATA_MEDIA_TYPES+ array. + # + # A request body is not assumed to contain form-data when no + # Content-Type header is provided and the request_method is POST. def form_data? - FORM_DATA_MEDIA_TYPES.include?(content_mime_type.to_s) + FORM_DATA_MEDIA_TYPES.include?(media_type) end def body_stream #:nodoc: @@ -338,10 +351,13 @@ module ActionDispatch # Override Rack's GET method to support indifferent access def GET fetch_header("action_dispatch.request.query_parameters") do |k| - set_header k, Request::Utils.normalize_encode_params(super || {}) + rack_query_params = super || {} + # Check for non UTF-8 parameter values, which would cause errors later + Request::Utils.check_param_encoding(rack_query_params) + set_header k, Request::Utils.normalize_encode_params(rack_query_params) end rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e - raise ActionController::BadRequest.new(:query, e) + raise ActionController::BadRequest.new("Invalid query parameters: #{e.message}") end alias :query_parameters :GET @@ -357,7 +373,7 @@ module ActionDispatch self.request_parameters = Request::Utils.normalize_encode_params(super || {}) raise rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e - raise ActionController::BadRequest.new(:request, e) + raise ActionController::BadRequest.new("Invalid request parameters: #{e.message}") end alias :request_parameters :POST diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index c54efb6541..14f86c7c07 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -149,7 +149,6 @@ module ActionDispatch # :nodoc: self.body, self.status = body, status - @blank = false @cv = new_cond @committed = false @sending = false @@ -233,7 +232,7 @@ module ActionDispatch # :nodoc: end # Sets the HTTP character set. In case of nil parameter - # it sets the charset to utf-8. + # it sets the charset to utf-8. # # response.charset = 'utf-16' # => 'utf-16' # response.charset = nil # => 'utf-8' @@ -287,12 +286,8 @@ module ActionDispatch # :nodoc: @stream.write string end - EMPTY = " " - # Allows you to manually set or override the response body. def body=(body) - @blank = true if body == EMPTY - if body.respond_to?(:to_path) @stream = body else @@ -417,6 +412,15 @@ module ActionDispatch # :nodoc: end def before_sending + # Normally we've already committed by now, but it's possible + # (e.g., if the controller action tries to read back its own + # response) to get here before that. In that case, we must force + # an "early" commit: we're about to freeze the headers, so this is + # our last chance. + commit! unless committed? + + headers.freeze + request.commit_cookie_jar! unless committed? end def build_buffer(response, body) diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb index 92b10b6d3b..37f41ae988 100644 --- a/actionpack/lib/action_dispatch/http/url.rb +++ b/actionpack/lib/action_dispatch/http/url.rb @@ -81,7 +81,8 @@ module ActionDispatch def add_params(path, params) params = { params: params } unless params.is_a?(Hash) params.reject! { |_,v| v.to_param.nil? } - path << "?#{params.to_query}" unless params.empty? + query = params.to_query + path << "?#{query}" unless query.empty? end def add_anchor(path, anchor) diff --git a/actionpack/lib/action_dispatch/journey/path/pattern.rb b/actionpack/lib/action_dispatch/journey/path/pattern.rb index 5ee8810066..018b89a2b7 100644 --- a/actionpack/lib/action_dispatch/journey/path/pattern.rb +++ b/actionpack/lib/action_dispatch/journey/path/pattern.rb @@ -124,7 +124,7 @@ module ActionDispatch end def captures - (length - 1).times.map { |i| self[i + 1] } + Array.new(length - 1) { |i| self[i + 1] } end def [](x) diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index 2889acaeb8..3477aa8b29 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -12,6 +12,12 @@ module ActionDispatch end # :stopdoc: + prepend Module.new { + def commit_cookie_jar! + cookie_jar.commit! + end + } + def have_cookie_jar? has_header? 'action_dispatch.cookies'.freeze end @@ -77,6 +83,12 @@ module ActionDispatch # # It can be read using the signed method `cookies.signed[:name]` # cookies.signed[:user_id] = current_user.id # + # # Sets an encrypted cookie value before sending it to the client which + # # prevent users from reading and tampering with its value. + # # The cookie is signed by your app's `secrets.secret_key_base` value. + # # It can be read using the encrypted method `cookies.encrypted[:name]` + # cookies.encrypted[:discount] = 45 + # # # Sets a "permanent" cookie (which expires in 20 years from now). # cookies.permanent[:login] = "XJ-122" # @@ -89,6 +101,7 @@ module ActionDispatch # cookies.size # => 2 # JSON.parse(cookies[:lat_lon]) # => [47.68, -122.37] # cookies.signed[:login] # => "XJ-122" + # cookies.encrypted[:discount] # => 45 # # Example for deleting: # diff --git a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb index 66bb74b9c5..b55c937e0c 100644 --- a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb @@ -38,9 +38,10 @@ module ActionDispatch end end - def initialize(app, routes_app = nil) - @app = app - @routes_app = routes_app + def initialize(app, routes_app = nil, response_format = :default) + @app = app + @routes_app = routes_app + @response_format = response_format end def call(env) @@ -66,41 +67,79 @@ module ActionDispatch log_error(request, wrapper) if request.get_header('action_dispatch.show_detailed_exceptions') - traces = wrapper.traces - - trace_to_show = 'Application Trace' - if traces[trace_to_show].empty? && wrapper.rescue_template != 'routing_error' - trace_to_show = 'Full Trace' + case @response_format + when :api + render_for_api_application(request, wrapper) + when :default + render_for_default_application(request, wrapper) end + else + raise exception + end + end - if source_to_show = traces[trace_to_show].first - source_to_show_id = source_to_show[:id] - end + def render_for_default_application(request, wrapper) + template = create_template(request, wrapper) + file = "rescues/#{wrapper.rescue_template}" - template = DebugView.new([RESCUES_TEMPLATE_PATH], - request: request, - exception: wrapper.exception, - traces: traces, - show_source_idx: source_to_show_id, - trace_to_show: trace_to_show, - routes_inspector: routes_inspector(exception), - source_extracts: wrapper.source_extracts, - line_number: wrapper.line_number, - file: wrapper.file - ) - file = "rescues/#{wrapper.rescue_template}" - - 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) + if request.xhr? + body = template.render(template: file, layout: false, formats: [:text]) + format = "text/plain" else - raise exception + body = template.render(template: file, layout: 'rescues/layout') + format = "text/html" end + render(wrapper.status_code, body, format) + end + + def render_for_api_application(request, wrapper) + body = { + status: wrapper.status_code, + error: Rack::Utils::HTTP_STATUS_CODES.fetch( + wrapper.status_code, + Rack::Utils::HTTP_STATUS_CODES[500] + ), + exception: wrapper.exception.inspect, + traces: wrapper.traces + } + + content_type = request.formats.first + to_format = "to_#{content_type.to_sym}" + + if content_type && body.respond_to?(to_format) + formatted_body = body.public_send(to_format) + format = content_type + else + formatted_body = body.to_json + format = Mime[:json] + end + + render(wrapper.status_code, formatted_body, format) + end + + def create_template(request, wrapper) + traces = wrapper.traces + + trace_to_show = 'Application Trace' + if traces[trace_to_show].empty? && wrapper.rescue_template != 'routing_error' + trace_to_show = 'Full Trace' + end + + if source_to_show = traces[trace_to_show].first + source_to_show_id = source_to_show[:id] + end + + DebugView.new([RESCUES_TEMPLATE_PATH], + request: request, + exception: wrapper.exception, + traces: traces, + show_source_idx: source_to_show_id, + trace_to_show: trace_to_show, + routes_inspector: routes_inspector(wrapper.exception), + source_extracts: wrapper.source_extracts, + line_number: wrapper.line_number, + file: wrapper.file + ) end def render(status, body, format) diff --git a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb index 5fd984cd07..3b61824cc9 100644 --- a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb +++ b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb @@ -37,7 +37,7 @@ module ActionDispatch @backtrace_cleaner = backtrace_cleaner @exception = original_exception(exception) - expand_backtrace if exception.is_a?(SyntaxError) || exception.try(:original_exception).try(:is_a?, SyntaxError) + expand_backtrace if exception.is_a?(SyntaxError) || exception.cause.is_a?(SyntaxError) end def rescue_template @@ -106,17 +106,13 @@ module ActionDispatch end def original_exception(exception) - if registered_original_exception?(exception) - exception.original_exception + if @@rescue_responses.has_key?(exception.cause.class.name) + exception.cause else exception end end - def registered_original_exception?(exception) - exception.respond_to?(:original_exception) && @@rescue_responses.has_key?(exception.original_exception.class.name) - end - def clean_backtrace(*args) if backtrace_cleaner backtrace_cleaner.clean(backtrace, *args) diff --git a/actionpack/lib/action_dispatch/middleware/params_parser.rb b/actionpack/lib/action_dispatch/middleware/params_parser.rb index 18af0a583a..c2a4f46e67 100644 --- a/actionpack/lib/action_dispatch/middleware/params_parser.rb +++ b/actionpack/lib/action_dispatch/middleware/params_parser.rb @@ -10,11 +10,25 @@ module ActionDispatch # Raised when raw data from the request cannot be parsed by the parser # defined for request's content mime type. class ParseError < StandardError - attr_reader :original_exception - def initialize(message, original_exception) - super(message) - @original_exception = original_exception + def initialize(message = nil, original_exception = nil) + if message + ActiveSupport::Deprecation.warn("Passing #message is deprecated and has no effect. " \ + "#{self.class} will automatically capture the message " \ + "of the original exception.", caller) + end + + if original_exception + ActiveSupport::Deprecation.warn("Passing #original_exception is deprecated and has no effect. " \ + "Exceptions will automatically capture the original exception.", caller) + end + + super($!.message) + end + + def original_exception + ActiveSupport::Deprecation.warn("#original_exception is deprecated. Use #cause instead.", caller) + cause end end diff --git a/actionpack/lib/action_dispatch/middleware/reloader.rb b/actionpack/lib/action_dispatch/middleware/reloader.rb index 6c7fba00cb..af9a29eb07 100644 --- a/actionpack/lib/action_dispatch/middleware/reloader.rb +++ b/actionpack/lib/action_dispatch/middleware/reloader.rb @@ -1,5 +1,3 @@ -require 'active_support/deprecation/reporting' - module ActionDispatch # ActionDispatch::Reloader provides prepare and cleanup callbacks, # intended to assist with code reloading during development. diff --git a/actionpack/lib/action_dispatch/middleware/remote_ip.rb b/actionpack/lib/action_dispatch/middleware/remote_ip.rb index aee2334da9..31b75498b6 100644 --- a/actionpack/lib/action_dispatch/middleware/remote_ip.rb +++ b/actionpack/lib/action_dispatch/middleware/remote_ip.rb @@ -43,7 +43,7 @@ module ActionDispatch # Create a new +RemoteIp+ middleware instance. # - # The +check_ip_spoofing+ option is on by default. When on, an exception + # The +ip_spoofing_check+ option is on by default. When on, an exception # is raised if it looks like the client is trying to lie about its own IP # address. It makes sense to turn off this check on sites aimed at non-IP # clients (like WAP devices), or behind proxies that set headers in an @@ -57,9 +57,9 @@ module ActionDispatch # with your proxy servers after it. If your proxies aren't removed, pass # them in via the +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) + def initialize(app, ip_spoofing_check = true, custom_proxies = nil) @app = app - @check_ip = check_ip_spoofing + @check_ip = ip_spoofing_check @proxies = if custom_proxies.blank? TRUSTED_PROXIES elsif custom_proxies.respond_to?(:any?) @@ -116,10 +116,18 @@ module ActionDispatch forwarded_ips = ips_from(@req.x_forwarded_for).reverse # +Client-Ip+ and +X-Forwarded-For+ should not, generally, both be set. - # If they are both set, it means that this request passed through two - # 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. + # If they are both set, it means that either: + # + # 1) This request passed through two proxies with incompatible IP header + # conventions. + # 2) The client passed one of +Client-Ip+ or +X-Forwarded-For+ + # (whichever the proxy servers weren't using) themselves. + # + # Either way, there is no way for us to determine which header is the + # right one after the fact. Since we have no idea, if we are concerned + # about IP spoofing we need to give up and explode. (If you're not + # concerned about IP spoofing you can turn the +ip_spoofing_check+ + # option off.) 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 diff --git a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb index 9e50fea3fc..5fb5953811 100644 --- a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb @@ -7,14 +7,22 @@ require 'action_dispatch/request/session' module ActionDispatch module Session class SessionRestoreError < StandardError #:nodoc: - attr_reader :original_exception - def initialize(const_error) - @original_exception = const_error + def initialize(const_error = nil) + if const_error + ActiveSupport::Deprecation.warn("Passing #original_exception is deprecated and has no effect. " \ + "Exceptions will automatically capture the original exception.", caller) + end super("Session contains objects whose class definition isn't available.\n" + "Remember to require the classes for all objects kept in the session.\n" + - "(Original exception: #{const_error.message} [#{const_error.class}])\n") + "(Original exception: #{$!.message} [#{$!.class}])\n") + set_backtrace $!.backtrace + end + + def original_exception + ActiveSupport::Deprecation.warn("#original_exception is deprecated. Use #cause instead.", caller) + cause end end @@ -59,8 +67,8 @@ module ActionDispatch begin # Note that the regexp does not allow $1 to end with a ':' $1.constantize - rescue LoadError, NameError => e - raise ActionDispatch::Session::SessionRestoreError, e, e.backtrace + rescue LoadError, NameError + raise ActionDispatch::Session::SessionRestoreError end retry else diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb index 0e636b8257..429a98f236 100644 --- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb @@ -36,7 +36,7 @@ module ActionDispatch # development: # secret_key_base: 'secret key' # - # To generate a secret key for an existing application, run `rake secret`. + # To generate a secret key for an existing application, run `rails secret`. # # If you are upgrading an existing Rails 3 app, you should leave your # existing secret_token in place and simply add the new secret_key_base. diff --git a/actionpack/lib/action_dispatch/middleware/ssl.rb b/actionpack/lib/action_dispatch/middleware/ssl.rb index 47f475559a..735b5939dd 100644 --- a/actionpack/lib/action_dispatch/middleware/ssl.rb +++ b/actionpack/lib/action_dispatch/middleware/ssl.rb @@ -1,19 +1,23 @@ module ActionDispatch - # This middleware is added to the stack when `config.force_ssl = true`. - # It does three jobs to enforce secure HTTP requests: + # This middleware is added to the stack when `config.force_ssl = true`, and is passed + # the options set in `config.ssl_options`. It does three jobs to enforce secure HTTP + # requests: # - # 1. TLS redirect. http:// requests are permanently redirected to https:// - # with the same URL host, path, etc. Pass `:host` and/or `:port` to - # modify the destination URL. This is always enabled. + # 1. TLS redirect: Permanently redirects http:// requests to https:// + # with the same URL host, path, etc. Enabled by default. Set `config.ssl_options` + # to modify the destination URL + # (e.g. `redirect: { host: "secure.widgets.com", port: 8080 }`), or set + # `redirect: false` to disable this feature. # - # 2. Secure cookies. Sets the `secure` flag on cookies to tell browsers they - # mustn't be sent along with http:// requests. This is always enabled. + # 2. Secure cookies: Sets the `secure` flag on cookies to tell browsers they + # mustn't be sent along with http:// requests. Enabled by default. Set + # `config.ssl_options` with `secure_cookies: false` to disable this feature. # - # 3. HTTP Strict Transport Security (HSTS). Tells the browser to remember + # 3. HTTP Strict Transport Security (HSTS): Tells the browser to remember # this site as TLS-only and automatically redirect non-TLS requests. - # Enabled by default. Pass `hsts: false` to disable. + # Enabled by default. Configure `config.ssl_options` with `hsts: false` to disable. # - # Configure HSTS with `hsts: { … }`: + # Set `config.ssl_options` with `hsts: { … }` to configure HSTS: # * `expires`: How long, in seconds, these settings will stick. Defaults to # `180.days` (recommended). The minimum required to qualify for browser # preload lists is `18.weeks`. @@ -26,10 +30,10 @@ module ActionDispatch # gap, browser vendors include a baked-in list of HSTS-enabled sites. # Go to https://hstspreload.appspot.com to submit your site for inclusion. # - # Disabling HSTS: To turn off HSTS, omitting the header is not enough. - # Browsers will remember the original HSTS directive until it expires. - # Instead, use the header to tell browsers to expire HSTS immediately. - # Setting `hsts: false` is a shortcut for `hsts: { expires: 0 }`. + # To turn off HSTS, omitting the header is not enough. Browsers will remember the + # original HSTS directive until it expires. Instead, use the header to tell browsers to + # expire HSTS immediately. Setting `hsts: false` is a shortcut for + # `hsts: { expires: 0 }`. class SSL # Default to 180 days, the low end for https://www.ssllabs.com/ssltest/ # and greater than the 18-week requirement for browser preload lists. @@ -39,7 +43,7 @@ module ActionDispatch { expires: HSTS_EXPIRES_IN, subdomains: false, preload: false } end - def initialize(app, redirect: {}, hsts: {}, **options) + def initialize(app, redirect: {}, hsts: {}, secure_cookies: true, **options) @app = app if options[:host] || options[:port] @@ -52,6 +56,7 @@ module ActionDispatch @redirect = redirect end + @secure_cookies = secure_cookies @hsts_header = build_hsts_header(normalize_hsts_options(hsts)) end @@ -61,10 +66,11 @@ module ActionDispatch if request.ssl? @app.call(env).tap do |status, headers, body| set_hsts_header! headers - flag_cookies_as_secure! headers + flag_cookies_as_secure! headers if @secure_cookies end else - redirect_to_https request + return redirect_to_https request if @redirect + @app.call(env) end end diff --git a/actionpack/lib/action_dispatch/middleware/stack.rb b/actionpack/lib/action_dispatch/middleware/stack.rb index 90e2ae6802..0b4bee5462 100644 --- a/actionpack/lib/action_dispatch/middleware/stack.rb +++ b/actionpack/lib/action_dispatch/middleware/stack.rb @@ -14,8 +14,21 @@ module ActionDispatch def name; klass.name; end + def ==(middleware) + case middleware + when Middleware + klass == middleware.klass + when Class + klass == middleware + end + end + def inspect - klass.to_s + if klass.is_a?(Class) + klass.to_s + else + klass.class.to_s + end end def build(app) diff --git a/actionpack/lib/action_dispatch/middleware/static.rb b/actionpack/lib/action_dispatch/middleware/static.rb index 75f8e05a3f..41c220236a 100644 --- a/actionpack/lib/action_dispatch/middleware/static.rb +++ b/actionpack/lib/action_dispatch/middleware/static.rb @@ -15,7 +15,6 @@ module ActionDispatch class FileHandler def initialize(root, index: 'index', headers: {}) @root = root.chomp('/') - @compiled_root = /^#{Regexp.escape(root)}/ @file_server = ::Rack::File.new(@root, headers) @index = index end @@ -28,7 +27,7 @@ module ActionDispatch # in the server's `public/` directory (see Static#call). def match?(path) path = ::Rack::Utils.unescape_path path - return false unless path.valid_encoding? + return false unless valid_path?(path) path = Rack::Utils.clean_path_info path paths = [path, "#{path}#{ext}", "#{path}/#{@index}#{ext}"] @@ -95,6 +94,10 @@ module ActionDispatch false end end + + def valid_path?(path) + path.valid_encoding? && !path.include?("\0") + end end # This middleware will attempt to return the contents of a file's body from diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.html.erb index e7b913bbe4..e7b913bbe4 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.html.erb diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.text.erb new file mode 100644 index 0000000000..23a9c7ba3f --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.text.erb @@ -0,0 +1,8 @@ +<% @source_extracts.first(3).each do |source_extract| %> +<% if source_extract[:code] %> +Extracted source (around line #<%= source_extract[:line_number] %>): + +<% source_extract[:code].each do |line, source| -%> +<%= line == source_extract[:line_number] ? "*#{line}" : "##{line}" -%> <%= source -%><% end -%> +<% end %> +<% end %> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb index c1e8b6cae3..5060da9369 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb @@ -1,6 +1,6 @@ <header> <h1> - <%= @exception.original_exception.class.to_s %> in + <%= @exception.cause.class.to_s %> in <%= @request.parameters["controller"].camelize if @request.parameters["controller"] %>#<%= @request.parameters["action"] %> </h1> </header> 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 index 77bcd26726..78d52acd96 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb @@ -1,4 +1,4 @@ -<%= @exception.original_exception.class.to_s %> in <%= @request.parameters["controller"].camelize if @request.parameters["controller"] %>#<%= @request.parameters["action"] %> +<%= @exception.cause.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 %> diff --git a/actionpack/lib/action_dispatch/request/session.rb b/actionpack/lib/action_dispatch/request/session.rb index 9e7fcbd849..42890225fa 100644 --- a/actionpack/lib/action_dispatch/request/session.rb +++ b/actionpack/lib/action_dispatch/request/session.rb @@ -109,7 +109,7 @@ module ActionDispatch @delegate.values end - # Writes given value to given key of the session. + # Writes given value to given key of the session. def []=(key, value) load_for_write! @delegate[key.to_s] = value @@ -148,8 +148,8 @@ module ActionDispatch @delegate.delete key.to_s end - # Returns value of given key from the session, or raises +KeyError+ - # if can't find given key in case of not setted dafault value. + # Returns value of the given key from the session, or raises +KeyError+ + # if can't find the given key and no default value is set. # Returns default value if specified. # # session.fetch(:foo) diff --git a/actionpack/lib/action_dispatch/request/utils.rb b/actionpack/lib/action_dispatch/request/utils.rb index a8151a8224..bb3df3c311 100644 --- a/actionpack/lib/action_dispatch/request/utils.rb +++ b/actionpack/lib/action_dispatch/request/utils.rb @@ -13,6 +13,21 @@ module ActionDispatch end end + def self.check_param_encoding(params) + case params + when Array + params.each { |element| check_param_encoding(element) } + when Hash + params.each_value { |value| check_param_encoding(value) } + when String + unless params.valid_encoding? + # Raise Rack::Utils::InvalidParameterError for consistency with Rack. + # ActionDispatch::Request#GET will re-raise as a BadRequest error. + raise Rack::Utils::InvalidParameterError, "Non UTF-8 value: #{params}" + end + end + end + class ParamEncoder # :nodoc: # Convert nested Hash to HashWithIndifferentAccess. # diff --git a/actionpack/lib/action_dispatch/routing.rb b/actionpack/lib/action_dispatch/routing.rb index 59c3f9248f..d00b2c3eb5 100644 --- a/actionpack/lib/action_dispatch/routing.rb +++ b/actionpack/lib/action_dispatch/routing.rb @@ -237,7 +237,7 @@ module ActionDispatch # # == View a list of all your routes # - # rake routes + # rails routes # # Target specific controllers by prefixing the command with <tt>CONTROLLER=x</tt>. # diff --git a/actionpack/lib/action_dispatch/routing/inspector.rb b/actionpack/lib/action_dispatch/routing/inspector.rb index f3a5268d2e..69e6dd5215 100644 --- a/actionpack/lib/action_dispatch/routing/inspector.rb +++ b/actionpack/lib/action_dispatch/routing/inspector.rb @@ -65,7 +65,7 @@ module ActionDispatch routes = collect_routes(routes_to_display) if routes.none? - formatter.no_routes + formatter.no_routes(collect_routes(@routes), filter) return formatter.result end @@ -84,7 +84,8 @@ module ActionDispatch def filter_routes(filter) if filter - @routes.select { |route| route.defaults[:controller] == filter } + filter_name = filter.underscore.sub(/_controller$/, '') + @routes.select { |route| route.defaults[:controller] == filter_name } else @routes end @@ -136,17 +137,27 @@ module ActionDispatch @buffer << draw_header(routes) end - def no_routes - @buffer << <<-MESSAGE.strip_heredoc + def no_routes(routes, filter) + @buffer << + if routes.none? + <<-MESSAGE.strip_heredoc You don't have any routes defined! Please add some routes in config/routes.rb. - - For more information about routes, see the Rails guide: http://guides.rubyonrails.org/routing.html. MESSAGE + elsif missing_controller?(filter) + "The controller #{filter} does not exist!" + else + "No routes were found for this controller" + end + @buffer << "For more information about routes, see the Rails guide: http://guides.rubyonrails.org/routing.html." end private + def missing_controller?(controller_name) + [ controller_name.camelize, "#{controller_name.camelize}Controller" ].none?(&:safe_constantize) + end + def draw_section(routes) header_lengths = ['Prefix', 'Verb', 'URI Pattern'].map(&:length) name_width, verb_width, path_width = widths(routes).zip(header_lengths).map(&:max) @@ -187,7 +198,7 @@ module ActionDispatch def header(routes) end - def no_routes + def no_routes(*) @buffer << <<-MESSAGE.strip_heredoc <p>You don't have any routes defined!</p> <ul> diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 921cda91ee..afbaa45d20 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -3,7 +3,6 @@ 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/regexp' -require 'active_support/deprecation' require 'action_dispatch/routing/redirection' require 'action_dispatch/routing/endpoint' @@ -12,7 +11,7 @@ module ActionDispatch class Mapper URL_OPTIONS = [:protocol, :subdomain, :domain, :host, :port] - class Constraints < Endpoint #:nodoc: + class Constraints < Routing::Endpoint #:nodoc: attr_reader :app, :constraints SERVE = ->(app, req) { app.serve req } @@ -185,26 +184,32 @@ module ActionDispatch def build_path(ast, requirements, anchor) pattern = Journey::Path::Pattern.new(ast, requirements, JOINED_SEPARATORS, anchor) - # Get all the symbol nodes followed by literals that are not the - # dummy node. - symbols = ast.find_all { |n| - n.cat? && n.left.symbol? && n.right.cat? && n.right.left.literal? - }.map(&:left) - - # Get all the symbol nodes preceded by literals. - symbols.concat ast.find_all { |n| - n.cat? && n.left.literal? && n.right.cat? && n.right.left.symbol? - }.map { |n| n.right.left } + # Find all the symbol nodes that are adjacent to literal nodes and alter + # the regexp so that Journey will partition them into custom routes. + ast.find_all { |node| + next unless node.cat? + + if node.left.literal? && node.right.symbol? + symbol = node.right + elsif node.left.literal? && node.right.cat? && node.right.left.symbol? + symbol = node.right.left + elsif node.left.symbol? && node.right.literal? + symbol = node.left + elsif node.left.symbol? && node.right.cat? && node.right.left.literal? + symbol = node.left + else + next + end - symbols.each { |x| - x.regexp = /(?:#{Regexp.union(x.regexp, '-')})+/ + if symbol + symbol.regexp = /(?:#{Regexp.union(symbol.regexp, '-')})+/ + end } pattern end private :build_path - private def add_wildcard_options(options, formatted, path_ast) # Add a constraint for wildcard route to make it non-greedy and match the @@ -388,24 +393,6 @@ module ActionDispatch end module Base - # You can specify what Rails should route "/" to with the root method: - # - # root to: 'pages#main' - # - # For options, see +match+, as +root+ uses it internally. - # - # You can also pass a string which will expand - # - # root 'pages#main' - # - # You should put the root route at the top of <tt>config/routes.rb</tt>, - # because this means it will be matched first. As this is the most popular route - # of most Rails applications, this is beneficial. - def root(options = {}) - name = has_named_route?(:root) ? nil : :root - match '/', { as: name, via: :get }.merge!(options) - end - # Matches a url pattern to one or more routes. # # You should not use the +match+ method in your router @@ -601,17 +588,20 @@ module ActionDispatch def mount(app, options = nil) if options path = options.delete(:at) - else - unless Hash === app - raise ArgumentError, "must be called with mount point" - end - + elsif Hash === app options = app app, path = options.find { |k, _| k.respond_to?(:call) } options.delete(app) if app end - raise "A rack application must be specified" unless path + raise ArgumentError, "A rack application must be specified" unless app.respond_to?(:call) + raise ArgumentError, <<-MSG.strip_heredoc unless path + Must be called with mount point + + mount SomeRackApp, at: "some_route" + or + mount(SomeRackApp => "some_route") + MSG rails_app = rails_app? app options[:as] ||= app_name(app, rails_app) @@ -1687,7 +1677,20 @@ to this: @set.add_route(mapping, ast, as, anchor) end - def root(path, options={}) + # You can specify what Rails should route "/" to with the root method: + # + # root to: 'pages#main' + # + # For options, see +match+, as +root+ uses it internally. + # + # You can also pass a string which will expand + # + # root 'pages#main' + # + # You should put the root route at the top of <tt>config/routes.rb</tt>, + # because this means it will be matched first. As this is the most popular route + # of most Rails applications, this is beneficial. + def root(path, options = {}) if path.is_a?(String) options[:to] = path elsif path.is_a?(Hash) and options.empty? @@ -1699,11 +1702,11 @@ to this: if @scope.resources? with_scope_level(:root) do path_scope(parent_resource.path) do - super(options) + match_root_route(options) end end else - super(options) + match_root_route(options) end end @@ -1898,6 +1901,11 @@ to this: ensure @scope = @scope.parent end + + def match_root_route(options) + name = has_named_route?(:root) ? nil : :root + match '/', { :as => name, :via => :get }.merge!(options) + end end # Routing Concerns allow you to declare common routes that can be reused diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 339e2b7c4a..846b5fa1fc 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -1,5 +1,4 @@ require 'action_dispatch/journey' -require 'forwardable' require 'active_support/concern' require 'active_support/core_ext/object/to_query' require 'active_support/core_ext/hash/slice' @@ -31,9 +30,9 @@ module ActionDispatch controller = controller req res = controller.make_response! req dispatch(controller, params[:action], req, res) - rescue NameError => e + rescue ActionController::RoutingError if @raise_on_name_error - raise ActionController::RoutingError, e.message, e.backtrace + raise else return [404, {'X-Cascade' => 'pass'}, []] end @@ -43,6 +42,8 @@ module ActionDispatch def controller(req) req.controller_class + rescue NameError => e + raise ActionController::RoutingError, e.message, e.backtrace end def dispatch(controller, action, req, res) @@ -280,8 +281,17 @@ module ActionDispatch helper = UrlHelper.create(route, opts, route_key, url_strategy) mod.module_eval do define_method(name) do |*args| - options = nil - options = args.pop if args.last.is_a? Hash + last = args.last + options = case last + when Hash + args.pop + when ActionController::Parameters + if last.permitted? + args.pop.to_h + else + raise ArgumentError, "Generating an URL from non sanitized request parameters is insecure!" + end + end helper.call self, args, options end end @@ -372,10 +382,6 @@ module ActionDispatch end def eval_block(block) - if block.arity == 1 - raise "You are using the old router DSL which has been removed in Rails 3.1. " << - "Please check how to update your routes file at: http://www.engineyard.com/blog/2010/the-lowdown-on-routes-in-rails-3/" - end mapper = Mapper.new(self) if default_scope mapper.with_default_scope(default_scope, &block) diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb index b6c031dcf4..f91679593e 100644 --- a/actionpack/lib/action_dispatch/routing/url_for.rb +++ b/actionpack/lib/action_dispatch/routing/url_for.rb @@ -172,8 +172,11 @@ module ActionDispatch _routes.url_for(options.symbolize_keys.reverse_merge!(url_options), route_name) when ActionController::Parameters + unless options.permitted? + raise ArgumentError.new("Generating an URL from non sanitized request parameters is insecure!") + end route_name = options.delete :use_route - _routes.url_for(options.to_unsafe_h.symbolize_keys. + _routes.url_for(options.to_h.symbolize_keys. reverse_merge!(url_options), route_name) when String options diff --git a/actionpack/lib/action_dispatch/testing/assertion_response.rb b/actionpack/lib/action_dispatch/testing/assertion_response.rb new file mode 100644 index 0000000000..3fb81ff083 --- /dev/null +++ b/actionpack/lib/action_dispatch/testing/assertion_response.rb @@ -0,0 +1,49 @@ +module ActionDispatch + # This is a class that abstracts away an asserted response. + # It purposely does not inherit from Response, because it doesn't need it. + # That means it does not have headers or a body. + # + # As an input to the initializer, we take a Fixnum, a String, or a Symbol. + # If it's a Fixnum or String, we figure out what its symbolized name. + # If it's a Symbol, we figure out what its corresponding code is. + # The resulting code will be a Fixnum, for real HTTP codes, and it will + # be a String for the pseudo-HTTP codes, such as: + # :success, :missing, :redirect and :error + class AssertionResponse + attr_reader :code, :name + + GENERIC_RESPONSE_CODES = { # :nodoc: + success: "2XX", + missing: "404", + redirect: "3XX", + error: "5XX" + } + + def initialize(code_or_name) + if code_or_name.is_a?(Symbol) + @name = code_or_name + @code = code_from_name(code_or_name) + else + @name = name_from_code(code_or_name) + @code = code_or_name + end + + raise ArgumentError, "Invalid response name: #{name}" if @code.nil? + raise ArgumentError, "Invalid response code: #{code}" if @name.nil? + end + + def code_and_name + "#{code}: #{name}" + end + + private + + def code_from_name(name) + GENERIC_RESPONSE_CODES[name] || Rack::Utils::SYMBOL_TO_STATUS_CODE[name] + end + + def name_from_code(code) + GENERIC_RESPONSE_CODES.invert[code] || Rack::Utils::HTTP_STATUS_CODES[code] + end + end +end diff --git a/actionpack/lib/action_dispatch/testing/assertions.rb b/actionpack/lib/action_dispatch/testing/assertions.rb index 8dd0bd63ad..fae266273e 100644 --- a/actionpack/lib/action_dispatch/testing/assertions.rb +++ b/actionpack/lib/action_dispatch/testing/assertions.rb @@ -12,7 +12,7 @@ module ActionDispatch include Rails::Dom::Testing::Assertions def html_document - @html_document ||= if @response.content_type === Mime[:xml] + @html_document ||= if @response.content_type.to_s =~ /xml\z/ Nokogiri::XML::Document.parse(@response.body) else Nokogiri::HTML::Document.parse(@response.body) diff --git a/actionpack/lib/action_dispatch/testing/assertions/response.rb b/actionpack/lib/action_dispatch/testing/assertions/response.rb index b6e21b0d28..cd55b7d975 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/response.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/response.rb @@ -1,4 +1,3 @@ - module ActionDispatch module Assertions # A small suite of assertions that test responses from \Rails applications. @@ -21,41 +20,35 @@ module ActionDispatch # or its symbolic equivalent <tt>assert_response(:not_implemented)</tt>. # See Rack::Utils::SYMBOL_TO_STATUS_CODE for a full list. # - # # assert that the response was a redirection + # # Asserts that the response was a redirection # assert_response :redirect # - # # assert that the response code was status code 401 (unauthorized) + # # Asserts that the response code was status code 401 (unauthorized) # assert_response 401 def assert_response(type, message = nil) - if Symbol === type - if [:success, :missing, :redirect, :error].include?(type) - assert_predicate @response, RESPONSE_PREDICATES[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 + message ||= generate_response_message(type) + + if RESPONSE_PREDICATES.keys.include?(type) + assert @response.send(RESPONSE_PREDICATES[type]), message else - assert_equal type, @response.response_code, message + assert_equal AssertionResponse.new(type).code, @response.response_code, message end end - # Assert that the redirection options passed in match those of the redirect called in the latest action. + # Asserts that the redirection options passed in match those of the redirect called in the latest action. # This match can be partial, such that <tt>assert_redirected_to(controller: "weblog")</tt> will also # match the redirection of <tt>redirect_to(controller: "weblog", action: "show")</tt> and so on. # - # # assert that the redirection was to the "index" action on the WeblogController + # # Asserts that the redirection was to the "index" action on the WeblogController # assert_redirected_to controller: "weblog", action: "index" # - # # assert that the redirection was to the named route login_url + # # Asserts that the redirection was to the named route login_url # assert_redirected_to login_url # - # # assert that the redirection was to the url for @customer + # # Asserts that the redirection was to the url for @customer # assert_redirected_to @customer # - # # asserts that the redirection matches the regular expression + # # Asserts that the redirection matches the regular expression # assert_redirected_to %r(\Ahttp://example.org) def assert_redirected_to(options = {}, message=nil) assert_response(:redirect, message) @@ -82,6 +75,26 @@ module ActionDispatch handle._compute_redirect_to_location(@request, fragment) end end + + def generate_response_message(expected, actual = @response.response_code) + "Expected response to be a <#{code_with_name(expected)}>,"\ + " but was a <#{code_with_name(actual)}>" + .concat location_if_redirected + end + + def location_if_redirected + return '' unless @response.redirection? && @response.location.present? + location = normalize_argument_to_redirection(@response.location) + " redirect to <#{location}>" + end + + def code_with_name(code_or_name) + if RESPONSE_PREDICATES.values.include?("#{code_or_name}?".to_sym) + code_or_name = RESPONSE_PREDICATES.invert["#{code_or_name}?".to_sym] + end + + AssertionResponse.new(code_or_name).code_and_name + 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 54e24ed6bf..78ef860548 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/routing.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/routing.rb @@ -14,14 +14,14 @@ module ActionDispatch # requiring a specific HTTP method. The hash should contain a :path with the incoming request path # and a :method containing the required HTTP verb. # - # # assert that POSTing to /items will call the create action on ItemsController + # # Asserts that POSTing to /items will call the create action on ItemsController # assert_recognizes({controller: 'items', action: 'create'}, {path: 'items', method: :post}) # # You can also pass in +extras+ with a hash containing URL parameters that would normally be in the query string. This can be used - # to assert that values in the query string string will end up in the params hash correctly. To test query strings you must use the + # to assert that values in the query string will end up in the params hash correctly. To test query strings you must use the # extras argument, appending the query string on the path directly will not work. For example: # - # # assert that a path of '/items/list/1?view=print' returns the correct options + # # Asserts that a path of '/items/list/1?view=print' returns the correct options # assert_recognizes({controller: 'items', action: 'list', id: '1', view: 'print'}, 'items/list/1', { view: "print" }) # # The +message+ parameter allows you to pass in an error message that is displayed upon failure. @@ -104,13 +104,13 @@ module ActionDispatch # The +extras+ hash allows you to specify options that would normally be provided as a query string to the action. The # +message+ parameter allows you to specify a custom error message to display upon failure. # - # # Assert a basic route: a controller with the default action (index) + # # Asserts a basic route: a controller with the default action (index) # assert_routing '/home', controller: 'home', action: 'index' # # # Test a route generated with a specific controller, action, and parameter (id) # assert_routing '/entries/show/23', controller: 'entries', action: 'show', id: 23 # - # # Assert a basic route (controller + default action), with an error message if it fails + # # Asserts a basic route (controller + default action), with an error message if it fails # assert_routing '/store', { controller: 'store', action: 'index' }, {}, {}, 'Route for store index not generated properly' # # # Tests a route, providing a defaults hash diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index 7e59bb68cf..711ca10419 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -131,35 +131,35 @@ module ActionDispatch # Performs a GET request, following any subsequent redirect. # See +request_via_redirect+ for more information. def get_via_redirect(path, *args) - ActiveSupport::Deprecation.warn('`get_via_redirect` is deprecated and will be removed in the next version of Rails. Please use follow_redirect! manually after the request call for the same behavior.') + ActiveSupport::Deprecation.warn('`get_via_redirect` is deprecated and will be removed in Rails 5.1. Please use follow_redirect! manually after the request call for the same behavior.') request_via_redirect(:get, path, *args) end # Performs a POST request, following any subsequent redirect. # See +request_via_redirect+ for more information. def post_via_redirect(path, *args) - ActiveSupport::Deprecation.warn('`post_via_redirect` is deprecated and will be removed in the next version of Rails. Please use follow_redirect! manually after the request call for the same behavior.') + ActiveSupport::Deprecation.warn('`post_via_redirect` is deprecated and will be removed in Rails 5.1. Please use follow_redirect! manually after the request call for the same behavior.') request_via_redirect(:post, path, *args) end # Performs a PATCH request, following any subsequent redirect. # See +request_via_redirect+ for more information. def patch_via_redirect(path, *args) - ActiveSupport::Deprecation.warn('`patch_via_redirect` is deprecated and will be removed in the next version of Rails. Please use follow_redirect! manually after the request call for the same behavior.') + ActiveSupport::Deprecation.warn('`patch_via_redirect` is deprecated and will be removed in Rails 5.1. Please use follow_redirect! manually after the request call for the same behavior.') request_via_redirect(:patch, path, *args) end # Performs a PUT request, following any subsequent redirect. # See +request_via_redirect+ for more information. def put_via_redirect(path, *args) - ActiveSupport::Deprecation.warn('`put_via_redirect` is deprecated and will be removed in the next version of Rails. Please use follow_redirect! manually after the request call for the same behavior.') + ActiveSupport::Deprecation.warn('`put_via_redirect` is deprecated and will be removed in Rails 5.1. Please use follow_redirect! manually after the request call for the same behavior.') request_via_redirect(:put, path, *args) end # Performs a DELETE request, following any subsequent redirect. # See +request_via_redirect+ for more information. def delete_via_redirect(path, *args) - ActiveSupport::Deprecation.warn('`delete_via_redirect` is deprecated and will be removed in the next version of Rails. Please use follow_redirect! manually after the request call for the same behavior.') + ActiveSupport::Deprecation.warn('`delete_via_redirect` is deprecated and will be removed in Rails 5.1. Please use follow_redirect! manually after the request call for the same behavior.') request_via_redirect(:delete, path, *args) end end @@ -375,6 +375,7 @@ module ActionDispatch @request = ActionDispatch::Request.new(session.last_request.env) response = _mock_session.last_response @response = ActionDispatch::TestResponse.from_response(response) + @response.request = @request @html_document = nil @url_options = nil diff --git a/actionpack/lib/action_dispatch/testing/test_process.rb b/actionpack/lib/action_dispatch/testing/test_process.rb index c28d701b48..eca0439909 100644 --- a/actionpack/lib/action_dispatch/testing/test_process.rb +++ b/actionpack/lib/action_dispatch/testing/test_process.rb @@ -26,7 +26,7 @@ module ActionDispatch @response.redirect_url end - # Shortcut for <tt>Rack::Test::UploadedFile.new(File.join(ActionController::TestCase.fixture_path, path), type)</tt>: + # Shortcut for <tt>Rack::Test::UploadedFile.new(File.join(ActionDispatch::IntegrationTest.fixture_path, path), type)</tt>: # # post :change_avatar, avatar: fixture_file_upload('files/spongebob.png', 'image/png') # diff --git a/actionpack/lib/action_pack.rb b/actionpack/lib/action_pack.rb index f664dab620..941877d10d 100644 --- a/actionpack/lib/action_pack.rb +++ b/actionpack/lib/action_pack.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2015 David Heinemeier Hansson +# Copyright (c) 2004-2016 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 index 255ac9f4ed..3a1410d492 100644 --- a/actionpack/lib/action_pack/gem_version.rb +++ b/actionpack/lib/action_pack/gem_version.rb @@ -8,7 +8,7 @@ module ActionPack MAJOR = 5 MINOR = 0 TINY = 0 - PRE = "alpha" + PRE = "beta1.1" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end |