Merge branch 'master' of github.com:rails/rails

This commit is contained in:
David Heinemeier Hansson 2014-08-17 12:53:15 -07:00
commit 8f15565de8
40 changed files with 429 additions and 1334 deletions

@ -7,7 +7,7 @@ gemspec
# ensure correct loading order
gem 'mocha', '~> 0.14', require: false
gem 'rack', github: 'rack/rack'
gem 'rack', github: 'rack/rack', branch: 'master'
gem 'rack-cache', '~> 1.2'
gem 'jquery-rails', '~> 3.1.0'
gem 'turbolinks', github: 'rails/turbolinks', branch: 'master'

@ -1,3 +1,39 @@
* Use the Active Support JSON encoder for cookie jars using the `:json` or
`:hybrid` serializer. This allows you to serialize custom Ruby objects into
cookies by defining the `#as_json` hook on such objects.
Fixes #16520.
*Godfrey Chan*
* Add `config.action_dispatch.cookies_digest` option for setting custom
digest. The default remains the same - 'SHA1'.
*Łukasz Strzałkowski*
* Move `respond_with` (and the class-level `respond_to`) to
the `responders` gem.
*José Valim*
* When your templates change, browser caches bust automatically.
New default: the template digest is automatically included in your ETags.
When you call `fresh_when @post`, the digest for `posts/show.html.erb`
is mixed in so future changes to the HTML will blow HTTP caches for you.
This makes it easy to HTTP-cache many more of your actions.
If you render a different template, you can now pass the `:template`
option to include its digest instead:
fresh_when @post, template: 'widgets/show'
Pass `template: false` to skip the lookup. To turn this off entirely, set:
config.action_controller.etag_with_template_digest = false
*Jeremy Kemper*
* Remove deprecated `AbstractController::Helpers::ClassMethods::MissingHelperError`
in favor of `AbstractController::Helpers::MissingHelperError`.

@ -17,6 +17,7 @@ module ActionController
autoload :ConditionalGet
autoload :Cookies
autoload :DataStreaming
autoload :EtagWithTemplateDigest
autoload :Flash
autoload :ForceSSL
autoload :Head

@ -213,6 +213,7 @@ def self.without_modules(*modules)
Rendering,
Renderers::All,
ConditionalGet,
EtagWithTemplateDigest,
RackDelegation,
Caching,
MimeResponds,

@ -41,6 +41,11 @@ def etag(&etagger)
# * <tt>:last_modified</tt>.
# * <tt>:public</tt> By default the Cache-Control header is private, set this to
# +true+ if you want your application to be cachable by other devices (proxy caches).
# * <tt>:template</tt> By default, the template digest for the current
# controller/action is included in ETags. If the action renders a
# different template, you can include its digest instead. If the action
# doesn't render a template at all, you can pass <tt>template: false</tt>
# to skip any attempt to check for a template digest.
#
# === Example:
#
@ -66,18 +71,24 @@ def etag(&etagger)
# @article = Article.find(params[:id])
# fresh_when(@article, public: true)
# end
#
# When rendering a different template than the default controller/action
# style, you can indicate which digest to include in the ETag:
#
# before_action { fresh_when @article, template: 'widgets/show' }
#
def fresh_when(record_or_options, additional_options = {})
if record_or_options.is_a? Hash
options = record_or_options
options.assert_valid_keys(:etag, :last_modified, :public)
options.assert_valid_keys(:etag, :last_modified, :public, :template)
else
record = record_or_options
options = { etag: record, last_modified: record.try(:updated_at) }.merge!(additional_options)
end
response.etag = combine_etags(options[:etag]) if options[:etag]
response.last_modified = options[:last_modified] if options[:last_modified]
response.cache_control[:public] = true if options[:public]
response.etag = combine_etags(options) if options[:etag] || options[:template]
response.last_modified = options[:last_modified] if options[:last_modified]
response.cache_control[:public] = true if options[:public]
head :not_modified if request.fresh?(response)
end
@ -93,6 +104,11 @@ def fresh_when(record_or_options, additional_options = {})
# * <tt>:last_modified</tt>.
# * <tt>:public</tt> By default the Cache-Control header is private, set this to
# +true+ if you want your application to be cachable by other devices (proxy caches).
# * <tt>:template</tt> By default, the template digest for the current
# controller/action is included in ETags. If the action renders a
# different template, you can include its digest instead. If the action
# doesn't render a template at all, you can pass <tt>template: false</tt>
# to skip any attempt to check for a template digest.
#
# === Example:
#
@ -133,6 +149,14 @@ def fresh_when(record_or_options, additional_options = {})
# end
# end
# end
#
# When rendering a different template than the default controller/action
# style, you can indicate which digest to include in the ETag:
#
# def show
# super if stale? @article, template: 'widgets/show'
# end
#
def stale?(record_or_options, additional_options = {})
fresh_when(record_or_options, additional_options)
!request.fresh?(response)
@ -168,8 +192,9 @@ def expires_now
end
private
def combine_etags(etag)
[ etag, *etaggers.map { |etagger| instance_exec(&etagger) }.compact ]
def combine_etags(options)
etags = etaggers.map { |etagger| instance_exec(options, &etagger) }.compact
etags.unshift options[:etag]
end
end
end

@ -0,0 +1,50 @@
module ActionController
# When our views change, they should bubble up into HTTP cache freshness
# and bust browser caches. So the template digest for the current action
# is automatically included in the ETag.
#
# Enabled by default for apps that use Action View. Disable by setting
#
# config.action_controller.etag_with_template_digest = false
#
# Override the template to digest by passing `:template` to `fresh_when`
# and `stale?` calls. For example:
#
# # We're going to render widgets/show, not posts/show
# fresh_when @post, template: 'widgets/show'
#
# # We're not going to render a template, so omit it from the ETag.
# fresh_when @post, template: false
#
module EtagWithTemplateDigest
extend ActiveSupport::Concern
include ActionController::ConditionalGet
included do
class_attribute :etag_with_template_digest
self.etag_with_template_digest = true
ActiveSupport.on_load :action_view, yield: true do |action_view_base|
etag do |options|
determine_template_etag(options) if etag_with_template_digest
end
end
end
private
def determine_template_etag(options)
if template = pick_template_for_etag(options)
lookup_and_digest_template(template)
end
end
def pick_template_for_etag(options)
options.fetch(:template) { "#{controller_name}/#{action_name}" }
end
def lookup_and_digest_template(template)
ActionView::Digestor.digest name: template, finder: lookup_context
end
end
end

