diff options
Diffstat (limited to 'actionpack/lib/action_controller/metal')
15 files changed, 487 insertions, 123 deletions
diff --git a/actionpack/lib/action_controller/metal/flash.rb b/actionpack/lib/action_controller/metal/flash.rb index b078beb675..65351284b9 100644 --- a/actionpack/lib/action_controller/metal/flash.rb +++ b/actionpack/lib/action_controller/metal/flash.rb @@ -11,6 +11,23 @@ module ActionController #:nodoc: end module ClassMethods + # Creates new flash types. You can pass as many types as you want to create + # flash types other than the default <tt>alert</tt> and <tt>notice</tt> in + # your controllers and views. For instance: + # + # # in application_controller.rb + # class ApplicationController < ActionController::Base + # add_flash_types :warning + # end + # + # # in your controller + # redirect_to user_path(@user), warning: "Incomplete profile" + # + # # in your view + # <%= warning %> + # + # This method will automatically define a new method for each of the given + # names, and it will be available in your views. def add_flash_types(*types) types.each do |type| next if _flash_types.include?(type) @@ -20,7 +37,7 @@ module ActionController #:nodoc: end helper_method type - _flash_types << type + self._flash_types += [type] end end end diff --git a/actionpack/lib/action_controller/metal/force_ssl.rb b/actionpack/lib/action_controller/metal/force_ssl.rb index f1e8714a86..a2cb6d1e66 100644 --- a/actionpack/lib/action_controller/metal/force_ssl.rb +++ b/actionpack/lib/action_controller/metal/force_ssl.rb @@ -1,3 +1,6 @@ +require 'active_support/core_ext/hash/except' +require 'active_support/core_ext/hash/slice' + module ActionController # This module provides a method which will redirect browser to use HTTPS # protocol. This will ensure that user's sensitive information will be @@ -14,6 +17,10 @@ module ActionController extend ActiveSupport::Concern include AbstractController::Callbacks + ACTION_OPTIONS = [:only, :except, :if, :unless] + URL_OPTIONS = [:protocol, :host, :domain, :subdomain, :port, :path] + REDIRECT_OPTIONS = [:status, :flash, :alert, :notice] + module ClassMethods # Force the request to this particular controller or specified actions to be # under HTTPS protocol. @@ -29,18 +36,34 @@ module ActionController # end # end # - # ==== Options - # * <tt>host</tt> - Redirect to a different host name - # * <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>if</tt> - A symbol naming an instance method or a proc; the callback - # will be called only when it returns a true value. - # * <tt>unless</tt> - A symbol naming an instance method or a proc; the callback - # will be called only when it returns a false value. + # ==== URL Options + # You can pass any of the following options to affect the redirect url + # * <tt>host</tt> - Redirect to a different host name + # * <tt>subdomain</tt> - Redirect to a different subdomain + # * <tt>domain</tt> - Redirect to a different domain + # * <tt>port</tt> - Redirect to a non-standard port + # * <tt>path</tt> - Redirect to a different path + # + # ==== Redirect Options + # You can pass any of the following options to affect the redirect status and response + # * <tt>status</tt> - Redirect with a custom status (default is 301 Moved Permanently) + # * <tt>flash</tt> - Set a flash message when redirecting + # * <tt>alert</tt> - Set an alert message when redirecting + # * <tt>notice</tt> - Set a notice message when redirecting + # + # ==== Action Options + # You can pass any of the following options to affect the before_action callback + # * <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>if</tt> - A symbol naming an instance method or a proc; the callback + # will be called only when it returns a true value. + # * <tt>unless</tt> - A symbol naming an instance method or a proc; the callback + # will be called only when it returns a false value. def force_ssl(options = {}) - host = options.delete(:host) - before_action(options) do - force_ssl_redirect(host) + action_options = options.slice(*ACTION_OPTIONS) + redirect_options = options.except(*ACTION_OPTIONS) + before_action(action_options) do + force_ssl_redirect(redirect_options) end end end @@ -48,14 +71,26 @@ module ActionController # Redirect the existing request to use the HTTPS protocol. # # ==== Parameters - # * <tt>host</tt> - Redirect to a different host name - def force_ssl_redirect(host = nil) + # * <tt>host_or_options</tt> - Either a host name or any of the url & redirect options + # available to the <tt>force_ssl</tt> method. + def force_ssl_redirect(host_or_options = nil) unless request.ssl? - redirect_options = {:protocol => 'https://', :status => :moved_permanently} - redirect_options.merge!(:host => host) if host - redirect_options.merge!(:params => request.query_parameters) + options = { + :protocol => 'https://', + :host => request.host, + :path => request.fullpath, + :status => :moved_permanently + } + + if host_or_options.is_a?(Hash) + options.merge!(host_or_options) + elsif host_or_options + options.merge!(:host => host_or_options) + end + + secure_url = ActionDispatch::Http::URL.url_for(options.slice(*URL_OPTIONS)) flash.keep if respond_to?(:flash) - redirect_to redirect_options + redirect_to secure_url, options.slice(*REDIRECT_OPTIONS) end end end diff --git a/actionpack/lib/action_controller/metal/head.rb b/actionpack/lib/action_controller/metal/head.rb index 8237db15ca..43407f5b78 100644 --- a/actionpack/lib/action_controller/metal/head.rb +++ b/actionpack/lib/action_controller/metal/head.rb @@ -1,8 +1,6 @@ module ActionController module Head - extend ActiveSupport::Concern - - # Return a response that has no content (merely headers). The options + # Returns a response that has no content (merely headers). The options # argument is interpreted to be a hash of header names and values. # This allows you to easily return a response that consists only of # significant headers: diff --git a/actionpack/lib/action_controller/metal/helpers.rb b/actionpack/lib/action_controller/metal/helpers.rb index 35facd13c8..a9c3e438fb 100644 --- a/actionpack/lib/action_controller/metal/helpers.rb +++ b/actionpack/lib/action_controller/metal/helpers.rb @@ -5,7 +5,7 @@ module ActionController # # In addition to using the standard template helpers provided, creating custom helpers to # extract complicated logic or reusable functionality is strongly encouraged. By default, each controller - # will include all helpers. + # will include all helpers. These helpers are only accessible on the controller through <tt>.helpers</tt> # # In previous versions of \Rails the controller will include a helper whose # name matches that of the controller, e.g., <tt>MyController</tt> will automatically @@ -73,7 +73,11 @@ module ActionController # Provides a proxy to access helpers methods from outside the view. def helpers - @helper_proxy ||= ActionView::Base.new.extend(_helpers) + @helper_proxy ||= begin + proxy = ActionView::Base.new + proxy.config = config.inheritable_copy + proxy.extend(_helpers) + end end # Overwrite modules_for_helpers to accept :all as argument, which loads @@ -94,7 +98,6 @@ module ActionController extract = /^#{Regexp.quote(_path.to_s)}\/?(.*)_helper.rb$/ names = Dir["#{_path}/**/*_helper.rb"].map { |file| file.sub(extract, '\1') } names.sort! - names end helpers.uniq! helpers 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 fb664a69dd..b7d1334dd3 100644 --- a/actionpack/lib/action_controller/metal/live.rb +++ b/actionpack/lib/action_controller/metal/live.rb @@ -1,5 +1,6 @@ require 'action_dispatch/http/response' require 'delegate' +require 'active_support/json' module ActionController # Mix this module in to your controller, and all actions in that controller @@ -32,8 +33,82 @@ module ActionController # the main thread. Make sure your actions are thread safe, and this shouldn't # be a problem (don't share state across threads, etc). module Live + # This class provides the ability to write an SSE (Server Sent Event) + # to an IO stream. The class is initialized with a stream and can be used + # to either write a JSON string or an object which can be converted to JSON. + # + # Writing an object will convert it into standard SSE format with whatever + # options you have configured. You may choose to set the following options: + # + # 1) Event. If specified, an event with this name will be dispatched on + # the browser. + # 2) Retry. The reconnection time in milliseconds used when attempting + # to send the event. + # 3) Id. If the connection dies while sending an SSE to the browser, then + # the server will receive a +Last-Event-ID+ header with value equal to +id+. + # + # After setting an option in the constructor of the SSE object, all future + # SSEs sent across the stream will use those options unless overridden. + # + # Example Usage: + # + # class MyController < ActionController::Base + # include ActionController::Live + # + # def index + # response.headers['Content-Type'] = 'text/event-stream' + # sse = SSE.new(response.stream, retry: 300, event: "event-name") + # sse.write({ name: 'John'}) + # sse.write({ name: 'John'}, id: 10) + # sse.write({ name: 'John'}, id: 10, event: "other-event") + # sse.write({ name: 'John'}, id: 10, event: "other-event", retry: 500) + # ensure + # sse.close + # end + # end + # + # Note: SSEs are not currently supported by IE. However, they are supported + # by Chrome, Firefox, Opera, and Safari. + class SSE + + WHITELISTED_OPTIONS = %w( retry event id ) + + def initialize(stream, options = {}) + @stream = stream + @options = options + end + + def close + @stream.close + end + + def write(object, options = {}) + case object + when String + perform_write(object, options) + else + perform_write(ActiveSupport::JSON.encode(object), options) + end + end + + private + + def perform_write(json, options) + current_options = @options.merge(options).stringify_keys + + WHITELISTED_OPTIONS.each do |option_name| + if (option_value = current_options[option_name]) + @stream.write "#{option_name}: #{option_value}\n" + end + end + + @stream.write "data: #{json}\n\n" + end + end + class Buffer < ActionDispatch::Response::Buffer #:nodoc: def initialize(response) + @error_callback = nil super(response, SizedQueue.new(10)) end @@ -56,6 +131,14 @@ module ActionController super @buf.push nil end + + def on_error(&block) + @error_callback = block + end + + def call_on_error + @error_callback.call + end end class Response < ActionDispatch::Response #:nodoc: all @@ -121,6 +204,16 @@ 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 + end ensure @_response.commit! end @@ -129,6 +222,16 @@ module ActionController @_response.await_commit end + def log_error(exception) + logger = ActionController::Base.logger + return unless logger + + message = "\n#{exception.class} (#{exception.message}):\n" + message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code) + message << " " << exception.backtrace.join("\n ") + logger.fatal("#{message}\n\n") + end + def response_body=(body) super response.stream.close if response diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index 834d44f045..d5e08b7034 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -181,6 +181,61 @@ 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 + # # 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 +315,7 @@ module ActionController #:nodoc: # * for other requests - i.e. data formats such as xml, json, csv etc, if # the resource passed to +respond_with+ responds to <code>to_<format></code>, # the method attempts to render the resource in the requested format - # directly, e.g. for an xml request, the response is equivalent to calling + # directly, e.g. for an xml request, the response is equivalent to calling # <code>render xml: resource</code>. # # === Nested resources @@ -321,11 +376,14 @@ module ActionController #:nodoc: # 2. <tt>:action</tt> - overwrites the default render action used after an # unsuccessful html +post+ request. def respond_with(*resources, &block) - raise "In order to use respond_with, first you need to declare the formats your " \ - "controller responds to in the class level" if self.class.mimes_for_respond_to.empty? + if self.class.mimes_for_respond_to.empty? + raise "In order to use respond_with, first you need to declare the " \ + "formats your controller responds to in the class level." + end if collector = retrieve_collector_from_mimes(&block) options = resources.size == 1 ? {} : resources.extract_options! + options = options.clone options[:default_response] = collector.response (options.delete(:responder) || self.class.responder).call(self, resources, options) end @@ -359,14 +417,12 @@ module ActionController #:nodoc: # is available. def retrieve_collector_from_mimes(mimes=nil, &block) #:nodoc: mimes ||= collect_mimes_from_class_level - collector = Collector.new(mimes) + collector = Collector.new(mimes, request.variant) block.call(collector) if block_given? format = collector.negotiate_format(request) if format - self.content_type ||= format.to_s - lookup_context.formats = [format.to_sym] - lookup_context.rendered_format = lookup_context.formats.first + _process_format(format) collector else raise ActionController::UnknownFormat @@ -397,11 +453,13 @@ module ActionController #:nodoc: # request, with this response then being accessible by calling #response. class Collector include AbstractController::Collector - attr_accessor :order, :format + attr_accessor :format + + def initialize(mimes, variant = nil) + @responses = {} + @variant = variant - def initialize(mimes) - @order, @responses = [], {} - mimes.each { |mime| send(mime) } + mimes.each { |mime| @responses["Mime::#{mime.upcase}".constantize] = nil } end def any(*args, &block) @@ -415,16 +473,62 @@ module ActionController #:nodoc: def custom(mime_type, &block) mime_type = Mime::Type.lookup(mime_type.to_s) unless mime_type.is_a?(Mime::Type) - @order << mime_type - @responses[mime_type] ||= block + @responses[mime_type] ||= if block_given? + block + else + VariantCollector.new(@variant) + end end def response - @responses.fetch(format, @responses[Mime::ALL]) + response = @responses.fetch(format, @responses[Mime::ALL]) + if response.is_a?(VariantCollector) # `format.html.phone` - variant inline syntax + response.variant + elsif response.nil? || response.arity == 0 # `format.html` - just a format, call its block + response + else # `format.html{ |variant| variant.phone }` - variant block syntax + variant_collector = VariantCollector.new(@variant) + response.call(variant_collector) #call format block with variants collector + variant_collector.variant + end end def negotiate_format(request) - @format = request.negotiate_mime(order) + @format = request.negotiate_mime(@responses.keys) + end + + class VariantCollector #:nodoc: + def initialize(variant = nil) + @variant = variant + @variants = {} + end + + def any(*args, &block) + if block_given? + if args.any? && args.none?{ |a| a == @variant } + args.each{ |v| @variants[v] = block } + else + @variants[:any] = block + end + end + end + alias :all :any + + def method_missing(name, *args, &block) + @variants[name] = block if block_given? + end + + def variant + key = if @variant.nil? + :none + elsif @variants.has_key?(@variant) + @variant + else + :any + end + + @variants[key] + end end end end diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb index e9031f3fac..2812038938 100644 --- a/actionpack/lib/action_controller/metal/redirecting.rb +++ b/actionpack/lib/action_controller/metal/redirecting.rb @@ -58,7 +58,7 @@ module ActionController # redirect_to post_url(@post), alert: "Watch it, mister!" # redirect_to post_url(@post), status: :found, notice: "Pay attention to the road" # redirect_to post_url(@post), status: 301, flash: { updated_post_id: @post.id } - # redirect_to { action: 'atom' }, alert: "Something serious happened" + # redirect_to({ action: 'atom' }, alert: "Something serious happened") # # When using <tt>redirect_to :back</tt>, if there is no referrer, ActionController::RedirectBackError will be raised. You may specify some fallback # behavior for this case by rescuing ActionController::RedirectBackError. @@ -71,6 +71,26 @@ module ActionController self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.h(location)}\">redirected</a>.</body></html>" end + def _compute_redirect_to_location(options) #:nodoc: + case options + # The scheme name consist of a letter followed by any combination of + # letters, digits, and the plus ("+"), period ("."), or hyphen ("-") + # characters; and is terminated by a colon (":"). + # See http://tools.ietf.org/html/rfc3986#section-3.1 + # The protocol relative scheme starts with a double slash "//". + when /\A([a-z][a-z\d\-+\.]*:|\/\/).*/i + options + when String + request.protocol + request.host_with_port + options + when :back + request.headers["Referer"] or raise RedirectBackError + when Proc + _compute_redirect_to_location options.call + else + url_for(options) + end.delete("\0\r\n") + end + private def _extract_redirect_to_status(options, response_status) if options.is_a?(Hash) && options.key?(:status) @@ -81,24 +101,5 @@ module ActionController 302 end end - - def _compute_redirect_to_location(options) - case options - # The scheme name consist of a letter followed by any combination of - # letters, digits, and the plus ("+"), period ("."), or hyphen ("-") - # characters; and is terminated by a colon (":"). - # The protocol relative scheme starts with a double slash "//" - when %r{\A(\w[\w+.-]*:|//).*} - options - when String - request.protocol + request.host_with_port + options - when :back - request.headers["Referer"] or raise RedirectBackError - when Proc - _compute_redirect_to_location options.call - else - url_for(options) - end.delete("\0\r\n") - end end end diff --git a/actionpack/lib/action_controller/metal/renderers.rb b/actionpack/lib/action_controller/metal/renderers.rb index 5272dc6cdb..6c7b4652d4 100644 --- a/actionpack/lib/action_controller/metal/renderers.rb +++ b/actionpack/lib/action_controller/metal/renderers.rb @@ -6,6 +6,12 @@ module ActionController Renderers.add(key, &block) end + class MissingRenderer < LoadError + def initialize(format) + super "No renderer defined for format: #{format}" + end + end + module Renderers extend ActiveSupport::Concern @@ -37,7 +43,7 @@ module ActionController end # Hash of available renderers, mapping a renderer name to its proc. - # Default keys are :json, :js, :xml. + # Default keys 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 c5e7d4e357..5c48b4ab98 100644 --- a/actionpack/lib/action_controller/metal/rendering.rb +++ b/actionpack/lib/action_controller/metal/rendering.rb @@ -2,39 +2,41 @@ module ActionController module Rendering extend ActiveSupport::Concern - include AbstractController::Rendering - # Before processing, set the request formats in current controller formats. def process_action(*) #:nodoc: - self.formats = request.formats.map { |x| x.ref } + self.formats = request.formats.map(&:ref).compact super end # Check for double render errors and set the content_type after rendering. def render(*args) #:nodoc: - raise ::AbstractController::DoubleRenderError if response_body + raise ::AbstractController::DoubleRenderError if self.response_body super - self.content_type ||= Mime[lookup_context.rendered_format].to_s - response_body end # Overwrite render_to_string because body can now be set to a rack body. def render_to_string(*) - if self.response_body = super + result = super + if result.respond_to?(:each) string = "" - response_body.each { |r| string << r } + result.each { |r| string << r } string + else + result end - ensure - self.response_body = nil end - def render_to_body(*) - super || " " + def render_to_body(options = {}) + super || options[:text].presence || ' ' end private + def _process_format(format) + super + self.content_type ||= format.to_s + end + # Normalize arguments by catching blocks and setting them on :update. def _normalize_args(action=nil, options={}, &blk) #:nodoc: options = super diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index d275a854fd..c88074d4c6 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: @@ -65,17 +75,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 +98,7 @@ module ActionController #:nodoc: self.forgery_protection_strategy = protection_method_class(options[:with] || :null_session) self.request_forgery_protection_token ||= :authenticity_token prepend_before_action :verify_authenticity_token, options + append_after_action :verify_same_origin_request end private @@ -124,6 +134,9 @@ module ActionController #:nodoc: @loaded = true end + # no-op + def destroy; end + def exists? true end @@ -166,18 +179,61 @@ 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? + logger.warn "Can't verify CSRF token authenticity" if logger + 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 @@ -199,6 +255,7 @@ module ActionController #:nodoc: params[request_forgery_protection_token] end + # Checks if the controller allows forgery protection. def protect_against_forgery? allow_forgery_protection end diff --git a/actionpack/lib/action_controller/metal/responder.rb b/actionpack/lib/action_controller/metal/responder.rb index 891819968b..e24b56fa91 100644 --- a/actionpack/lib/action_controller/metal/responder.rb +++ b/actionpack/lib/action_controller/metal/responder.rb @@ -97,8 +97,12 @@ module ActionController #:nodoc: # # This will return status 201 if the task was saved successfully. If not, # it will simply ignore the given options and return status 422 and the - # resource errors. To customize the failure scenario, you can pass a - # a block to <code>respond_with</code>: + # resource errors. You can also override the location to redirect to: + # + # respond_with(@project, location: root_path) + # + # To customize the failure scenario, you can pass a block to + # <code>respond_with</code>: # # def create # @project = Project.find(params[:project_id]) @@ -140,7 +144,7 @@ module ActionController #:nodoc: undef_method(:to_json) if method_defined?(:to_json) undef_method(:to_yaml) if method_defined?(:to_yaml) - # Initializes a new responder an invoke the proper format. If the format is + # Initializes a new responder and invokes the proper format. If the format is # not defined, call to_format. # def self.call(*args) @@ -198,6 +202,7 @@ module ActionController #:nodoc: # This is the common behavior for formats associated with APIs, such as :xml and :json. def api_behavior(error) raise error unless resourceful? + raise MissingRenderer.new(format) unless has_renderer? if get? display resource @@ -265,6 +270,11 @@ module ActionController #:nodoc: resource.respond_to?(:errors) && !resource.errors.empty? end + # Check whether the necessary Renderer is available + def has_renderer? + Renderers::RENDERERS.include?(format) + end + # By default, render the <code>:edit</code> action for HTML requests with errors, unless # the verb was POST. # diff --git a/actionpack/lib/action_controller/metal/streaming.rb b/actionpack/lib/action_controller/metal/streaming.rb index 73e9b5660d..62d5931b45 100644 --- a/actionpack/lib/action_controller/metal/streaming.rb +++ b/actionpack/lib/action_controller/metal/streaming.rb @@ -193,31 +193,29 @@ module ActionController #:nodoc: module Streaming extend ActiveSupport::Concern - include AbstractController::Rendering - protected - # Set proper cache control and transfer encoding when streaming - def _process_options(options) #:nodoc: - super - if options[:stream] - if env["HTTP_VERSION"] == "HTTP/1.0" - options.delete(:stream) - else - headers["Cache-Control"] ||= "no-cache" - headers["Transfer-Encoding"] = "chunked" - headers.delete("Content-Length") + # Set proper cache control and transfer encoding when streaming + def _process_options(options) #:nodoc: + super + if options[:stream] + if env["HTTP_VERSION"] == "HTTP/1.0" + options.delete(:stream) + else + headers["Cache-Control"] ||= "no-cache" + headers["Transfer-Encoding"] = "chunked" + headers.delete("Content-Length") + end end end - end - # Call render_body if we are streaming instead of usual +render+. - def _render_template(options) #:nodoc: - if options.delete(:stream) - Rack::Chunked::Body.new view_renderer.render_body(view_context, options) - else - super + # Call render_body if we are streaming instead of usual +render+. + def _render_template(options) #:nodoc: + if options.delete(:stream) + Rack::Chunked::Body.new view_renderer.render_body(view_context, options) + else + super + end end - end end end diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index 23d70c9ea2..48a916f2b1 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -3,6 +3,7 @@ require 'active_support/core_ext/array/wrap' require 'active_support/rescuable' require 'action_dispatch/http/upload' require 'stringio' +require 'set' module ActionController # Raised when a required parameter is missing. @@ -17,7 +18,7 @@ module ActionController def initialize(param) # :nodoc: @param = param - super("param not found: #{param}") + super("param is missing or the value is empty: #{param}") end end @@ -125,6 +126,13 @@ module ActionController @permitted = self.class.permit_all_parameters end + # Attribute that keeps track of converted arrays, if any, to avoid double + # looping in the common use case permit + mass-assignment. Defined in a + # method to instantiate it only if needed. + def converted_arrays + @converted_arrays ||= Set.new + end + # Returns +true+ if the parameter is permitted, +false+ otherwise. # # params = ActionController::Parameters.new @@ -149,8 +157,10 @@ module ActionController # Person.new(params) # => #<Person id: nil, name: "Francesco"> def permit! each_pair do |key, value| - convert_hashes_to_parameters(key, value) - self[key].permit! if self[key].respond_to? :permit! + value = convert_hashes_to_parameters(key, value) + Array.wrap(value).each do |_| + _.permit! if _.respond_to? :permit! + end end @permitted = true @@ -201,6 +211,7 @@ module ActionController # You may declare that the parameter should be an array of permitted scalars # by mapping it to an empty array: # + # params = ActionController::Parameters.new(tags: ['rails', 'parameters']) # params.permit(tags: []) # # You can also use +permit+ on nested parameters, like: @@ -230,7 +241,7 @@ module ActionController # params = ActionController::Parameters.new({ # person: { # contact: { - # email: 'none@test.com' + # email: 'none@test.com', # phone: '555-1234' # } # } @@ -283,7 +294,7 @@ module ActionController # params.fetch(:none, 'Francesco') # => "Francesco" # params.fetch(:none) { 'Francesco' } # => "Francesco" def fetch(key, *args) - convert_hashes_to_parameters(key, super) + convert_hashes_to_parameters(key, super, false) rescue KeyError raise ActionController::ParameterMissing.new(key) end @@ -297,7 +308,7 @@ module ActionController # params.slice(:d) # => {} def slice(*keys) self.class.new(super).tap do |new_instance| - new_instance.instance_variable_set :@permitted, @permitted + new_instance.permitted = @permitted end end @@ -311,24 +322,38 @@ module ActionController # copy_params.permitted? # => true def dup super.tap do |duplicate| - duplicate.instance_variable_set :@permitted, @permitted + duplicate.permitted = @permitted end end + protected + def permitted=(new_permitted) + @permitted = new_permitted + end + private - def convert_hashes_to_parameters(key, value) - if value.is_a?(Parameters) || !value.is_a?(Hash) + def convert_hashes_to_parameters(key, value, assign_if_converted=true) + converted = convert_value_to_parameters(value) + self[key] = converted if assign_if_converted && !converted.equal?(value) + converted + end + + def convert_value_to_parameters(value) + if value.is_a?(Array) && !converted_arrays.member?(value) + converted = value.map { |_| convert_value_to_parameters(_) } + converted_arrays << converted + converted + elsif value.is_a?(Parameters) || !value.is_a?(Hash) value else - # Convert to Parameters on first access - self[key] = self.class.new(value) + self.class.new(value) end end def each_element(object) if object.is_a?(Array) object.map { |el| yield el }.compact - elsif object.is_a?(Hash) && object.keys.all? { |k| k =~ /\A-?\d+\z/ } + elsif fields_for_style?(object) hash = object.class.new object.each { |k,v| hash[k] = yield v } hash @@ -337,6 +362,10 @@ module ActionController end end + def fields_for_style?(object) + object.is_a?(Hash) && object.all? { |k, v| k =~ /\A-?\d+\z/ && v.is_a?(Hash) } + end + def unpermitted_parameters!(params) unpermitted_keys = unpermitted_keys(params) if unpermitted_keys.any? @@ -415,7 +444,7 @@ module ActionController # Slicing filters out non-declared keys. slice(*filter.keys).each do |key, value| - return unless value + next unless value if filter[key] == EMPTY_ARRAY # Declaration { comment_ids: [] }. diff --git a/actionpack/lib/action_controller/metal/url_for.rb b/actionpack/lib/action_controller/metal/url_for.rb index 505f3b4e61..754249cbc8 100644 --- a/actionpack/lib/action_controller/metal/url_for.rb +++ b/actionpack/lib/action_controller/metal/url_for.rb @@ -32,7 +32,8 @@ module ActionController if (same_origin = _routes.equal?(env["action_dispatch.routes"])) || (script_name = env["ROUTES_#{_routes.object_id}_SCRIPT_NAME"]) || - (original_script_name = env['SCRIPT_NAME']) + (original_script_name = env['ORIGINAL_SCRIPT_NAME']) + @_url_options.dup.tap do |options| if original_script_name options[:original_script_name] = original_script_name |