aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_controller
diff options
context:
space:
mode:
Diffstat (limited to 'actionpack/lib/action_controller')
-rw-r--r--actionpack/lib/action_controller/base.rb21
-rw-r--r--actionpack/lib/action_controller/metal/instrumentation.rb2
-rw-r--r--actionpack/lib/action_controller/metal/live.rb2
-rw-r--r--actionpack/lib/action_controller/metal/mime_responds.rb97
-rw-r--r--actionpack/lib/action_controller/metal/redirecting.rb2
-rw-r--r--actionpack/lib/action_controller/metal/rendering.rb3
-rw-r--r--actionpack/lib/action_controller/metal/request_forgery_protection.rb79
-rw-r--r--actionpack/lib/action_controller/metal/responder.rb2
8 files changed, 165 insertions, 43 deletions
diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb
index c84776ab7a..c0f10da23a 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.
@@ -99,7 +86,7 @@ module ActionController
# or you can remove the entire session with +reset_session+.
#
# Sessions are stored by default in a browser cookie that's cryptographically signed, but unencrypted.
- # This prevents the user from tampering with the session but also allows him to see its contents.
+ # This prevents the user from tampering with the session but also allows them to see its contents.
#
# Do not put secret information in cookie-based sessions!
#
@@ -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/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..b7d1334dd3 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:
#
diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb
index 84ade41036..fbc4024c2d 100644
--- a/actionpack/lib/action_controller/metal/mime_responds.rb
+++ b/actionpack/lib/action_controller/metal/mime_responds.rb
@@ -181,13 +181,50 @@ 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
+ #
# Be sure to check the documentation of +respond_with+ and
# <tt>ActionController::MimeResponds.respond_to</tt> for more examples.
def respond_to(*mimes, &block)
raise ArgumentError, "respond_to takes either types or a block, never both" if mimes.any? && block_given?
if collector = retrieve_collector_from_mimes(mimes, &block)
- response = collector.response
+ response = collector.response(request.variant)
response ? response.call : render({})
end
end
@@ -260,7 +297,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,13 +358,15 @@ 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[:default_response] = collector.response(request.variant)
(options.delete(:responder) || self.class.responder).call(self, resources, options)
end
end
@@ -400,7 +439,8 @@ module ActionController #:nodoc:
def initialize(mimes)
@responses = {}
- mimes.each { |mime| send(mime) }
+
+ mimes.each { |mime| @responses["Mime::#{mime.upcase}".constantize] = nil }
end
def any(*args, &block)
@@ -414,16 +454,55 @@ 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
+ end
end
- def response
- @responses.fetch(format, @responses[Mime::ALL])
+ def response(variant)
+ response = @responses.fetch(format, @responses[Mime::ALL])
+ if response.is_a?(VariantCollector)
+ response.variant(variant)
+ elsif response.nil? || response.arity == 0
+ response
+ else
+ lambda { response.call VariantFilter.new(variant) }
+ end
end
def negotiate_format(request)
@format = request.negotiate_mime(@responses.keys)
end
+
+ #Used for inline syntax
+ class VariantCollector #:nodoc:
+ def initialize
+ @variants = {}
+ end
+
+ def method_missing(name, *args, &block)
+ @variants[name] = block if block_given?
+ end
+
+ def variant(name)
+ @variants[name.nil? ? :none : name]
+ end
+ end
+
+ #Used for nested block syntax
+ class VariantFilter #:nodoc:
+ def initialize(variant)
+ @variant = variant
+ end
+
+ def method_missing(name)
+ if block_given?
+ yield if name == @variant || (name == :none && @variant.nil?)
+ end
+ end
+ end
end
end
end
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/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb
index 5c48b4ab98..66d34f3b67 100644
--- a/actionpack/lib/action_controller/metal/rendering.rb
+++ b/actionpack/lib/action_controller/metal/rendering.rb
@@ -34,7 +34,8 @@ module ActionController
def _process_format(format)
super
- self.content_type ||= format.to_s
+ # 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?
end
# Normalize arguments by catching blocks and setting them on :update.
diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb
index bd64b1f812..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
@@ -169,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
diff --git a/actionpack/lib/action_controller/metal/responder.rb b/actionpack/lib/action_controller/metal/responder.rb
index b4ba169e8f..e24b56fa91 100644
--- a/actionpack/lib/action_controller/metal/responder.rb
+++ b/actionpack/lib/action_controller/metal/responder.rb
@@ -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