@ -5,56 +5,22 @@ module ActionController #:nodoc:
module MimeResponds
extend ActiveSupport::Concern
included do
class_attribute :responder, :mimes_for_respond_to
self.responder = ActionController::Responder
clear_respond_to
module ClassMethods
def respond_to(*)
raise NoMethodError, "The controller-level `respond_to' feature has " \
"been extracted to the `responders` gem. Add it to your Gemfile to " \
"continue using this feature:\n" \
" gem 'responders', '~> 2.0'\n" \
"Consult the Rails upgrade guide for details."
end
end
module ClassMethods
# Defines mime types that are rendered by default when invoking
# <tt>respond_with</tt>.
#
# respond_to :html, :xml, :json
#
# Specifies that all actions in the controller respond to requests
# for <tt>:html</tt>, <tt>:xml</tt> and <tt>:json</tt>.
#
# To specify on per-action basis, use <tt>:only</tt> and
# <tt>:except</tt> with an array of actions or a single action:
#
# respond_to :html
# respond_to :xml, :json, except: [ :edit ]
#
# This specifies that all actions respond to <tt>:html</tt>
# and all actions except <tt>:edit</tt> respond to <tt>:xml</tt> and
# <tt>:json</tt>.
#
# respond_to :json, only: :create
#
# This specifies that the <tt>:create</tt> action and no other responds
# to <tt>:json</tt>.
def respond_to(*mimes)
options = mimes.extract_options!
only_actions = Array(options.delete(:only)).map(&:to_s)
except_actions = Array(options.delete(:except)).map(&:to_s)
new = mimes_for_respond_to.dup
mimes.each do |mime|
mime = mime.to_sym
new[mime] = {}
new[mime][:only] = only_actions unless only_actions.empty?
new[mime][:except] = except_actions unless except_actions.empty?
end
self.mimes_for_respond_to = new.freeze
end
# Clear all mime types in <tt>respond_to</tt>.
#
def clear_respond_to
self.mimes_for_respond_to = Hash.new.freeze
end
def respond_with(*)
raise NoMethodError, "The `respond_with' feature has been extracted " \
"to the `responders` gem. Add it to your Gemfile to continue using " \
"this feature:\n" \
" gem 'responders', '~> 2.0'\n" \
"Consult the Rails upgrade guide for details."
end
# Without web-service support, an action which collects the data for displaying a list of people
@ -217,7 +183,7 @@ def clear_respond_to
# 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:
@ -253,189 +219,13 @@ def clear_respond_to
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 ? response.call : render({})
end
end
# For a given controller action, respond_with generates an appropriate
# response based on the mime-type requested by the client.
#
# If the method is called with just a resource, as in this example -
#
# class PeopleController < ApplicationController
# respond_to :html, :xml, :json
#
# def index
# @people = Person.all
# respond_with @people
# end
# end
#
# then the mime-type of the response is typically selected based on the
# request's Accept header and the set of available formats declared
# by previous calls to the controller's class method +respond_to+. Alternatively
# the mime-type can be selected by explicitly setting <tt>request.format</tt> in
# the controller.
#
# If an acceptable format is not identified, the application returns a
# '406 - not acceptable' status. Otherwise, the default response is to render
# a template named after the current action and the selected format,
# e.g. <tt>index.html.erb</tt>. If no template is available, the behavior
# depends on the selected format:
#
# * for an html response - if the request method is +get+, an exception
# is raised but for other requests such as +post+ the response
# depends on whether the resource has any validation errors (i.e.
# assuming that an attempt has been made to save the resource,
# e.g. by a +create+ action) -
# 1. If there are no errors, i.e. the resource
# was saved successfully, the response +redirect+'s to the resource
# i.e. its +show+ action.
# 2. If there are validation errors, the response
# renders a default action, which is <tt>:new</tt> for a
# +post+ request or <tt>:edit</tt> for +patch+ or +put+.
# Thus an example like this -
#
# respond_to :html, :xml
#
# def create
# @user = User.new(params[:user])
# flash[:notice] = 'User was successfully created.' if @user.save
# respond_with(@user)
# end
#
# is equivalent, in the absence of <tt>create.html.erb</tt>, to -
#
# def create
# @user = User.new(params[:user])
# respond_to do |format|
# if @user.save
# flash[:notice] = 'User was successfully created.'
# format.html { redirect_to(@user) }
# format.xml { render xml: @user }
# else
# format.html { render action: "new" }
# format.xml { render xml: @user }
# end
# end
# end
#
# * for a JavaScript request - if the template isn't found, an exception is
# raised.
# * 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
# <code>render xml: resource</code>.
#
# === Nested resources
#
# As outlined above, the +resources+ argument passed to +respond_with+
# can play two roles. It can be used to generate the redirect url
# for successful html requests (e.g. for +create+ actions when
# no template exists), while for formats other than html and JavaScript
# it is the object that gets rendered, by being converted directly to the
# required format (again assuming no template exists).
#
# For redirecting successful html requests, +respond_with+ also supports
# the use of nested resources, which are supplied in the same way as
# in <code>form_for</code> and <code>polymorphic_url</code>. For example -
#
# def create
# @project = Project.find(params[:project_id])
# @task = @project.comments.build(params[:task])
# flash[:notice] = 'Task was successfully created.' if @task.save
# respond_with(@project, @task)
# end
#
# This would cause +respond_with+ to redirect to <code>project_task_url</code>
# instead of <code>task_url</code>. For request formats other than html or
# JavaScript, if multiple resources are passed in this way, it is the last
# one specified that is rendered.
#
# === Customizing response behavior
#
# Like +respond_to+, +respond_with+ may also be called with a block that
# can be used to overwrite any of the default responses, e.g. -
#
# def create
# @user = User.new(params[:user])
# flash[:notice] = "User was successfully created." if @user.save
#
# respond_with(@user) do |format|
# format.html { render }
# end
# end
#
# The argument passed to the block is an ActionController::MimeResponds::Collector
# object which stores the responses for the formats defined within the
# block. Note that formats with responses defined explicitly in this way
# do not have to first be declared using the class method +respond_to+.
#
# Also, a hash passed to +respond_with+ immediately after the specified
# resource(s) is interpreted as a set of options relevant to all
# formats. Any option accepted by +render+ can be used, e.g.
# respond_with @people, status: 200
# However, note that these options are ignored after an unsuccessful attempt
# to save a resource, e.g. when automatically rendering <tt>:new</tt>
# after a post request.
#
# Two additional options are relevant specifically to +respond_with+ -
# 1. <tt>:location</tt> - overwrites the default redirect location used after
# a successful html +post+ request.
# 2. <tt>:action</tt> - overwrites the default render action used after an
# unsuccessful html +post+ request.
def respond_with(*resources, &block)
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
end
protected
# Collect mimes declared in the class method respond_to valid for the
# current action.
def collect_mimes_from_class_level #:nodoc:
action = action_name.to_s
self.class.mimes_for_respond_to.keys.select do |mime|
config = self.class.mimes_for_respond_to[mime]
if config[:except]
!config[:except].include?(action)
elsif config[:only]
config[:only].include?(action)
else
true
end
end
end
# Returns a Collector object containing the appropriate mime-type response
# for the current request, based on the available responses defined by a block.
# In typical usage this is the block passed to +respond_with+ or +respond_to+.
#
# Sends :not_acceptable to the client and returns nil if no suitable format
# is available.
def retrieve_collector_from_mimes(mimes=nil, &block) #:nodoc:
mimes ||= collect_mimes_from_class_level
collector = Collector.new(mimes, request.variant)
block.call(collector) if block_given?
format = collector.negotiate_format(request)
if format
if format = collector.negotiate_format(request)
_process_format(format)
collector
response = collector.response
response ? response.call : render({})
else
raise ActionController::UnknownFormat
end

