提交 8f15565d 编写于 作者: D David Heinemeier Hansson

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

......@@ -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'
......
* 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
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
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
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
# 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
......
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
......
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
......
<content>I should not be displayed</content>
\ No newline at end of file
<customer-name><%= @customer.name %></customer-name>
\ No newline at end of file
......@@ -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
* 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
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册