diff options
Diffstat (limited to 'actionpack/lib/action_controller')
20 files changed, 428 insertions, 116 deletions
diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index 7f9ed54264..e6fe6b0b00 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -1,21 +1,8 @@ +require 'action_view' require "action_controller/log_subscriber" require "action_controller/metal/params_wrapper" module ActionController - # The <tt>metal</tt> anonymous class was introduced to solve issue with including modules in <tt>ActionController::Base</tt>. - # Modules needs to be included in particluar order. First we need to have <tt>AbstractController::Rendering</tt> included, - # next we should include actuall implementation which would be for example <tt>ActionView::Rendering</tt> and after that - # <tt>ActionController::Rendering</tt>. This order must be preserved and as we want to have middle module included dynamicaly - # <tt>metal</tt> class was introduced. It has <tt>AbstractController::Rendering</tt> included and is parent class of - # <tt>ActionController::Base</tt> which includes <tt>ActionController::Rendering</tt>. If we include <tt>ActionView::Rendering</tt> - # beetween them to perserve the required order, we can simply do this by: - # - # ActionController::Base.superclass.send(:include, ActionView::Rendering) - # - metal = Class.new(Metal) do - include AbstractController::Rendering - end - # Action Controllers are the core of a web request in \Rails. They are made up of one or more actions that are executed # on request and then either it renders a template or redirects to another action. An action is defined as a public method # on the controller, which will automatically be made accessible to the web-server through \Rails Routes. @@ -58,7 +45,7 @@ module ActionController # # def server_ip # location = request.env["SERVER_ADDR"] - # render text: "This server hosted at #{location}" + # render plain: "This server hosted at #{location}" # end # # == Parameters @@ -174,7 +161,7 @@ module ActionController # render action: "overthere" # won't be called if monkeys is nil # end # - class Base < metal + class Base < Metal abstract! # We document the request and response methods here because albeit they are @@ -214,6 +201,7 @@ module ActionController end MODULES = [ + AbstractController::Rendering, AbstractController::Translation, AbstractController::AssetPaths, @@ -221,6 +209,7 @@ module ActionController HideActions, UrlFor, Redirecting, + ActionView::Layouts, Rendering, Renderers::All, ConditionalGet, diff --git a/actionpack/lib/action_controller/log_subscriber.rb b/actionpack/lib/action_controller/log_subscriber.rb index 9279d8bcea..b1acca2435 100644 --- a/actionpack/lib/action_controller/log_subscriber.rb +++ b/actionpack/lib/action_controller/log_subscriber.rb @@ -50,7 +50,16 @@ module ActionController def unpermitted_parameters(event) unpermitted_keys = event.payload[:keys] - debug("Unpermitted parameters: #{unpermitted_keys.join(", ")}") + debug("Unpermitted parameter#{'s' if unpermitted_keys.size > 1}: #{unpermitted_keys.join(", ")}") + end + + def deep_munge(event) + message = "Value for params[:#{event.payload[:keys].join('][:')}] was set "\ + "to nil, because it was one of [], [null] or [null, null, ...]. "\ + "Go to http://guides.rubyonrails.org/security.html#unsafe-query-generation "\ + "for more information."\ + + debug(message) end %w(write_fragment read_fragment exist_fragment? diff --git a/actionpack/lib/action_controller/metal.rb b/actionpack/lib/action_controller/metal.rb index b84c9e78c3..0f4cc7a8f5 100644 --- a/actionpack/lib/action_controller/metal.rb +++ b/actionpack/lib/action_controller/metal.rb @@ -70,7 +70,8 @@ module ActionController # can do the following: # # class HelloController < ActionController::Metal - # include ActionController::Rendering + # include AbstractController::Rendering + # include ActionView::Layouts # append_view_path "#{Rails.root}/app/views" # # def index diff --git a/actionpack/lib/action_controller/metal/data_streaming.rb b/actionpack/lib/action_controller/metal/data_streaming.rb index 75c4d3ef99..1abd8d3a33 100644 --- a/actionpack/lib/action_controller/metal/data_streaming.rb +++ b/actionpack/lib/action_controller/metal/data_streaming.rb @@ -96,7 +96,7 @@ module ActionController #:nodoc: end # Sends the given binary data to the browser. This method is similar to - # <tt>render text: data</tt>, but also allows you to specify whether + # <tt>render plain: data</tt>, but also allows you to specify whether # the browser should display the response as a file attachment (i.e. in a # download dialog) or as inline data. You may also set the content type, # the apparent file name, and other things. diff --git a/actionpack/lib/action_controller/metal/head.rb b/actionpack/lib/action_controller/metal/head.rb index 424473801d..43407f5b78 100644 --- a/actionpack/lib/action_controller/metal/head.rb +++ b/actionpack/lib/action_controller/metal/head.rb @@ -1,6 +1,6 @@ module ActionController module Head - # Return a response that has no content (merely headers). The options + # Returns a response that has no content (merely headers). The options # argument is interpreted to be a hash of header names and values. # This allows you to easily return a response that consists only of # significant headers: diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb index 158d552ec7..2eb7853aa6 100644 --- a/actionpack/lib/action_controller/metal/http_authentication.rb +++ b/actionpack/lib/action_controller/metal/http_authentication.rb @@ -11,11 +11,11 @@ module ActionController # http_basic_authenticate_with name: "dhh", password: "secret", except: :index # # def index - # render text: "Everyone can see me!" + # render plain: "Everyone can see me!" # end # # def edit - # render text: "I'm only accessible if you know the password" + # render plain: "I'm only accessible if you know the password" # end # end # @@ -96,7 +96,7 @@ module ActionController end def user_name_and_password(request) - decode_credentials(request).split(/:/, 2) + decode_credentials(request).split(':', 2) end def decode_credentials(request) @@ -127,11 +127,11 @@ module ActionController # before_action :authenticate, except: [:index] # # def index - # render text: "Everyone can see me!" + # render plain: "Everyone can see me!" # end # # def edit - # render text: "I'm only accessible if you know the password" + # render plain: "I'm only accessible if you know the password" # end # # private @@ -321,11 +321,11 @@ module ActionController # before_action :authenticate, except: [ :index ] # # def index - # render text: "Everyone can see me!" + # render plain: "Everyone can see me!" # end # # def edit - # render text: "I'm only accessible if you know the password" + # render plain: "I'm only accessible if you know the password" # end # # private diff --git a/actionpack/lib/action_controller/metal/instrumentation.rb b/actionpack/lib/action_controller/metal/instrumentation.rb index d3aa8f90c5..b0e164bc57 100644 --- a/actionpack/lib/action_controller/metal/instrumentation.rb +++ b/actionpack/lib/action_controller/metal/instrumentation.rb @@ -67,7 +67,7 @@ module ActionController private - # A hook invoked everytime a before callback is halted. + # A hook invoked every time a before callback is halted. def halted_callback_hook(filter) ActiveSupport::Notifications.instrument("halted_callback.action_controller", :filter => filter) end diff --git a/actionpack/lib/action_controller/metal/live.rb b/actionpack/lib/action_controller/metal/live.rb index 0dd788645b..acf40b2e16 100644 --- a/actionpack/lib/action_controller/metal/live.rb +++ b/actionpack/lib/action_controller/metal/live.rb @@ -48,7 +48,7 @@ module ActionController # the server will receive a +Last-Event-ID+ header with value equal to +id+. # # After setting an option in the constructor of the SSE object, all future - # SSEs sent accross the stream will use those options unless overridden. + # SSEs sent across the stream will use those options unless overridden. # # Example Usage: # @@ -107,8 +107,11 @@ module ActionController end class Buffer < ActionDispatch::Response::Buffer #:nodoc: + include MonitorMixin + def initialize(response) - @error_callback = nil + @error_callback = lambda { true } + @cv = new_cond super(response, SizedQueue.new(10)) end @@ -122,14 +125,25 @@ module ActionController end def each + @response.sending! while str = @buf.pop yield str end + @response.sent! end def close - super - @buf.push nil + synchronize do + super + @buf.push nil + @cv.broadcast + end + end + + def await_close + synchronize do + @cv.wait_until { @closed } + end end def on_error(&block) @@ -165,12 +179,20 @@ module ActionController end end - def commit! - headers.freeze + private + + def before_committed super + jar = request.cookie_jar + # The response can be committed multiple times + jar.write self unless committed? end - private + def before_sending + super + request.cookie_jar.commit! + headers.freeze + end def build_buffer(response, body) buf = Live::Buffer.new response @@ -191,6 +213,7 @@ module ActionController t1 = Thread.current locals = t1.keys.map { |key| [key, t1[key]] } + error = nil # This processes the action in a child thread. It lets us return the # response code and headers back up the rack stack, and still process # the body in parallel with sending data to the client @@ -205,14 +228,18 @@ module ActionController begin super(name) rescue => e - begin - @_response.stream.write(ActionView::Base.streaming_completion_on_exception) if request.format == :html - @_response.stream.call_on_error - rescue => exception - log_error(exception) - ensure - log_error(e) - @_response.stream.close + if @_response.committed? + begin + @_response.stream.write(ActionView::Base.streaming_completion_on_exception) if request.format == :html + @_response.stream.call_on_error + rescue => exception + log_error(exception) + ensure + log_error(e) + @_response.stream.close + end + else + error = e end ensure @_response.commit! @@ -220,6 +247,7 @@ module ActionController } @_response.await_commit + raise error if error end def log_error(exception) @@ -234,7 +262,7 @@ module ActionController def response_body=(body) super - response.stream.close if response + response.close if response end def set_response!(request) diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index 84ade41036..1974bbf529 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -181,6 +181,73 @@ module ActionController #:nodoc: # end # end # + # Formats can have different variants. + # + # The request variant is a specialization of the request format, like <tt>:tablet</tt>, + # <tt>:phone</tt>, or <tt>:desktop</tt>. + # + # We often want to render different html/json/xml templates for phones, + # tablets, and desktop browsers. Variants make it easy. + # + # You can set the variant in a +before_action+: + # + # request.variant = :tablet if request.user_agent =~ /iPad/ + # + # Respond to variants in the action just like you respond to formats: + # + # respond_to do |format| + # format.html do |variant| + # variant.tablet # renders app/views/projects/show.html+tablet.erb + # variant.phone { extra_setup; render ... } + # variant.none { special_setup } # executed only if there is no variant set + # end + # end + # + # Provide separate templates for each format and variant: + # + # app/views/projects/show.html.erb + # app/views/projects/show.html+tablet.erb + # app/views/projects/show.html+phone.erb + # + # When you're not sharing any code within the format, you can simplify defining variants + # using the inline syntax: + # + # respond_to do |format| + # format.js { render "trash" } + # format.html.phone { redirect_to progress_path } + # format.html.none { render "trash" } + # end + # + # Variants also support common `any`/`all` block that formats have. + # + # It works for both inline: + # + # respond_to do |format| + # format.html.any { render text: "any" } + # format.html.phone { render text: "phone" } + # end + # + # and block syntax: + # + # respond_to do |format| + # format.html do |variant| + # variant.any(:tablet, :phablet){ render text: "any" } + # variant.phone { render text: "phone" } + # end + # end + # + # You can also set an array of variants: + # + # request.variant = [:tablet, :phone] + # + # which will work similarly to formats and MIME types negotiation. If there will be no + # :tablet variant declared, :phone variant will be picked: + # + # respond_to do |format| + # format.html.none + # format.html.phone # this gets rendered + # end + # # Be sure to check the documentation of +respond_with+ and # <tt>ActionController::MimeResponds.respond_to</tt> for more examples. def respond_to(*mimes, &block) @@ -260,7 +327,7 @@ module ActionController #:nodoc: # * for other requests - i.e. data formats such as xml, json, csv etc, if # the resource passed to +respond_with+ responds to <code>to_<format></code>, # the method attempts to render the resource in the requested format - # directly, e.g. for an xml request, the response is equivalent to calling + # directly, e.g. for an xml request, the response is equivalent to calling # <code>render xml: resource</code>. # # === Nested resources @@ -321,8 +388,10 @@ module ActionController #:nodoc: # 2. <tt>:action</tt> - overwrites the default render action used after an # unsuccessful html +post+ request. def respond_with(*resources, &block) - raise "In order to use respond_with, first you need to declare the formats your " \ - "controller responds to in the class level" if self.class.mimes_for_respond_to.empty? + if self.class.mimes_for_respond_to.empty? + raise "In order to use respond_with, first you need to declare the " \ + "formats your controller responds to in the class level." + end if collector = retrieve_collector_from_mimes(&block) options = resources.size == 1 ? {} : resources.extract_options! @@ -360,7 +429,7 @@ module ActionController #:nodoc: # is available. def retrieve_collector_from_mimes(mimes=nil, &block) #:nodoc: mimes ||= collect_mimes_from_class_level - collector = Collector.new(mimes) + collector = Collector.new(mimes, request.variant) block.call(collector) if block_given? format = collector.negotiate_format(request) @@ -398,9 +467,11 @@ module ActionController #:nodoc: include AbstractController::Collector attr_accessor :format - def initialize(mimes) + def initialize(mimes, variant = nil) @responses = {} - mimes.each { |mime| send(mime) } + @variant = variant + + mimes.each { |mime| @responses["Mime::#{mime.upcase}".constantize] = nil } end def any(*args, &block) @@ -414,16 +485,63 @@ module ActionController #:nodoc: def custom(mime_type, &block) mime_type = Mime::Type.lookup(mime_type.to_s) unless mime_type.is_a?(Mime::Type) - @responses[mime_type] ||= block + @responses[mime_type] ||= if block_given? + block + else + VariantCollector.new(@variant) + end end def response - @responses.fetch(format, @responses[Mime::ALL]) + response = @responses.fetch(format, @responses[Mime::ALL]) + if response.is_a?(VariantCollector) # `format.html.phone` - variant inline syntax + response.variant + elsif response.nil? || response.arity == 0 # `format.html` - just a format, call its block + response + else # `format.html{ |variant| variant.phone }` - variant block syntax + variant_collector = VariantCollector.new(@variant) + response.call(variant_collector) # call format block with variants collector + variant_collector.variant + end end def negotiate_format(request) @format = request.negotiate_mime(@responses.keys) end + + class VariantCollector #:nodoc: + def initialize(variant = nil) + @variant = variant + @variants = {} + end + + def any(*args, &block) + if block_given? + if args.any? && args.none?{ |a| a == @variant } + args.each{ |v| @variants[v] = block } + else + @variants[:any] = block + end + end + end + alias :all :any + + def method_missing(name, *args, &block) + @variants[name] = block if block_given? + end + + def variant + if @variant.nil? + @variants[:none] || @variants[:any] + elsif (@variants.keys & @variant).any? + @variant.each do |v| + return @variants[v] if @variants.key?(v) + end + else + @variants[:any] + end + end + end end end end diff --git a/actionpack/lib/action_controller/metal/params_wrapper.rb b/actionpack/lib/action_controller/metal/params_wrapper.rb index c9f1d8dcb4..2ca8955741 100644 --- a/actionpack/lib/action_controller/metal/params_wrapper.rb +++ b/actionpack/lib/action_controller/metal/params_wrapper.rb @@ -231,7 +231,12 @@ module ActionController # by the metal call stack. def process_action(*args) if _wrapper_enabled? - wrapped_hash = _wrap_parameters request.request_parameters + if request.parameters[_wrapper_key].present? + wrapped_hash = _extract_parameters(request.parameters) + else + wrapped_hash = _wrap_parameters request.request_parameters + end + wrapped_keys = request.request_parameters.keys wrapped_filtered_hash = _wrap_parameters request.filtered_parameters.slice(*wrapped_keys) @@ -259,14 +264,16 @@ module ActionController # Returns the list of parameters which will be selected for wrapped. def _wrap_parameters(parameters) - value = if include_only = _wrapper_options.include + { _wrapper_key => _extract_parameters(parameters) } + end + + def _extract_parameters(parameters) + if include_only = _wrapper_options.include parameters.slice(*include_only) else exclude = _wrapper_options.exclude || [] parameters.except(*(exclude + EXCLUDE_PARAMETERS)) end - - { _wrapper_key => value } end # Checks if we should perform parameters wrapping. diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb index ab14a61b97..2812038938 100644 --- a/actionpack/lib/action_controller/metal/redirecting.rb +++ b/actionpack/lib/action_controller/metal/redirecting.rb @@ -58,7 +58,7 @@ module ActionController # redirect_to post_url(@post), alert: "Watch it, mister!" # redirect_to post_url(@post), status: :found, notice: "Pay attention to the road" # redirect_to post_url(@post), status: 301, flash: { updated_post_id: @post.id } - # redirect_to { action: 'atom' }, alert: "Something serious happened" + # redirect_to({ action: 'atom' }, alert: "Something serious happened") # # When using <tt>redirect_to :back</tt>, if there is no referrer, ActionController::RedirectBackError will be raised. You may specify some fallback # behavior for this case by rescuing ActionController::RedirectBackError. diff --git a/actionpack/lib/action_controller/metal/renderers.rb b/actionpack/lib/action_controller/metal/renderers.rb index 62a3844b04..0443b73953 100644 --- a/actionpack/lib/action_controller/metal/renderers.rb +++ b/actionpack/lib/action_controller/metal/renderers.rb @@ -42,8 +42,8 @@ module ActionController nil end - # Hash of available renderers, mapping a renderer name to its proc. - # Default keys are :json, :js, :xml. + # A Set containing renderer names that correspond to available renderer procs. + # Default values are <tt>:json</tt>, <tt>:js</tt>, <tt>:xml</tt>. RENDERERS = Set.new # Adds a new renderer to call within controller actions. diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb index 66d34f3b67..93e7d6954c 100644 --- a/actionpack/lib/action_controller/metal/rendering.rb +++ b/actionpack/lib/action_controller/metal/rendering.rb @@ -2,6 +2,8 @@ module ActionController module Rendering extend ActiveSupport::Concern + RENDER_FORMATS_IN_PRIORITY = [:body, :text, :plain, :html] + # Before processing, set the request formats in current controller formats. def process_action(*) #:nodoc: self.formats = request.formats.map(&:ref).compact @@ -27,15 +29,27 @@ module ActionController end def render_to_body(options = {}) - super || options[:text].presence || ' ' + super || _render_in_priorities(options) || ' ' end private - def _process_format(format) + def _render_in_priorities(options) + RENDER_FORMATS_IN_PRIORITY.each do |format| + return options[format] if options.key?(format) + end + + nil + end + + def _process_format(format, options = {}) super - # format is a Mime::NullType instance here then this condition can't be changed to `if format` - self.content_type ||= format.to_s unless format.nil? + + if options[:plain] + self.content_type = Mime::TEXT + else + self.content_type ||= format.to_s + end end # Normalize arguments by catching blocks and setting them on :update. @@ -47,12 +61,14 @@ module ActionController # Normalize both text and status options. def _normalize_options(options) #:nodoc: - if options.key?(:text) && options[:text].respond_to?(:to_text) - options[:text] = options[:text].to_text + _normalize_text(options) + + if options[:html] + options[:html] = ERB::Util.html_escape(options[:html]) end - if options.delete(:nothing) || (options.key?(:text) && options[:text].nil?) - options[:text] = " " + if options.delete(:nothing) || _any_render_format_is_nil?(options) + options[:body] = " " end if options[:status] @@ -62,6 +78,18 @@ module ActionController super end + def _normalize_text(options) + RENDER_FORMATS_IN_PRIORITY.each do |format| + if options.key?(format) && options[format].respond_to?(:to_text) + options[format] = options[format].to_text + end + end + end + + def _any_render_format_is_nil?(options) + RENDER_FORMATS_IN_PRIORITY.any? { |format| options.key?(format) && options[format].nil? } + end + # Process controller specific options, as status, content-type and location. def _process_options(options) #:nodoc: status, content_type, location = options.values_at(:status, :content_type, :location) diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index bd64b1f812..1355fe87d0 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -5,14 +5,24 @@ module ActionController #:nodoc: class InvalidAuthenticityToken < ActionControllerError #:nodoc: end + class InvalidCrossOriginRequest < ActionControllerError #:nodoc: + end + # Controller actions are protected from Cross-Site Request Forgery (CSRF) attacks # by including a token in the rendered html for your application. This token is # stored as a random string in the session, to which an attacker does not have # access. When a request reaches your application, \Rails verifies the received # token with the token in the session. Only HTML and JavaScript requests are checked, # so this will not protect your XML API (presumably you'll have a different - # authentication scheme there anyway). Also, GET requests are not protected as these - # should be idempotent. + # authentication scheme there anyway). + # + # GET requests are not protected since they don't have side effects like writing + # to the database and don't leak sensitive information. JavaScript requests are + # an exception: a third-party site can use a <script> tag to reference a JavaScript + # URL on your site. When your JavaScript response loads on their site, it executes. + # With carefully crafted JavaScript on their end, sensitive data in your JavaScript + # response may be extracted. To prevent this, only XmlHttpRequest (known as XHR or + # Ajax) requests are allowed to make GET requests for JavaScript responses. # # It's important to remember that XML or JSON requests are also affected and if # you're building an API you'll need something like: @@ -58,6 +68,10 @@ module ActionController #:nodoc: config_accessor :allow_forgery_protection self.allow_forgery_protection = true if allow_forgery_protection.nil? + # Controls whether a CSRF failure logs a warning. On by default. + config_accessor :log_warning_on_csrf_failure + self.log_warning_on_csrf_failure = true + helper_method :form_authenticity_token helper_method :protect_against_forgery? end @@ -65,17 +79,16 @@ module ActionController #:nodoc: module ClassMethods # Turn on request forgery protection. Bear in mind that only non-GET, HTML/JavaScript requests are checked. # + # class ApplicationController < ActionController::Base + # protect_from_forgery + # end + # # class FooController < ApplicationController # protect_from_forgery except: :index # - # You can disable csrf protection on controller-by-controller basis: - # + # You can disable CSRF protection on controller by skipping the verification before_action: # skip_before_action :verify_authenticity_token # - # It can also be disabled for specific controller actions: - # - # skip_before_action :verify_authenticity_token, except: [:create] - # # Valid Options: # # * <tt>:only/:except</tt> - Passed to the <tt>before_action</tt> call. Set which actions are verified. @@ -89,6 +102,7 @@ module ActionController #:nodoc: self.forgery_protection_strategy = protection_method_class(options[:with] || :null_session) self.request_forgery_protection_token ||= :authenticity_token prepend_before_action :verify_authenticity_token, options + append_after_action :verify_same_origin_request end private @@ -169,18 +183,63 @@ module ActionController #:nodoc: end protected + # The actual before_action that is used to verify the CSRF token. + # Don't override this directly. Provide your own forgery protection + # strategy instead. If you override, you'll disable same-origin + # `<script>` verification. + # + # Lean on the protect_from_forgery declaration to mark which actions are + # due for same-origin request verification. If protect_from_forgery is + # enabled on an action, this before_action flags its after_action to + # verify that JavaScript responses are for XHR requests, ensuring they + # follow the browser's same-origin policy. + def verify_authenticity_token + mark_for_same_origin_verification! + + if !verified_request? + if logger && log_warning_on_csrf_failure + logger.warn "Can't verify CSRF token authenticity" + end + handle_unverified_request + end + end + def handle_unverified_request forgery_protection_strategy.new(self).handle_unverified_request end - # The actual before_action that is used. Modify this to change how you handle unverified requests. - def verify_authenticity_token - unless verified_request? - logger.warn "Can't verify CSRF token authenticity" if logger - handle_unverified_request + CROSS_ORIGIN_JAVASCRIPT_WARNING = "Security warning: an embedded " \ + "<script> tag on another site requested protected JavaScript. " \ + "If you know what you're doing, go ahead and disable forgery " \ + "protection on this action to permit cross-origin JavaScript embedding." + private_constant :CROSS_ORIGIN_JAVASCRIPT_WARNING + + # If `verify_authenticity_token` was run (indicating that we have + # forgery protection enabled for this request) then also verify that + # we aren't serving an unauthorized cross-origin response. + def verify_same_origin_request + if marked_for_same_origin_verification? && non_xhr_javascript_response? + logger.warn CROSS_ORIGIN_JAVASCRIPT_WARNING if logger + raise ActionController::InvalidCrossOriginRequest, CROSS_ORIGIN_JAVASCRIPT_WARNING end end + # GET requests are checked for cross-origin JavaScript after rendering. + def mark_for_same_origin_verification! + @marked_for_same_origin_verification = request.get? + end + + # If the `verify_authenticity_token` before_action ran, verify that + # JavaScript responses are only served to same-origin GET requests. + def marked_for_same_origin_verification? + @marked_for_same_origin_verification ||= false + end + + # Check for cross-origin JavaScript responses. + def non_xhr_javascript_response? + content_type =~ %r(\Atext/javascript) && !request.xhr? + end + # Returns true or false if a request is verified. Checks: # # * is it a GET or HEAD request? Gets should be safe and idempotent @@ -188,7 +247,7 @@ module ActionController #:nodoc: # * Does the X-CSRF-Token header match the form_authenticity_token def verified_request? !protect_against_forgery? || request.get? || request.head? || - form_authenticity_token == params[request_forgery_protection_token] || + form_authenticity_token == form_authenticity_param || form_authenticity_token == request.headers['X-CSRF-Token'] end diff --git a/actionpack/lib/action_controller/metal/responder.rb b/actionpack/lib/action_controller/metal/responder.rb index b4ba169e8f..5096558c67 100644 --- a/actionpack/lib/action_controller/metal/responder.rb +++ b/actionpack/lib/action_controller/metal/responder.rb @@ -22,7 +22,7 @@ module ActionController #:nodoc: # # 3) if the responder does not <code>respond_to :to_xml</code>, call <code>#to_format</code> on it. # - # === Builtin HTTP verb semantics + # === Built-in HTTP verb semantics # # The default \Rails responder holds semantics for each HTTP verb. Depending on the # content type, verb and the resource status, it will behave differently. @@ -270,7 +270,7 @@ module ActionController #:nodoc: resource.respond_to?(:errors) && !resource.errors.empty? end - # Check whether the neceessary Renderer is available + # Check whether the necessary Renderer is available def has_renderer? Renderers::RENDERERS.include?(format) end diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index b4948d99a8..d86d49c9dc 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -3,6 +3,7 @@ require 'active_support/core_ext/array/wrap' require 'active_support/rescuable' require 'action_dispatch/http/upload' require 'stringio' +require 'set' module ActionController # Raised when a required parameter is missing. @@ -31,7 +32,7 @@ module ActionController def initialize(params) # :nodoc: @params = params - super("found unpermitted parameters: #{params.join(", ")}") + super("found unpermitted parameter#{'s' if params.size > 1 }: #{params.join(", ")}") end end @@ -125,6 +126,13 @@ module ActionController @permitted = self.class.permit_all_parameters end + # Attribute that keeps track of converted arrays, if any, to avoid double + # looping in the common use case permit + mass-assignment. Defined in a + # method to instantiate it only if needed. + def converted_arrays + @converted_arrays ||= Set.new + end + # Returns +true+ if the parameter is permitted, +false+ otherwise. # # params = ActionController::Parameters.new @@ -149,8 +157,10 @@ module ActionController # Person.new(params) # => #<Person id: nil, name: "Francesco"> def permit! each_pair do |key, value| - convert_hashes_to_parameters(key, value) - self[key].permit! if self[key].respond_to? :permit! + value = convert_hashes_to_parameters(key, value) + Array.wrap(value).each do |_| + _.permit! if _.respond_to? :permit! + end end @permitted = true @@ -284,14 +294,7 @@ module ActionController # params.fetch(:none, 'Francesco') # => "Francesco" # params.fetch(:none) { 'Francesco' } # => "Francesco" def fetch(key, *args) - value = super - # Don't rely on +convert_hashes_to_parameters+ - # so as to not mutate via a +fetch+ - if value.is_a?(Hash) - value = self.class.new(value) - value.permit! if permitted? - end - value + convert_hashes_to_parameters(key, super, false) rescue KeyError raise ActionController::ParameterMissing.new(key) end @@ -329,12 +332,21 @@ module ActionController end private - def convert_hashes_to_parameters(key, value) - if value.is_a?(Parameters) || !value.is_a?(Hash) + def convert_hashes_to_parameters(key, value, assign_if_converted=true) + converted = convert_value_to_parameters(value) + self[key] = converted if assign_if_converted && !converted.equal?(value) + converted + end + + def convert_value_to_parameters(value) + if value.is_a?(Array) && !converted_arrays.member?(value) + converted = value.map { |_| convert_value_to_parameters(_) } + converted_arrays << converted + converted + elsif value.is_a?(Parameters) || !value.is_a?(Hash) value else - # Convert to Parameters on first access - self[key] = self.class.new(value) + self.class.new(value) end end @@ -490,7 +502,7 @@ module ActionController # end # end # - # In order to use <tt>accepts_nested_attribute_for</tt> with Strong \Parameters, you + # In order to use <tt>accepts_nested_attributes_for</tt> with Strong \Parameters, you # will need to specify which nested attributes should be whitelisted. # # class Person diff --git a/actionpack/lib/action_controller/metal/testing.rb b/actionpack/lib/action_controller/metal/testing.rb index 0377b8c4cf..dd8da4b5dc 100644 --- a/actionpack/lib/action_controller/metal/testing.rb +++ b/actionpack/lib/action_controller/metal/testing.rb @@ -17,7 +17,6 @@ module ActionController def recycle! @_url_options = nil - self.response_body = nil self.formats = nil self.params = nil end diff --git a/actionpack/lib/action_controller/metal/url_for.rb b/actionpack/lib/action_controller/metal/url_for.rb index 754249cbc8..37d4a96ee1 100644 --- a/actionpack/lib/action_controller/metal/url_for.rb +++ b/actionpack/lib/action_controller/metal/url_for.rb @@ -30,9 +30,9 @@ module ActionController :_recall => request.symbolized_path_parameters ).freeze - if (same_origin = _routes.equal?(env["action_dispatch.routes"])) || + if (same_origin = _routes.equal?(env["action_dispatch.routes".freeze])) || (script_name = env["ROUTES_#{_routes.object_id}_SCRIPT_NAME"]) || - (original_script_name = env['ORIGINAL_SCRIPT_NAME']) + (original_script_name = env['ORIGINAL_SCRIPT_NAME'.freeze]) @_url_options.dup.tap do |options| if original_script_name diff --git a/actionpack/lib/action_controller/railtie.rb b/actionpack/lib/action_controller/railtie.rb index 0833e65d23..a2fc814221 100644 --- a/actionpack/lib/action_controller/railtie.rb +++ b/actionpack/lib/action_controller/railtie.rb @@ -3,6 +3,7 @@ require "action_controller" require "action_dispatch/railtie" require "abstract_controller/railties/routes_helpers" require "action_controller/railties/helpers" +require "action_view/railtie" module ActionController class Railtie < Rails::Railtie #:nodoc: diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index 5ed3d2ebc1..c6a8f581de 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -17,8 +17,9 @@ module ActionController @_templates = Hash.new(0) @_layouts = Hash.new(0) @_files = Hash.new(0) + @_subscribers = [] - ActiveSupport::Notifications.subscribe("render_template.action_view") do |_name, _start, _finish, _id, payload| + @_subscribers << ActiveSupport::Notifications.subscribe("render_template.action_view") do |_name, _start, _finish, _id, payload| path = payload[:layout] if path @_layouts[path] += 1 @@ -28,7 +29,7 @@ module ActionController end end - ActiveSupport::Notifications.subscribe("!render_template.action_view") do |_name, _start, _finish, _id, payload| + @_subscribers << ActiveSupport::Notifications.subscribe("!render_template.action_view") do |_name, _start, _finish, _id, payload| path = payload[:virtual_path] next unless path partial = path =~ /^.*\/_[^\/]*$/ @@ -41,7 +42,7 @@ module ActionController @_templates[path] += 1 end - ActiveSupport::Notifications.subscribe("!render_template.action_view") do |_name, _start, _finish, _id, payload| + @_subscribers << ActiveSupport::Notifications.subscribe("!render_template.action_view") do |_name, _start, _finish, _id, payload| next if payload[:virtual_path] # files don't have virtual path path = payload[:identifier] @@ -53,8 +54,9 @@ module ActionController end def teardown_subscriptions - ActiveSupport::Notifications.unsubscribe("render_template.action_view") - ActiveSupport::Notifications.unsubscribe("!render_template.action_view") + @_subscribers.each do |subscriber| + ActiveSupport::Notifications.unsubscribe(subscriber) + end end def process(*args) @@ -213,6 +215,9 @@ module ActionController # Clear the combined params hash in case it was already referenced. @env.delete("action_dispatch.request.parameters") + # Clear the filter cache variables so they're not stale + @filtered_parameters = @filtered_env = @filtered_path = nil + params = self.request_parameters.dup %w(controller action only_path).each do |k| params.delete(k) @@ -255,6 +260,29 @@ module ActionController end end + class LiveTestResponse < Live::Response + def recycle! + @body = nil + initialize + end + + def body + @body ||= super + end + + # Was the response successful? + alias_method :success?, :successful? + + # Was the URL not found? + alias_method :missing?, :not_found? + + # Were we redirected? + alias_method :redirect?, :redirection? + + # Was there a server-side error? + alias_method :error?, :server_error? + end + # Methods #destroy and #load! are overridden to avoid calling methods on the # @store object, which does not exist for the TestSession class. class TestSession < Rack::Session::Abstract::SessionHash #:nodoc: @@ -460,8 +488,8 @@ module ActionController # - +session+: A hash of parameters to store in the session. This may be +nil+. # - +flash+: A hash of parameters to store in the flash. This may be +nil+. # - # You can also simulate POST, PATCH, PUT, DELETE, HEAD, and OPTIONS requests with - # +post+, +patch+, +put+, +delete+, +head+, and +options+. + # You can also simulate POST, PATCH, PUT, DELETE, and HEAD requests with + # +post+, +patch+, +put+, +delete+, and +head+. # # Note that the request method is not verified. The different methods are # available to make the tests more expressive. @@ -522,6 +550,31 @@ module ActionController end end + # Simulate a HTTP request to +action+ by specifying request method, + # parameters and set/volley the response. + # + # - +action+: The controller action to call. + # - +http_method+: Request method used to send the http request. Possible values + # are +GET+, +POST+, +PATCH+, +PUT+, +DELETE+, +HEAD+. Defaults to +GET+. + # - +parameters+: The HTTP parameters. This may be +nil+, a hash, or a + # string that is appropriately encoded (+application/x-www-form-urlencoded+ + # or +multipart/form-data+). + # - +session+: A hash of parameters to store in the session. This may be +nil+. + # - +flash+: A hash of parameters to store in the flash. This may be +nil+. + # + # Example calling +create+ action and sending two params: + # + # process :create, 'POST', user: { name: 'Gaurish Sharma', email: 'user@example.com' } + # + # Example sending parameters, +nil+ session and setting a flash message: + # + # process :view, 'GET', { id: 7 }, nil, { notice: 'This is flash message' } + # + # To simulate +GET+, +POST+, +PATCH+, +PUT+, +DELETE+ and +HEAD+ requests + # prefer using #get, #post, #patch, #put, #delete and #head methods + # respectively which will make tests more expressive. + # + # Note that the request method is not verified. def process(action, http_method = 'GET', *args) check_required_ivars @@ -565,10 +618,13 @@ module ActionController name = @request.parameters[:action] + @controller.recycle! @controller.process(name) if cookies = @request.env['action_dispatch.cookies'] - cookies.write(@response) + unless @response.committed? + cookies.write(@response) + end end @response.prepare! @@ -579,13 +635,14 @@ module ActionController end def setup_controller_request_and_response - @request = build_request - @response = build_response - @response.request = @request - @controller = nil unless defined? @controller + response_klass = TestResponse + if klass = self.class.controller_class + if klass < ActionController::Live + response_klass = LiveTestResponse + end unless @controller begin @controller = klass.new @@ -595,6 +652,10 @@ module ActionController end end + @request = build_request + @response = build_response response_klass + @response.request = @request + if @controller @controller.request = @request @controller.params = {} @@ -605,8 +666,8 @@ module ActionController TestRequest.new end - def build_response - TestResponse.new + def build_response(klass) + klass.new end included do |