@ -1,297 +0,0 @@
require 'active_support/json'
module ActionController #:nodoc:
# Responsible for exposing a resource to different mime requests,
# usually depending on the HTTP verb. The responder is triggered when
# <code>respond_with</code> is called. The simplest case to study is a GET request:
#
# class PeopleController < ApplicationController
# respond_to :html, :xml, :json
#
# def index
# @people = Person.all
# respond_with(@people)
# end
# end
#
# When a request comes in, for example for an XML response, three steps happen:
#
# 1) the responder searches for a template at people/index.xml;
#
# 2) if the template is not available, it will invoke <code>#to_xml</code> on the given resource;
#
# 3) if the responder does not <code>respond_to :to_xml</code>, call <code>#to_format</code> on it.
#
# === 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.
#
# Using \Rails default responder, a POST request for creating an object could
# be written as:
#
# def create
# @user = User.new(params[:user])
# flash[:notice] = 'User was successfully created.' if @user.save
# respond_with(@user)
# end
#
# Which is exactly the same as:
#
# def create
# @user = User.new(params[:user])
#
# respond_to do |format|
# if @user.save
# flash[:notice] = 'User was successfully created.'
# format.html { redirect_to(@user) }
# format.xml { render xml: @user, status: :created, location: @user }
# else
# format.html { render action: "new" }
# format.xml { render xml: @user.errors, status: :unprocessable_entity }
# end
# end
# end
#
# The same happens for PATCH/PUT and DELETE requests.
#
# === Nested resources
#
# You can supply nested resources as you do in <code>form_for</code> and <code>polymorphic_url</code>.
# Consider the project has many tasks example. The create action for
# TasksController would be like:
#
# def create
# @project = Project.find(params[:project_id])
# @task = @project.tasks.build(params[:task])
# flash[:notice] = 'Task was successfully created.' if @task.save
# respond_with(@project, @task)
# end
#
# Giving several resources ensures that the responder will redirect to
# <code>project_task_url</code> instead of <code>task_url</code>.
#
# Namespaced and singleton resources require a symbol to be given, as in
# polymorphic urls. If a project has one manager which has many tasks, it
# should be invoked as:
#
# respond_with(@project, :manager, @task)
#
# Note that if you give an array, it will be treated as a collection,
# so the following is not equivalent:
#
# respond_with [@project, :manager, @task]
#
# === Custom options
#
# <code>respond_with</code> also allows you to pass options that are forwarded
# to the underlying render call. Those options are only applied for success
# scenarios. For instance, you can do the following in the create method above:
#
# def create
# @project = Project.find(params[:project_id])
# @task = @project.tasks.build(params[:task])
# flash[:notice] = 'Task was successfully created.' if @task.save
# respond_with(@project, @task, status: 201)
# end
#
# 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. 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])
# @task = @project.tasks.build(params[:task])
# respond_with(@project, @task, status: 201) do |format|
# if @task.save
# flash[:notice] = 'Task was successfully created.'
# else
# format.html { render "some_special_template" }
# end
# end
# end
#
# Using <code>respond_with</code> with a block follows the same syntax as <code>respond_to</code>.
class Responder
attr_reader :controller, :request, :format, :resource, :resources, :options
DEFAULT_ACTIONS_FOR_VERBS = {
:post => :new,
:patch => :edit,
:put => :edit
}
def initialize(controller, resources, options={})
@controller = controller
@request = @controller.request
@format = @controller.formats.first
@resource = resources.last
@resources = resources
@options = options
@action = options.delete(:action)
@default_response = options.delete(:default_response)
end
delegate :head, :render, :redirect_to, :to => :controller
delegate :get?, :post?, :patch?, :put?, :delete?, :to => :request
# Undefine :to_json and :to_yaml since it's defined on Object
undef_method(:to_json) if method_defined?(:to_json)
undef_method(:to_yaml) if method_defined?(:to_yaml)
# Initializes a new responder and invokes the proper format. If the format is
# not defined, call to_format.
#
def self.call(*args)
new(*args).respond
end
# Main entry point for responder responsible to dispatch to the proper format.
#
def respond
method = "to_#{format}"
respond_to?(method) ? send(method) : to_format
end
# HTML format does not render the resource, it always attempt to render a
# template.
#
def to_html
default_render
rescue ActionView::MissingTemplate => e
navigation_behavior(e)
end
# to_js simply tries to render a template. If no template is found, raises the error.
def to_js
default_render
end
# All other formats follow the procedure below. First we try to render a
# template, if the template is not available, we verify if the resource
# responds to :to_format and display it.
#
def to_format
if get? || !has_errors? || response_overridden?
default_render
else
display_errors
end
rescue ActionView::MissingTemplate => e
api_behavior(e)
end
protected
# This is the common behavior for formats associated with browsing, like :html, :iphone and so forth.
def navigation_behavior(error)
if get?
raise error
elsif has_errors? && default_action
render :action => default_action
else
redirect_to navigation_location
end
end
# 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
elsif post?
display resource, :status => :created, :location => api_location
else
head :no_content
end
end
# Checks whether the resource responds to the current format or not.
#
def resourceful?
resource.respond_to?("to_#{format}")
end
# Returns the resource location by retrieving it from the options or
# returning the resources array.
#
def resource_location
options[:location] || resources
end
alias :navigation_location :resource_location
alias :api_location :resource_location
# If a response block was given, use it, otherwise call render on
# controller.
#
def default_render
if @default_response
@default_response.call(options)
else
controller.default_render(options)
end
end
# Display is just a shortcut to render a resource with the current format.
#
# display @user, status: :ok
#
# For XML requests it's equivalent to:
#
# render xml: @user, status: :ok
#
# Options sent by the user are also used:
#
# respond_with(@user, status: :created)
# display(@user, status: :ok)
#
# Results in:
#
# render xml: @user, status: :created
#
def display(resource, given_options={})
controller.render given_options.merge!(options).merge!(format => resource)
end
def display_errors
controller.render format => resource_errors, :status => :unprocessable_entity
end
# Check whether the resource has errors.
#
def has_errors?
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.
#
def default_action
@action ||= DEFAULT_ACTIONS_FOR_VERBS[request.request_method_symbol]
end
def resource_errors
respond_to?("#{format}_resource_errors", true) ? send("#{format}_resource_errors") : resource.errors
end
def json_resource_errors
{:errors => resource.errors}
end
def response_overridden?
@default_response.present?
end
end
end

@ -3,6 +3,7 @@
require 'active_support/core_ext/object/blank'
require 'active_support/key_generator'
require 'active_support/message_verifier'
require 'active_support/json'
module ActionDispatch
class Request < Rack::Request
@ -90,6 +91,7 @@ class Cookies
SECRET_TOKEN = "action_dispatch.secret_token".freeze
SECRET_KEY_BASE = "action_dispatch.secret_key_base".freeze
COOKIES_SERIALIZER = "action_dispatch.cookies_serializer".freeze
COOKIES_DIGEST = "action_dispatch.cookies_digest".freeze
# Cookies can typically store 4096 bytes.
MAX_COOKIE_SIZE = 4096
@ -173,10 +175,14 @@ def signed_or_encrypted
end
end
# Passing the ActiveSupport::MessageEncryptor::NullSerializer downstream
# to the Message{Encryptor,Verifier} allows us to handle the
# (de)serialization step within the cookie jar, which gives us the
# opportunity to detect and migrate legacy cookies.
module VerifyAndUpgradeLegacySignedMessage
def initialize(*args)
super
@legacy_verifier = ActiveSupport::MessageVerifier.new(@options[:secret_token], serializer: NullSerializer)
@legacy_verifier = ActiveSupport::MessageVerifier.new(@options[:secret_token], serializer: ActiveSupport::MessageEncryptor::NullSerializer)
end
def verify_and_upgrade_legacy_signed_message(name, signed_message)
@ -212,7 +218,8 @@ def self.options_for_env(env) #:nodoc:
secret_token: env[SECRET_TOKEN],
secret_key_base: env[SECRET_KEY_BASE],
upgrade_legacy_signed_cookies: env[SECRET_TOKEN].present? && env[SECRET_KEY_BASE].present?,
serializer: env[COOKIES_SERIALIZER]
serializer: env[COOKIES_SERIALIZER],
digest: env[COOKIES_DIGEST]
}
end
@ -385,24 +392,11 @@ def []=(name, options)
class JsonSerializer
def self.load(value)
JSON.parse(value, quirks_mode: true)
ActiveSupport::JSON.decode(value)
end
def self.dump(value)
JSON.generate(value, quirks_mode: true)
end
end
# Passing the NullSerializer downstream to the Message{Encryptor,Verifier}
# allows us to handle the (de)serialization step within the cookie jar,
# which gives us the opportunity to detect and migrate legacy cookies.
class NullSerializer
def self.load(value)
value
end
def self.dump(value)
value
ActiveSupport::JSON.encode(value)
end
end
@ -441,6 +435,10 @@ def serializer
serializer
end
end
def digest
@options[:digest] || 'SHA1'
end
end
class SignedCookieJar #:nodoc:
@ -451,7 +449,7 @@ def initialize(parent_jar, key_generator, options = {})
@parent_jar = parent_jar
@options = options
secret = key_generator.generate_key(@options[:signed_cookie_salt])
@verifier = ActiveSupport::MessageVerifier.new(secret, serializer: NullSerializer)
@verifier = ActiveSupport::MessageVerifier.new(secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer)
end
def [](name)
@ -508,7 +506,7 @@ def initialize(parent_jar, key_generator, options = {})
@options = options
secret = key_generator.generate_key(@options[:encrypted_cookie_salt])
sign_secret = key_generator.generate_key(@options[:encrypted_signed_cookie_salt])
@encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, serializer: NullSerializer)
@encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer)
end
def [](name)

@ -162,7 +162,7 @@ def thread_locals
end
def with_stale
render :text => 'stale' if stale?(:etag => "123")
render text: 'stale' if stale?(etag: "123", template: false)
end
def exception_in_view

@ -1,737 +0,0 @@
require 'abstract_unit'
require 'controller/fake_models'
class RespondWithController < ActionController::Base
class CustomerWithJson < Customer
def to_json; super; end
end
respond_to :html, :json, :touch
respond_to :xml, :except => :using_resource_with_block
respond_to :js, :only => [ :using_resource_with_block, :using_resource, 'using_hash_resource' ]
def using_resource
respond_with(resource)
end
def using_hash_resource
respond_with({:result => resource})
end
def using_resource_with_block
respond_with(resource) do |format|
format.csv { render :text => "CSV" }
end
end
def using_resource_with_overwrite_block
respond_with(resource) do |format|
format.html { render :text => "HTML" }
end
end
def using_resource_with_collection
respond_with([resource, Customer.new("jamis", 9)])
end
def using_resource_with_parent
respond_with(Quiz::Store.new("developer?", 11), Customer.new("david", 13))
end
def using_resource_with_status_and_location
respond_with(resource, :location => "http://test.host/", :status => :created)
end
def using_resource_with_json
respond_with(CustomerWithJson.new("david", request.delete? ? nil : 13))
end
def using_invalid_resource_with_template
respond_with(resource)
end
def using_options_with_template
@customer = resource
respond_with(@customer, :status => 123, :location => "http://test.host/")
end
def using_resource_with_responder
responder = proc { |c, r, o| c.render :text => "Resource name is #{r.first.name}" }
respond_with(resource, :responder => responder)
end
def using_resource_with_action
respond_with(resource, :action => :foo) do |format|
format.html { raise ActionView::MissingTemplate.new([], "bar", ["foo"], {}, false) }
end
end
def using_responder_with_respond
responder = Class.new(ActionController::Responder) do
def respond; @controller.render :text => "respond #{format}"; end
end
respond_with(resource, :responder => responder)
end
def respond_with_additional_params
@params = RespondWithController.params
respond_with({:result => resource}, @params)
end
protected
def self.params
{
:foo => 'bar'
}
end
def resource
Customer.new("david", request.delete? ? nil : 13)
end
end
class InheritedRespondWithController < RespondWithController
clear_respond_to
respond_to :xml, :json
def index
respond_with(resource) do |format|
format.json { render :text => "JSON" }
end
end
end
class RenderJsonRespondWithController < RespondWithController
clear_respond_to
respond_to :json
def index
respond_with(resource) do |format|
format.json { render :json => RenderJsonTestException.new('boom') }
end
end
def create
resource = ValidatedCustomer.new(params[:name], 1)
respond_with(resource) do |format|
format.json do
if resource.errors.empty?
render :json => { :valid => true }
else
render :json => { :valid => false }
end
end
end
end
end
class CsvRespondWithController < ActionController::Base
respond_to :csv
class RespondWithCsv
def to_csv
"c,s,v"
end
end
def index
respond_with(RespondWithCsv.new)
end
end
class EmptyRespondWithController < ActionController::Base
def index
respond_with(Customer.new("david", 13))
end
end
class RespondWithControllerTest < ActionController::TestCase
def setup
super
@request.host = "www.example.com"
Mime::Type.register_alias('text/html', :iphone)
Mime::Type.register_alias('text/html', :touch)
Mime::Type.register('text/x-mobile', :mobile)
end
def teardown
super
Mime::Type.unregister(:iphone)
Mime::Type.unregister(:touch)
Mime::Type.unregister(:mobile)
end
def test_respond_with_shouldnt_modify_original_hash
get :respond_with_additional_params
assert_equal RespondWithController.params, assigns(:params)
end
def test_using_resource
@request.accept = "application/xml"
get :using_resource
assert_equal "application/xml", @response.content_type
assert_equal "<name>david</name>", @response.body
@request.accept = "application/json"
assert_raise ActionView::MissingTemplate do
get :using_resource
end
end
def test_using_resource_with_js_simply_tries_to_render_the_template
@request.accept = "text/javascript"
get :using_resource
assert_equal "text/javascript", @response.content_type
assert_equal "alert(\"Hi\");", @response.body
end
def test_using_hash_resource_with_js_raises_an_error_if_template_cant_be_found
@request.accept = "text/javascript"
assert_raise ActionView::MissingTemplate do
get :using_hash_resource
end
end
def test_using_hash_resource
@request.accept = "application/xml"
get :using_hash_resource
assert_equal "application/xml", @response.content_type
assert_equal "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<hash>\n <name>david</name>\n</hash>\n", @response.body
@request.accept = "application/json"
get :using_hash_resource
assert_equal "application/json", @response.content_type
assert @response.body.include?("result")
assert @response.body.include?('"name":"david"')
assert @response.body.include?('"id":13')
end
def test_using_hash_resource_with_post
@request.accept = "application/json"
assert_raise ArgumentError, "Nil location provided. Can't build URI." do
post :using_hash_resource
end
end
def test_using_resource_with_block
@request.accept = "*/*"
get :using_resource_with_block
assert_equal "text/html", @response.content_type
assert_equal 'Hello world!', @response.body
@request.accept = "text/csv"
get :using_resource_with_block
assert_equal "text/csv", @response.content_type
assert_equal "CSV", @response.body
@request.accept = "application/xml"
get :using_resource
assert_equal "application/xml", @response.content_type
assert_equal "<name>david</name>", @response.body
end
def test_using_resource_with_overwrite_block
get :using_resource_with_overwrite_block
assert_equal "text/html", @response.content_type
assert_equal "HTML", @response.body
end
def test_not_acceptable
@request.accept = "application/xml"
assert_raises(ActionController::UnknownFormat) do
get :using_resource_with_block
end
@request.accept = "text/javascript"
assert_raises(ActionController::UnknownFormat) do
get :using_resource_with_overwrite_block
end
end
def test_using_resource_for_post_with_html_redirects_on_success
with_test_route_set do
post :using_resource
assert_equal "text/html", @response.content_type
assert_equal 302, @response.status
assert_equal "http://www.example.com/customers/13", @response.location
assert @response.redirect?
end
end
def test_using_resource_for_post_with_html_rerender_on_failure
with_test_route_set do
errors = { :name => :invalid }
Customer.any_instance.stubs(:errors).returns(errors)
post :using_resource
assert_equal "text/html", @response.content_type
assert_equal 200, @response.status
assert_equal "New world!\n", @response.body
assert_nil @response.location
end
end
def test_using_resource_for_post_with_xml_yields_created_on_success
with_test_route_set do
@request.accept = "application/xml"
post :using_resource
assert_equal "application/xml", @response.content_type
assert_equal 201, @response.status
assert_equal "<name>david</name>", @response.body
assert_equal "http://www.example.com/customers/13", @response.location
end
end
def test_using_resource_for_post_with_xml_yields_unprocessable_entity_on_failure
with_test_route_set do
@request.accept = "application/xml"
errors = { :name => :invalid }
Customer.any_instance.stubs(:errors).returns(errors)
post :using_resource
assert_equal "application/xml", @response.content_type
assert_equal 422, @response.status
assert_equal errors.to_xml, @response.body
assert_nil @response.location
end
end
def test_using_resource_for_post_with_json_yields_unprocessable_entity_on_failure
with_test_route_set do
@request.accept = "application/json"
errors = { :name => :invalid }
Customer.any_instance.stubs(:errors).returns(errors)
post :using_resource
assert_equal "application/json", @response.content_type
assert_equal 422, @response.status
errors = {:errors => errors}
assert_equal errors.to_json, @response.body
assert_nil @response.location
end
end
def test_using_resource_for_patch_with_html_redirects_on_success
with_test_route_set do
patch :using_resource
assert_equal "text/html", @response.content_type
assert_equal 302, @response.status
assert_equal "http://www.example.com/customers/13", @response.location
assert @response.redirect?
end
end
def test_using_resource_for_patch_with_html_rerender_on_failure
with_test_route_set do
errors = { :name => :invalid }
Customer.any_instance.stubs(:errors).returns(errors)
patch :using_resource
assert_equal "text/html", @response.content_type
assert_equal 200, @response.status
assert_equal "Edit world!\n", @response.body
assert_nil @response.location
end
end
def test_using_resource_for_patch_with_html_rerender_on_failure_even_on_method_override
with_test_route_set do
errors = { :name => :invalid }
Customer.any_instance.stubs(:errors).returns(errors)
@request.env["rack.methodoverride.original_method"] = "POST"
patch :using_resource
assert_equal "text/html", @response.content_type
assert_equal 200, @response.status
assert_equal "Edit world!\n", @response.body
assert_nil @response.location
end
end
def test_using_resource_for_put_with_html_redirects_on_success
with_test_route_set do
put :using_resource
assert_equal "text/html", @response.content_type
assert_equal 302, @response.status
assert_equal "http://www.example.com/customers/13", @response.location
assert @response.redirect?
end
end
def test_using_resource_for_put_with_html_rerender_on_failure
with_test_route_set do
errors = { :name => :invalid }
Customer.any_instance.stubs(:errors).returns(errors)
put :using_resource
assert_equal "text/html", @response.content_type
assert_equal 200, @response.status
assert_equal "Edit world!\n", @response.body
assert_nil @response.location
end
end
def test_using_resource_for_put_with_html_rerender_on_failure_even_on_method_override
with_test_route_set do
errors = { :name => :invalid }
Customer.any_instance.stubs(:errors).returns(errors)
@request.env["rack.methodoverride.original_method"] = "POST"
put :using_resource
assert_equal "text/html", @response.content_type
assert_equal 200, @response.status
assert_equal "Edit world!\n", @response.body
assert_nil @response.location
end
end
def test_using_resource_for_put_with_xml_yields_no_content_on_success
@request.accept = "application/xml"
put :using_resource
assert_equal "application/xml", @response.content_type
assert_equal 204, @response.status
assert_equal "", @response.body
end
def test_using_resource_for_put_with_json_yields_no_content_on_success
@request.accept = "application/json"
put :using_resource_with_json
assert_equal "application/json", @response.content_type
assert_equal 204, @response.status
assert_equal "", @response.body
end
def test_using_resource_for_put_with_xml_yields_unprocessable_entity_on_failure
@request.accept = "application/xml"
errors = { :name => :invalid }
Customer.any_instance.stubs(:errors).returns(errors)
put :using_resource
assert_equal "application/xml", @response.content_type
assert_equal 422, @response.status
assert_equal errors.to_xml, @response.body
assert_nil @response.location
end
def test_using_resource_for_put_with_json_yields_unprocessable_entity_on_failure
@request.accept = "application/json"
errors = { :name => :invalid }
Customer.any_instance.stubs(:errors).returns(errors)
put :using_resource
assert_equal "application/json", @response.content_type
assert_equal 422, @response.status
errors = {:errors => errors}
assert_equal errors.to_json, @response.body
assert_nil @response.location
end
def test_using_resource_for_delete_with_html_redirects_on_success
with_test_route_set do
Customer.any_instance.stubs(:destroyed?).returns(true)
delete :using_resource
assert_equal "text/html", @response.content_type
assert_equal 302, @response.status
assert_equal "http://www.example.com/customers", @response.location
end
end
def test_using_resource_for_delete_with_xml_yields_no_content_on_success
Customer.any_instance.stubs(:destroyed?).returns(true)
@request.accept = "application/xml"
delete :using_resource
assert_equal "application/xml", @response.content_type
assert_equal 204, @response.status
assert_equal "", @response.body
end
def test_using_resource_for_delete_with_json_yields_no_content_on_success
Customer.any_instance.stubs(:destroyed?).returns(true)
@request.accept = "application/json"
delete :using_resource_with_json
assert_equal "application/json", @response.content_type
assert_equal 204, @response.status
assert_equal "", @response.body
end
def test_using_resource_for_delete_with_html_redirects_on_failure
with_test_route_set do
errors = { :name => :invalid }
Customer.any_instance.stubs(:errors).returns(errors)
Customer.any_instance.stubs(:destroyed?).returns(false)
delete :using_resource
assert_equal "text/html", @response.content_type
assert_equal 302, @response.status
assert_equal "http://www.example.com/customers", @response.location
end
end
def test_using_resource_with_parent_for_get
@request.accept = "application/xml"
get :using_resource_with_parent
assert_equal "application/xml", @response.content_type
assert_equal 200, @response.status
assert_equal "<name>david</name>", @response.body
end
def test_using_resource_with_parent_for_post
with_test_route_set do
@request.accept = "application/xml"
post :using_resource_with_parent
assert_equal "application/xml", @response.content_type
assert_equal 201, @response.status
assert_equal "<name>david</name>", @response.body
assert_equal "http://www.example.com/quiz_stores/11/customers/13", @response.location
errors = { :name => :invalid }
Customer.any_instance.stubs(:errors).returns(errors)
post :using_resource
assert_equal "application/xml", @response.content_type
assert_equal 422, @response.status
assert_equal errors.to_xml, @response.body
assert_nil @response.location
end
end
def test_using_resource_with_collection
@request.accept = "application/xml"
get :using_resource_with_collection
assert_equal "application/xml", @response.content_type
assert_equal 200, @response.status
assert_match(/<name>david<\/name>/, @response.body)
assert_match(/<name>jamis<\/name>/, @response.body)
end
def test_using_resource_with_action
@controller.instance_eval do
def render(params={})
self.response_body = "#{params[:action]} - #{formats}"
end
end
errors = { :name => :invalid }
Customer.any_instance.stubs(:errors).returns(errors)
post :using_resource_with_action
assert_equal "foo - #{[:html].to_s}", @controller.response.body
end
def test_respond_as_responder_entry_point
@request.accept = "text/html"
get :using_responder_with_respond
assert_equal "respond html", @response.body
@request.accept = "application/xml"
get :using_responder_with_respond
assert_equal "respond xml", @response.body
end
def test_clear_respond_to
@controller = InheritedRespondWithController.new
@request.accept = "text/html"
assert_raises(ActionController::UnknownFormat) do
get :index
end
end
def test_first_in_respond_to_has_higher_priority
@controller = InheritedRespondWithController.new
@request.accept = "*/*"
get :index
assert_equal "application/xml", @response.content_type
assert_equal "<name>david</name>", @response.body
end
def test_block_inside_respond_with_is_rendered
@controller = InheritedRespondWithController.new
@request.accept = "application/json"
get :index
assert_equal "JSON", @response.body
end
def test_render_json_object_responds_to_str_still_produce_json
@controller = RenderJsonRespondWithController.new
@request.accept = "application/json"
get :index, :format => :json
assert_match(/"message":"boom"/, @response.body)
assert_match(/"error":"RenderJsonTestException"/, @response.body)
end
def test_api_response_with_valid_resource_respect_override_block
@controller = RenderJsonRespondWithController.new
post :create, :name => "sikachu", :format => :json
assert_equal '{"valid":true}', @response.body
end
def test_api_response_with_invalid_resource_respect_override_block
@controller = RenderJsonRespondWithController.new
post :create, :name => "david", :format => :json
assert_equal '{"valid":false}', @response.body
end
def test_no_double_render_is_raised
@request.accept = "text/html"
assert_raise ActionView::MissingTemplate do
get :using_resource
end
end
def test_using_resource_with_status_and_location
@request.accept = "text/html"
post :using_resource_with_status_and_location
assert @response.redirect?
assert_equal "http://test.host/", @response.location
@request.accept = "application/xml"
get :using_resource_with_status_and_location
assert_equal 201, @response.status
end
def test_using_resource_with_status_and_location_with_invalid_resource
errors = { :name => :invalid }
Customer.any_instance.stubs(:errors).returns(errors)
@request.accept = "text/xml"
post :using_resource_with_status_and_location
assert_equal errors.to_xml, @response.body
assert_equal 422, @response.status
assert_equal nil, @response.location
put :using_resource_with_status_and_location
assert_equal errors.to_xml, @response.body
assert_equal 422, @response.status
assert_equal nil, @response.location
end
def test_using_invalid_resource_with_template
errors = { :name => :invalid }
Customer.any_instance.stubs(:errors).returns(errors)
@request.accept = "text/xml"
post :using_invalid_resource_with_template
assert_equal errors.to_xml, @response.body
assert_equal 422, @response.status
assert_equal nil, @response.location
put :using_invalid_resource_with_template
assert_equal errors.to_xml, @response.body
assert_equal 422, @response.status
assert_equal nil, @response.location
end
def test_using_options_with_template
@request.accept = "text/xml"
post :using_options_with_template
assert_equal "<customer-name>david</customer-name>", @response.body
assert_equal 123, @response.status
assert_equal "http://test.host/", @response.location
put :using_options_with_template
assert_equal "<customer-name>david</customer-name>", @response.body
assert_equal 123, @response.status
assert_equal "http://test.host/", @response.location
end
def test_using_resource_with_responder
get :using_resource_with_responder
assert_equal "Resource name is david", @response.body
end
def test_using_resource_with_set_responder
RespondWithController.responder = proc { |c, r, o| c.render :text => "Resource name is #{r.first.name}" }
get :using_resource
assert_equal "Resource name is david", @response.body
ensure
RespondWithController.responder = ActionController::Responder
end
def test_uses_renderer_if_an_api_behavior
ActionController::Renderers.add :csv do |obj, options|
send_data obj.to_csv, type: Mime::CSV
end
@controller = CsvRespondWithController.new
get :index, format: 'csv'
assert_equal Mime::CSV, @response.content_type
assert_equal "c,s,v", @response.body
ensure
ActionController::Renderers.remove :csv
end
def test_raises_missing_renderer_if_an_api_behavior_with_no_renderer
@controller = CsvRespondWithController.new
assert_raise ActionController::MissingRenderer do
get :index, format: 'csv'
end
end
def test_removing_renderers
ActionController::Renderers.add :csv do |obj, options|
send_data obj.to_csv, type: Mime::CSV
end
@controller = CsvRespondWithController.new
@request.accept = "text/csv"
get :index, format: 'csv'
assert_equal Mime::CSV, @response.content_type
ActionController::Renderers.remove :csv
assert_raise ActionController::MissingRenderer do
get :index, format: 'csv'
end
ensure
ActionController::Renderers.remove :csv
end
def test_error_is_raised_if_no_respond_to_is_declared_and_respond_with_is_called
@controller = EmptyRespondWithController.new
@request.accept = "*/*"
assert_raise RuntimeError do
get :index
end
end
private
def with_test_route_set
with_routing do |set|
set.draw do
resources :customers
resources :quiz_stores do
resources :customers
end
get ":controller/:action"
end
yield
end
end
end
class FlashResponder < ActionController::Responder
def initialize(controller, resources, options={})
super
end
def to_html
controller.flash[:notice] = 'Success'
super
end
end
class FlashResponderController < ActionController::Base
self.responder = FlashResponder
respond_to :html
def index
respond_with Object.new do |format|
format.html { render :text => 'HTML' }
end
end
end
class FlashResponderControllerTest < ActionController::TestCase
tests FlashResponderController
def test_respond_with_block_executed
get :index
assert_equal 'HTML', @response.body
end
def test_flash_responder_executed
get :index
assert_equal 'Success', flash[:notice]
end
end

@ -0,0 +1,32 @@
require 'abstract_unit'
require 'controller/fake_models'
class ResponderTest < ActionController::TestCase
def test_class_level_respond_to
e = assert_raises(NoMethodError) do
Class.new(ActionController::Base) do
respond_to :json
end
end
assert_includes e.message, '`responders` gem'
assert_includes e.message, '~> 2.0'
end
def test_respond_with
klass = Class.new(ActionController::Base) do
def index
respond_with Customer.new("david", 13)
end
end
@controller = klass.new
e = assert_raises(NoMethodError) do
get :index
end
assert_includes e.message, '`responders` gem'
assert_includes e.message, '~> 2.0'
end
end

@ -10,11 +10,17 @@ class TestControllerWithExtraEtags < ActionController::Base
etag { nil }
def fresh
render text: "stale" if stale?(etag: '123')
render text: "stale" if stale?(etag: '123', template: false)
end
def array
render text: "stale" if stale?(etag: %w(1 2 3))
render text: "stale" if stale?(etag: %w(1 2 3), template: false)
end
def with_template
if stale? template: 'test/hello_world'
render text: 'stale'
end
end
end
@ -409,6 +415,32 @@ def test_array
assert_response :success
end
def test_etag_reflects_template_digest
get :with_template
assert_response :ok
assert_not_nil etag = @response.etag
request.if_none_match = etag
get :with_template
assert_response :not_modified
# Modify the template digest
path = File.expand_path('../../fixtures/test/hello_world.erb', __FILE__)
old = File.read(path)
begin
File.write path, 'foo'
ActionView::Digestor.cache.clear
request.if_none_match = etag
get :with_template
assert_response :ok
assert_not_equal etag, @response.etag
ensure
File.write path, old
end
end
def etag(record)
Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key(record)).inspect
end

@ -21,6 +21,16 @@ def self.dump(value)
end
end
class JSONWrapper
def initialize(obj)
@obj = obj
end
def as_json(options = nil)
"wrapped: #{@obj.as_json(options)}"
end
end
class TestController < ActionController::Base
def authenticate
cookies["user_name"] = "david"
@ -85,6 +95,11 @@ def set_signed_cookie
head :ok
end
def set_wrapped_signed_cookie
cookies.signed[:user_id] = JSONWrapper.new(45)
head :ok
end
def get_signed_cookie
cookies.signed[:user_id]
head :ok
@ -95,6 +110,11 @@ def set_encrypted_cookie
head :ok
end
def set_wrapped_encrypted_cookie
cookies.encrypted[:foo] = JSONWrapper.new('bar')
head :ok
end
def get_encrypted_cookie
cookies.encrypted[:foo]
head :ok
@ -369,6 +389,35 @@ def test_read_permanent_cookie
assert_equal 'Jamie', @controller.send(:cookies).permanent[:user_name]
end
def test_signed_cookie_using_default_digest
get :set_signed_cookie
cookies = @controller.send :cookies
assert_not_equal 45, cookies[:user_id]
assert_equal 45, cookies.signed[:user_id]
key_generator = @request.env["action_dispatch.key_generator"]
signed_cookie_salt = @request.env["action_dispatch.signed_cookie_salt"]
secret = key_generator.generate_key(signed_cookie_salt)
verifier = ActiveSupport::MessageVerifier.new(secret, serializer: Marshal, digest: 'SHA1')
assert_equal verifier.generate(45), cookies[:user_id]
end
def test_signed_cookie_using_custom_digest
@request.env["action_dispatch.cookies_digest"] = 'SHA256'
get :set_signed_cookie
cookies = @controller.send :cookies
assert_not_equal 45, cookies[:user_id]
assert_equal 45, cookies.signed[:user_id]
key_generator = @request.env["action_dispatch.key_generator"]
signed_cookie_salt = @request.env["action_dispatch.signed_cookie_salt"]
secret = key_generator.generate_key(signed_cookie_salt)
verifier = ActiveSupport::MessageVerifier.new(secret, serializer: Marshal, digest: 'SHA256')
assert_equal verifier.generate(45), cookies[:user_id]
end
def test_signed_cookie_using_default_serializer
get :set_signed_cookie
cookies = @controller.send :cookies
@ -392,6 +441,14 @@ def test_signed_cookie_using_json_serializer
assert_equal 45, cookies.signed[:user_id]
end
def test_wrapped_signed_cookie_using_json_serializer
@request.env["action_dispatch.cookies_serializer"] = :json
get :set_wrapped_signed_cookie
cookies = @controller.send :cookies
assert_not_equal 'wrapped: 45', cookies[:user_id]
assert_equal 'wrapped: 45', cookies.signed[:user_id]
end
def test_signed_cookie_using_custom_serializer
@request.env["action_dispatch.cookies_serializer"] = CustomSerializer
get :set_signed_cookie
@ -474,6 +531,17 @@ def test_encrypted_cookie_using_json_serializer
assert_equal 'bar', cookies.encrypted[:foo]
end
def test_wrapped_encrypted_cookie_using_json_serializer
@request.env["action_dispatch.cookies_serializer"] = :json
get :set_wrapped_encrypted_cookie
cookies = @controller.send :cookies
assert_not_equal 'wrapped: bar', cookies[:foo]
assert_raises ::JSON::ParserError do
cookies.signed[:foo]
end
assert_equal 'wrapped: bar', cookies.encrypted[:foo]
end
def test_encrypted_cookie_using_custom_serializer
@request.env["action_dispatch.cookies_serializer"] = CustomSerializer
get :set_encrypted_cookie
@ -481,6 +549,27 @@ def test_encrypted_cookie_using_custom_serializer
assert_equal 'bar was dumped and loaded', cookies.encrypted[:foo]
end
def test_encrypted_cookie_using_custom_digest
@request.env["action_dispatch.cookies_digest"] = 'SHA256'
get :set_encrypted_cookie
cookies = @controller.send :cookies
assert_not_equal 'bar', cookies[:foo]
assert_equal 'bar', cookies.encrypted[:foo]
sign_secret = @request.env["action_dispatch.key_generator"].generate_key(@request.env["action_dispatch.encrypted_signed_cookie_salt"])
sha1_verifier = ActiveSupport::MessageVerifier.new(sign_secret, serializer: ActiveSupport::MessageEncryptor::NullSerializer, digest: 'SHA1')
sha256_verifier = ActiveSupport::MessageVerifier.new(sign_secret, serializer: ActiveSupport::MessageEncryptor::NullSerializer, digest: 'SHA256')
assert_raises(ActiveSupport::MessageVerifier::InvalidSignature) do
sha1_verifier.verify(cookies[:foo])
end
assert_nothing_raised do
sha256_verifier.verify(cookies[:foo])
end
end
def test_encrypted_cookie_using_hybrid_serializer_can_migrate_marshal_dumped_value_to_json
@request.env["action_dispatch.cookies_serializer"] = :hybrid

@ -1 +0,0 @@
Edit world!

@ -1 +0,0 @@
New world!

@ -1 +0,0 @@
<content>I should not be displayed</content>

@ -1 +0,0 @@
<customer-name><%= @customer.name %></customer-name>

@ -1 +0,0 @@
alert("Hi");

@ -1 +0,0 @@
Hello world!

@ -8,8 +8,6 @@
class ControllerRuntimeLogSubscriberTest < ActionController::TestCase
class LogSubscriberController < ActionController::Base
respond_to :html
def show
render :inline => "<%= Project.all %>"
end
@ -21,7 +19,7 @@ def zero
def create
ActiveRecord::LogSubscriber.runtime += 100
project = Project.last
respond_with(project, location: url_for(action: :show))
redirect_to "/"
end
def redirect

@ -219,7 +219,7 @@ def attribute_will_change!(attr)
rescue TypeError, NoMethodError
end
changed_attributes[attr] = value
set_attribute_was(attr, value)
end
# Handle <tt>reset_*!</tt> for +method_missing+.
@ -233,8 +233,22 @@ def reset_attribute!(attr)
def restore_attribute!(attr)
if attribute_changed?(attr)
__send__("#{attr}=", changed_attributes[attr])
changed_attributes.delete(attr)
clear_attribute_changes([attr])
end
end
# This is necessary because `changed_attributes` might be overridden in
# other implemntations (e.g. in `ActiveRecord`)
alias_method :attributes_changed_by_setter, :changed_attributes # :nodoc:
# Force an attribute to have a particular "before" value
def set_attribute_was(attr, old_value)
attributes_changed_by_setter[attr] = old_value
end
# Remove changes information for the provided attributes.
def clear_attribute_changes(attributes)
attributes_changed_by_setter.except!(*attributes)
end
end
end

@ -1,3 +1,16 @@
* Fixed an issue where custom accessor methods (such as those generated by
`enum`) with the same name as a global method are incorrectly overridden
when subclassing.
Fixes #16288.
*Godfrey Chan*
* `*_was` and `changes` now work correctly for in-place attribute changes as
well.
*Sean Griffin*
* Fix regression on after_commit that didnt fire when having nested transactions.
Fixes #16425

@ -103,7 +103,7 @@ def update_counter_in_memory(difference, reflection = reflection())
if has_cached_counter?(reflection)
counter = cached_counter_attribute_name(reflection)
owner[counter] += difference
owner.changed_attributes.delete(counter) # eww
owner.send(:clear_attribute_changes, counter) # eww
end
end

@ -30,10 +30,14 @@ def initialize(name, value_before_type_cast, type)
def value
# `defined?` is cheaper than `||=` when we get back falsy values
@value = type_cast(value_before_type_cast) unless defined?(@value)
@value = original_value unless defined?(@value)
@value
end
def original_value
type_cast(value_before_type_cast)
end
def value_for_database
type.type_cast_for_database(value)
end
@ -54,7 +58,7 @@ def with_value_from_database(value)
self.class.from_database(name, value, type)
end
def type_cast
def type_cast(*)
raise NotImplementedError
end

@ -57,6 +57,8 @@ def method_body(method_name, const_name)
end
end
class GeneratedAttributeMethods < Module; end # :nodoc:
module ClassMethods
def inherited(child_class) #:nodoc:
child_class.initialize_generated_modules
@ -64,7 +66,7 @@ def inherited(child_class) #:nodoc:
end
def initialize_generated_modules # :nodoc:
@generated_attribute_methods = Module.new { extend Mutex_m }
@generated_attribute_methods = GeneratedAttributeMethods.new { extend Mutex_m }
@attribute_methods_generated = false
include @generated_attribute_methods
end
@ -113,10 +115,11 @@ def instance_method_already_implemented?(method_name)
if superclass == Base
super
else
# If B < A and A defines its own attribute method, then we don't want to overwrite that.
defined = method_defined_within?(method_name, superclass, superclass.generated_attribute_methods)
base_defined = Base.method_defined?(method_name) || Base.private_method_defined?(method_name)
defined && !base_defined || super
# If ThisClass < ... < SomeSuperClass < ... < Base and SomeSuperClass
# defines its own attribute method, then we don't want to overwrite that.
defined = method_defined_within?(method_name, superclass, Base) &&
! superclass.instance_method(method_name).owner.is_a?(GeneratedAttributeMethods)
defined || super
end
end

@ -51,14 +51,6 @@ def changed
super | changed_in_place
end
def attribute_changed?(attr_name, options = {})
result = super
# We can't change "from" something in place. Only setters can define
# "from" and "to"
result ||= changed_in_place?(attr_name) unless options.key?(:from)
result
end
def changes_applied
super
store_original_raw_attributes
@ -69,12 +61,16 @@ def clear_changes_information
original_raw_attributes.clear
end
def changed_attributes
super.reverse_merge(attributes_changed_in_place).freeze
end
private
def calculate_changes_from_defaults
@changed_attributes = nil
self.class.column_defaults.each do |attr, orig_value|
changed_attributes[attr] = orig_value if _field_changed?(attr, orig_value)
set_attribute_was(attr, orig_value) if _field_changed?(attr, orig_value)
end
end
@ -100,9 +96,9 @@ def raw_write_attribute(attr, value)
def save_changed_attribute(attr, old_value)
if attribute_changed?(attr)
changed_attributes.delete(attr) unless _field_changed?(attr, old_value)
clear_attribute_changes(attr) unless _field_changed?(attr, old_value)
else
changed_attributes[attr] = old_value if _field_changed?(attr, old_value)
set_attribute_was(attr, old_value) if _field_changed?(attr, old_value)
end
end
@ -132,6 +128,13 @@ def _field_changed?(attr, old_value)
@attributes[attr].changed_from?(old_value)
end
def attributes_changed_in_place
changed_in_place.each_with_object({}) do |attr_name, h|
orig = @attributes[attr_name].original_value
h[attr_name] = orig
end
end
def changed_in_place
self.class.attribute_names.select do |attr_name|
changed_in_place?(attr_name)

@ -145,11 +145,11 @@ def save_changed_attribute(attr_name, old)
value = read_attribute(attr_name)
if attribute_changed?(attr_name)
if mapping[old] == value
changed_attributes.delete(attr_name)
clear_attribute_changes([attr_name])
end
else
if old != value
changed_attributes[attr_name] = mapping.key old
set_attribute_was(attr_name, mapping.key(old))
end
end
else

@ -466,7 +466,7 @@ def touch(*names)
changes[self.class.locking_column] = increment_lock if locking_enabled?
changed_attributes.except!(*changes.keys)
clear_attribute_changes(changes.keys)
primary_key = self.class.primary_key
self.class.unscoped.where(primary_key => self[primary_key]).update_all(changes) == 1
else

@ -114,7 +114,7 @@ def current_time_from_proper_timezone
def clear_timestamp_attributes
all_timestamp_attributes_in_model.each do |attribute_name|
self[attribute_name] = nil
changed_attributes.delete(attribute_name)
clear_attribute_changes([attribute_name])
end
end
end

@ -810,6 +810,24 @@ def title=(val); self.author_name = val; end
assert_equal "lol", topic.author_name
end
def test_inherited_custom_accessors_with_reserved_names
klass = Class.new(ActiveRecord::Base) do
self.table_name = 'computers'
self.abstract_class = true
def system; "omg"; end
def system=(val); self.developer = val; end
end
subklass = Class.new(klass)
[klass, subklass].each(&:define_attribute_methods)
computer = subklass.find(1)
assert_equal "omg", computer.system
computer.developer = 99
assert_equal 99, computer.developer
end
def test_on_the_fly_super_invokable_generated_attribute_methods_via_method_missing
klass = new_topic_like_ar_class do
def title

@ -661,6 +661,27 @@ def type_cast_for_database(value)
assert_not model.foo_changed?
end
test "in place mutation detection" do
pirate = Pirate.create!(catchphrase: "arrrr")
pirate.catchphrase << " matey!"
assert pirate.catchphrase_changed?
expected_changes = {
"catchphrase" => ["arrrr", "arrrr matey!"]
}
assert_equal(expected_changes, pirate.changes)
assert_equal("arrrr", pirate.catchphrase_was)
assert pirate.catchphrase_changed?(from: "arrrr")
assert_not pirate.catchphrase_changed?(from: "anything else")
assert pirate.changed_attributes.include?(:catchphrase)
pirate.save!
pirate.reload
assert_equal "arrrr matey!", pirate.catchphrase
assert_not pirate.changed?
end
private
def with_partial_writes(klass, on = true)
old = klass.partial_writes?

@ -303,8 +303,8 @@ def constantize(camel_cased_word)
def safe_constantize(camel_cased_word)
constantize(camel_cased_word)
rescue NameError => e
raise unless e.message =~ /(uninitialized constant|wrong constant name) #{const_regexp(camel_cased_word)}$/ ||
e.name.to_s == camel_cased_word.to_s
raise if e.name && !(camel_cased_word.to_s.split("::").include?(e.name.to_s) ||
e.name.to_s == camel_cased_word.to_s)
rescue ArgumentError => e
raise unless e.message =~ /not missing constant #{const_regexp(camel_cased_word)}\!$/
end

@ -40,6 +40,7 @@ class InvalidMessage < StandardError; end
# Options:
# * <tt>:cipher</tt> - Cipher to use. Can be any cipher returned by
# <tt>OpenSSL::Cipher.ciphers</tt>. Default is 'aes-256-cbc'.
# * <tt>:digest</tt> - String of digest to use for signing. Default is +SHA1+.
# * <tt>:serializer</tt> - Object serializer to use. Default is +Marshal+.
def initialize(secret, *signature_key_or_options)
options = signature_key_or_options.extract_options!
@ -47,7 +48,7 @@ def initialize(secret, *signature_key_or_options)
@secret = secret
@sign_secret = sign_secret
@cipher = options[:cipher] || 'aes-256-cbc'
@verifier = MessageVerifier.new(@sign_secret || @secret, :serializer => NullSerializer)
@verifier = MessageVerifier.new(@sign_secret || @secret, digest: options[:digest] || 'SHA1', serializer: NullSerializer)
@serializer = options[:serializer] || Marshal
end

@ -336,7 +336,7 @@ def load
begin
@codepoints, @composition_exclusion, @composition_map, @boundary, @cp1252 = File.open(self.class.filename, 'rb') { |f| Marshal.load f.read }
rescue => e
raise IOError.new("Couldn't load the Unicode tables for UTF8Handler (#{e.message}), ActiveSupport::Multibyte is unusable")
raise IOError.new("Couldn't load the Unicode tables for UTF8Handler (#{e.message}), ActiveSupport::Multibyte is unusable")
end
# Redefine the === method so we can write shorter rules for grapheme cluster breaks
@ -368,6 +368,7 @@ def self.filename
private
def apply_mapping(string, mapping) #:nodoc:
database.codepoints
string.each_codepoint.map do |codepoint|
cp = database.codepoints[codepoint]
if cp and (ncp = cp.send(mapping)) and ncp > 0
@ -385,7 +386,6 @@ def recode_windows1252_chars(string)
def database
@database ||= UnicodeDatabase.new
end
end
end
end

@ -353,7 +353,12 @@ Instead of an options hash, you can also simply pass in a model, Rails will use
class ProductsController < ApplicationController
def show
@product = Product.find(params[:id])
respond_with(@product) if stale?(@product)
if stale?(@product)
respond_to do |wants|
# ... normal response processing
end
end
end
end
```

@ -397,7 +397,7 @@ inside, just indent it with 4 spaces:
class ArticlesController
def index
respond_with Article.limit(10)
render json: Article.limit(10)
end
end

@ -903,7 +903,7 @@ You can also specify multiple videos to play by passing an array of videos to th
This will produce:
```erb
<video><source src="trailer.ogg" /><source src="movie.ogg" /></video>
<video><source src="/videos/trailer.ogg" /><source src="/videos/trailer.flv" /></video>
```
#### Linking to Audio Files with the `audio_tag`

@ -256,7 +256,8 @@ def env_config
"action_dispatch.signed_cookie_salt" => config.action_dispatch.signed_cookie_salt,
"action_dispatch.encrypted_cookie_salt" => config.action_dispatch.encrypted_cookie_salt,
"action_dispatch.encrypted_signed_cookie_salt" => config.action_dispatch.encrypted_signed_cookie_salt,
"action_dispatch.cookies_serializer" => config.action_dispatch.cookies_serializer
"action_dispatch.cookies_serializer" => config.action_dispatch.cookies_serializer,
"action_dispatch.cookies_digest" => config.action_dispatch.cookies_digest
})
end
end