diff options
28 files changed, 294 insertions, 191 deletions
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 0c6973f9b6..52e0f68279 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,5 +1,14 @@ ## Rails 4.0.0 (unreleased) ## +* Create `UpgradeLegacySignedCookieJar` to transparently upgrade existing signed + cookies generated by Rails 3.x to avoid invalidating them when upgrading to Rails 4.x. + + *Trevor Turk + Neeraj Singh* + +* Raise an `ArgumentError` when a clashing named route is defined. + + *Trevor Turk* + * Allow default url options to accept host with protocol such as `http://` config.action_mailer.default_url_options = { host: "http://mydomain.com" } diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index 4103354d13..23d70c9ea2 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -421,7 +421,7 @@ module ActionController # Declaration { comment_ids: [] }. array_of_permitted_scalars_filter(params, key) else - # Declaration { user: :name } or { user: [:name, :age, { adress: ... }] }. + # Declaration { user: :name } or { user: [:name, :age, { address: ... }] }. params[key] = each_element(value) do |element| if element.is_a?(Hash) element = self.class.new(element) unless element.respond_to?(:permit) diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index 36a0db6e61..f21d1d4ee5 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -1,5 +1,6 @@ require 'active_support/core_ext/hash/keys' require 'active_support/core_ext/module/attribute_accessors' +require 'active_support/core_ext/object/blank' require 'active_support/key_generator' require 'active_support/message_verifier' @@ -86,7 +87,8 @@ module ActionDispatch SIGNED_COOKIE_SALT = "action_dispatch.signed_cookie_salt".freeze ENCRYPTED_COOKIE_SALT = "action_dispatch.encrypted_cookie_salt".freeze ENCRYPTED_SIGNED_COOKIE_SALT = "action_dispatch.encrypted_signed_cookie_salt".freeze - TOKEN_KEY = "action_dispatch.secret_token".freeze + SECRET_TOKEN = "action_dispatch.secret_token".freeze + SECRET_KEY_BASE = "action_dispatch.secret_key_base".freeze # Cookies can typically store 4096 bytes. MAX_COOKIE_SIZE = 4096 @@ -94,8 +96,68 @@ module ActionDispatch # Raised when storing more than 4K of session data. CookieOverflow = Class.new StandardError + # Include in a cookie jar to allow chaining, e.g. cookies.permanent.signed + module ChainedCookieJars + # Returns a jar that'll automatically set the assigned cookies to have an expiration date 20 years from now. Example: + # + # cookies.permanent[:prefers_open_id] = true + # # => Set-Cookie: prefers_open_id=true; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT + # + # This jar is only meant for writing. You'll read permanent cookies through the regular accessor. + # + # This jar allows chaining with the signed jar as well, so you can set permanent, signed cookies. Examples: + # + # cookies.permanent.signed[:remember_me] = current_user.id + # # => Set-Cookie: remember_me=BAhU--848956038e692d7046deab32b7131856ab20e14e; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT + def permanent + @permanent ||= PermanentCookieJar.new(self, @key_generator, @options) + end + + # Returns a jar that'll automatically generate a signed representation of cookie value and verify it when reading from + # the cookie again. This is useful for creating cookies with values that the user is not supposed to change. If a signed + # cookie was tampered with by the user (or a 3rd party), nil will be returned. + # + # This jar requires that you set a suitable secret for the verification on your app's +config.secret_key_base+. + # + # Example: + # + # cookies.signed[:discount] = 45 + # # => Set-Cookie: discount=BAhpMg==--2c1c6906c90a3bc4fd54a51ffb41dffa4bf6b5f7; path=/ + # + # cookies.signed[:discount] # => 45 + def signed + @signed ||= begin + if @options[:upgrade_legacy_signed_cookie_jar] + UpgradeLegacySignedCookieJar.new(self, @key_generator, @options) + else + SignedCookieJar.new(self, @key_generator, @options) + end + end + end + + # Only needed for supporting the +UpgradeSignatureToEncryptionCookieStore+, users and plugin authors should not use this + def signed_using_old_secret #:nodoc: + @signed_using_old_secret ||= SignedCookieJar.new(self, ActiveSupport::DummyKeyGenerator.new(@options[:secret_token]), @options) + end + + # Returns a jar that'll automatically encrypt cookie values before sending them to the client and will decrypt them for read. + # If the cookie was tampered with by the user (or a 3rd party), nil will be returned. + # + # This jar requires that you set a suitable secret for the verification on your app's +config.secret_key_base+. + # + # Example: + # + # cookies.encrypted[:discount] = 45 + # # => Set-Cookie: discount=ZS9ZZ1R4cG1pcUJ1bm80anhQang3dz09LS1mbDZDSU5scGdOT3ltQ2dTdlhSdWpRPT0%3D--ab54663c9f4e3bc340c790d6d2b71e92f5b60315; path=/ + # + # cookies.encrypted[:discount] # => 45 + def encrypted + @encrypted ||= EncryptedCookieJar.new(self, @key_generator, @options) + end + end + class CookieJar #:nodoc: - include Enumerable + include Enumerable, ChainedCookieJars # This regular expression is used to split the levels of a domain. # The top level domain can be any string without a period or @@ -115,7 +177,10 @@ module ActionDispatch { signed_cookie_salt: env[SIGNED_COOKIE_SALT] || '', encrypted_cookie_salt: env[ENCRYPTED_COOKIE_SALT] || '', encrypted_signed_cookie_salt: env[ENCRYPTED_SIGNED_COOKIE_SALT] || '', - token_key: env[TOKEN_KEY] } + secret_token: env[SECRET_TOKEN], + secret_key_base: env[SECRET_KEY_BASE], + upgrade_legacy_signed_cookie_jar: env[SECRET_TOKEN].present? && env[SECRET_KEY_BASE].present? + } end def self.build(request) @@ -232,59 +297,6 @@ module ActionDispatch @cookies.each_key{ |k| delete(k, options) } end - # Returns a jar that'll automatically set the assigned cookies to have an expiration date 20 years from now. Example: - # - # cookies.permanent[:prefers_open_id] = true - # # => Set-Cookie: prefers_open_id=true; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT - # - # This jar is only meant for writing. You'll read permanent cookies through the regular accessor. - # - # This jar allows chaining with the signed jar as well, so you can set permanent, signed cookies. Examples: - # - # cookies.permanent.signed[:remember_me] = current_user.id - # # => Set-Cookie: remember_me=BAhU--848956038e692d7046deab32b7131856ab20e14e; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT - def permanent - @permanent ||= PermanentCookieJar.new(self, @key_generator, @options) - end - - # Returns a jar that'll automatically generate a signed representation of cookie value and verify it when reading from - # the cookie again. This is useful for creating cookies with values that the user is not supposed to change. If a signed - # cookie was tampered with by the user (or a 3rd party), an ActiveSupport::MessageVerifier::InvalidSignature exception will - # be raised. - # - # This jar requires that you set a suitable secret for the verification on your app's +config.secret_key_base+. - # - # Example: - # - # cookies.signed[:discount] = 45 - # # => Set-Cookie: discount=BAhpMg==--2c1c6906c90a3bc4fd54a51ffb41dffa4bf6b5f7; path=/ - # - # cookies.signed[:discount] # => 45 - def signed - @signed ||= SignedCookieJar.new(self, @key_generator, @options) - end - - # Only needed for supporting the +UpgradeSignatureToEncryptionCookieStore+, users and plugin authors should not use this - def signed_using_old_secret #:nodoc: - @signed_using_old_secret ||= SignedCookieJar.new(self, ActiveSupport::DummyKeyGenerator.new(@options[:token_key]), @options) - end - - # Returns a jar that'll automatically encrypt cookie values before sending them to the client and will decrypt them for read. - # If the cookie was tampered with by the user (or a 3rd party), an ActiveSupport::MessageVerifier::InvalidSignature exception - # will be raised. - # - # This jar requires that you set a suitable secret for the verification on your app's +config.secret_key_base+. - # - # Example: - # - # cookies.encrypted[:discount] = 45 - # # => Set-Cookie: discount=ZS9ZZ1R4cG1pcUJ1bm80anhQang3dz09LS1mbDZDSU5scGdOT3ltQ2dTdlhSdWpRPT0%3D--ab54663c9f4e3bc340c790d6d2b71e92f5b60315; path=/ - # - # cookies.encrypted[:discount] # => 45 - def encrypted - @encrypted ||= EncryptedCookieJar.new(self, @key_generator, @options) - end - def write(headers) @set_cookies.each { |k, v| ::Rack::Utils.set_cookie_header!(headers, k, v) if write_cookie?(v) } @delete_cookies.each { |k, v| ::Rack::Utils.delete_cookie_header!(headers, k, v) } @@ -306,6 +318,8 @@ module ActionDispatch end class PermanentCookieJar #:nodoc: + include ChainedCookieJars + def initialize(parent_jar, key_generator, options = {}) @parent_jar = parent_jar @key_generator = key_generator @@ -326,26 +340,11 @@ module ActionDispatch options[:expires] = 20.years.from_now @parent_jar[key] = options end - - def permanent - @permanent ||= PermanentCookieJar.new(self, @key_generator, @options) - end - - def signed - @signed ||= SignedCookieJar.new(self, @key_generator, @options) - end - - def encrypted - @encrypted ||= EncryptedCookieJar.new(self, @key_generator, @options) - end - - def method_missing(method, *arguments, &block) - ActiveSupport::Deprecation.warn "#{method} is deprecated with no replacement. " + - "You probably want to try this method over the parent CookieJar." - end end class SignedCookieJar #:nodoc: + include ChainedCookieJars + def initialize(parent_jar, key_generator, options = {}) @parent_jar = parent_jar @options = options @@ -372,26 +371,42 @@ module ActionDispatch raise CookieOverflow if options[:value].size > MAX_COOKIE_SIZE @parent_jar[key] = options end + end - def permanent - @permanent ||= PermanentCookieJar.new(self, @key_generator, @options) + # UpgradeLegacySignedCookieJar is used instead of SignedCookieJar if + # config.secret_token and config.secret_key_base are both set. It reads + # legacy cookies signed with the old dummy key generator and re-saves + # them using the new key generator to provide a smooth upgrade path. + class UpgradeLegacySignedCookieJar < SignedCookieJar #:nodoc: + def initialize(*args) + super + @legacy_verifier = ActiveSupport::MessageVerifier.new(@options[:secret_token]) end - def signed - @signed ||= SignedCookieJar.new(self, @key_generator, @options) + def [](name) + if signed_message = @parent_jar[name] + verify_signed_message(signed_message) || verify_and_upgrade_legacy_signed_message(name, signed_message) + end end - def encrypted - @encrypted ||= EncryptedCookieJar.new(self, @key_generator, @options) + def verify_signed_message(signed_message) + @verifier.verify(signed_message) + rescue ActiveSupport::MessageVerifier::InvalidSignature + nil end - def method_missing(method, *arguments, &block) - ActiveSupport::Deprecation.warn "#{method} is deprecated with no replacement. " + - "You probably want to try this method over the parent CookieJar." + def verify_and_upgrade_legacy_signed_message(name, signed_message) + @legacy_verifier.verify(signed_message).tap do |value| + self[name] = value + end + rescue ActiveSupport::MessageVerifier::InvalidSignature + nil end end class EncryptedCookieJar #:nodoc: + include ChainedCookieJars + def initialize(parent_jar, key_generator, options = {}) if ActiveSupport::DummyKeyGenerator === key_generator raise "Encrypted Cookies must be used in conjunction with config.secret_key_base." + @@ -425,23 +440,6 @@ module ActionDispatch raise CookieOverflow if options[:value].size > MAX_COOKIE_SIZE @parent_jar[key] = options end - - def permanent - @permanent ||= PermanentCookieJar.new(self, @key_generator, @options) - end - - def signed - @signed ||= SignedCookieJar.new(self, @key_generator, @options) - end - - def encrypted - @encrypted ||= EncryptedCookieJar.new(self, @key_generator, @options) - end - - def method_missing(method, *arguments, &block) - ActiveSupport::Deprecation.warn "#{method} is deprecated with no replacement. " + - "You probably want to try this method over the parent CookieJar." - end end def initialize(app) diff --git a/actionpack/lib/action_dispatch/middleware/remote_ip.rb b/actionpack/lib/action_dispatch/middleware/remote_ip.rb index 93a2b52996..8879291dbd 100644 --- a/actionpack/lib/action_dispatch/middleware/remote_ip.rb +++ b/actionpack/lib/action_dispatch/middleware/remote_ip.rb @@ -101,7 +101,7 @@ module ActionDispatch (([0-9A-Fa-f]{1,4}:){0,4}:([0-9A-Fa-f]{1,4}:){1}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4 (::([0-9A-Fa-f]{1,4}:){0,5}((\b((25[0-5])|(1\d{2})|(2[0-4]\d) |(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4 ([0-9A-Fa-f]{1,4}::([0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4}) | # ip v6 with compatible to v4 - (::([0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4}) | # ip v6 with double colon at the begining + (::([0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4}) | # ip v6 with double colon at the beginning (([0-9A-Fa-f]{1,4}:){1,7}:) # ip v6 without ending )$) }x diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 619dd22ec1..7fb4719fa0 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -403,11 +403,19 @@ module ActionDispatch def add_route(app, conditions = {}, requirements = {}, defaults = {}, name = nil, anchor = true) raise ArgumentError, "Invalid route name: '#{name}'" unless name.blank? || name.to_s.match(/^[_a-z]\w*$/i) + if name && named_routes[name] + raise ArgumentError, "Invalid route name, already in use: '#{name}' \n" \ + "You may have defined two routes with the same name using the `:as` option, or " + "you may be overriding a route already defined by a resource with the same naming. " \ + "For the latter, you can restrict the routes created with `resources` as explained here: \n" \ + "http://guides.rubyonrails.org/routing.html#restricting-the-routes-created" + end + path = build_path(conditions.delete(:path_info), requirements, SEPARATORS, anchor) conditions = build_conditions(conditions, path.names.map { |x| x.to_sym }) route = @set.add_route(app, path, conditions, defaults, name) - named_routes[name] = route if name && !named_routes[name] + named_routes[name] = route if name route end diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb index 93e94f0f48..978c5aa7ac 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -908,12 +908,13 @@ class RouteSetTest < ActiveSupport::TestCase assert_equal set.routes.first, set.named_routes[:hello] end - def test_earlier_named_routes_take_precedence - set.draw do - get '/hello/world' => 'a#b', :as => 'hello' - get '/hello' => 'a#b', :as => 'hello' + def test_duplicate_named_route_raises_rather_than_pick_precedence + assert_raise ArgumentError do + set.draw do + get '/hello/world' => 'a#b', :as => 'hello' + get '/hello' => 'a#b', :as => 'hello' + end end - assert_equal set.routes.first, set.named_routes[:hello] end def setup_named_route_test diff --git a/actionpack/test/dispatch/cookies_test.rb b/actionpack/test/dispatch/cookies_test.rb index 5ada5a7603..892b89b12e 100644 --- a/actionpack/test/dispatch/cookies_test.rb +++ b/actionpack/test/dispatch/cookies_test.rb @@ -1,6 +1,7 @@ require 'abstract_unit' # FIXME remove DummyKeyGenerator and this require in 4.1 require 'active_support/key_generator' +require 'active_support/message_verifier' class CookiesTest < ActionController::TestCase class TestController < ActionController::Base @@ -67,6 +68,11 @@ class CookiesTest < ActionController::TestCase head :ok end + def get_signed_cookie + cookies.signed[:user_id] + head :ok + end + def set_encrypted_cookie cookies.encrypted[:foo] = 'bar' head :ok @@ -421,6 +427,55 @@ class CookiesTest < ActionController::TestCase } end + def test_signed_uses_signed_cookie_jar_if_only_secret_token_is_set + @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" + @request.env["action_dispatch.secret_key_base"] = nil + get :set_signed_cookie + assert_kind_of ActionDispatch::Cookies::SignedCookieJar, cookies.signed + end + + def test_signed_uses_signed_cookie_jar_if_only_secret_key_base_is_set + @request.env["action_dispatch.secret_token"] = nil + @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" + get :set_signed_cookie + assert_kind_of ActionDispatch::Cookies::SignedCookieJar, cookies.signed + end + + def test_signed_uses_upgrade_legacy_signed_cookie_jar_if_both_secret_token_and_secret_key_base_are_set + @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" + @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" + get :set_signed_cookie + assert_kind_of ActionDispatch::Cookies::UpgradeLegacySignedCookieJar, cookies.signed + end + + def test_legacy_signed_cookie_is_read_and_transparently_upgraded_if_both_secret_token_and_secret_key_base_are_set + @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" + @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" + + legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33").generate(45) + + @request.headers["Cookie"] = "user_id=#{legacy_value}" + get :get_signed_cookie + + assert_equal 45, @controller.send(:cookies).signed[:user_id] + + key_generator = @request.env["action_dispatch.key_generator"] + secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"]) + verifier = ActiveSupport::MessageVerifier.new(secret) + assert_equal 45, verifier.verify(@response.cookies["user_id"]) + end + + def test_legacy_signed_cookie_is_nil_if_tampered + @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" + @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" + + @request.headers["Cookie"] = "user_id=45" + get :get_signed_cookie + + assert_equal nil, @controller.send(:cookies).signed[:user_id] + assert_equal nil, @response.cookies["user_id"] + end + def test_cookie_with_all_domain_option get :set_cookie_with_domain assert_response :success diff --git a/actionpack/test/dispatch/mount_test.rb b/actionpack/test/dispatch/mount_test.rb index 3b008fdff0..e5e28c28be 100644 --- a/actionpack/test/dispatch/mount_test.rb +++ b/actionpack/test/dispatch/mount_test.rb @@ -21,7 +21,7 @@ class TestRoutingMount < ActionDispatch::IntegrationTest mount SprocketsApp, :at => "/sprockets" mount SprocketsApp => "/shorthand" - mount FakeEngine, :at => "/fakeengine" + mount FakeEngine, :at => "/fakeengine", :as => :fake mount FakeEngine, :at => "/getfake", :via => :get scope "/its_a" do diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index 2bf7056ff7..df359ba77d 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -2577,22 +2577,6 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert_raises(ActionController::UrlGenerationError){ list_todo_path(:list_id => '2', :id => '1') } end - def test_named_routes_collision_is_avoided_unless_explicitly_given_as - draw do - scope :as => "routes" do - get "/c/:id", :as => :collision, :to => "collision#show" - get "/collision", :to => "collision#show" - get "/no_collision", :to => "collision#show", :as => nil - - get "/fc/:id", :as => :forced_collision, :to => "forced_collision#show" - get "/forced_collision", :as => :forced_collision, :to => "forced_collision#show" - end - end - - assert_equal "/c/1", routes_collision_path(1) - assert_equal "/fc/1", routes_forced_collision_path(1) - end - def test_redirect_argument_error routes = Class.new { include ActionDispatch::Routing::Redirection }.new assert_raises(ArgumentError) { routes.redirect Object.new } @@ -2604,9 +2588,6 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest get "/c/:id", :as => :collision, :to => "collision#show" get "/collision", :to => "collision#show" get "/no_collision", :to => "collision#show", :as => nil - - get "/fc/:id", :as => :forced_collision, :to => "forced_collision#show" - get "/forced_collision", :as => :forced_collision, :to => "forced_collision#show" end end @@ -2657,6 +2638,24 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end end + def test_duplicate_route_name_raises_error + assert_raise(ArgumentError) do + draw do + get '/collision', :to => 'collision#show', :as => 'collision' + get '/duplicate', :to => 'duplicate#show', :as => 'collision' + end + end + end + + def test_duplicate_route_name_via_resources_raises_error + assert_raise(ArgumentError) do + draw do + resources :collisions + get '/collision', :to => 'collision#show', :as => 'collision' + end + end + end + def test_nested_route_in_nested_resource draw do resources :posts, :only => [:index, :show] do diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb index 5a9abe0204..139fb6795a 100644 --- a/activemodel/test/cases/errors_test.rb +++ b/activemodel/test/cases/errors_test.rb @@ -245,6 +245,7 @@ class ErrorsTest < ActiveModel::TestCase test 'full_message should return the given message with the attribute name included' do person = Person.new assert_equal "name can not be blank", person.errors.full_message(:name, "can not be blank") + assert_equal "name_test can not be blank", person.errors.full_message(:name_test, "can not be blank") end test 'should return a JSON hash representation of the errors' do diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 968dad5844..013f9b92b9 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -176,6 +176,7 @@ module ActiveRecord @columns_hash = self.class.column_types.dup init_internals + init_changed_attributes ensure_proper_type populate_with_current_scope_attributes @@ -246,9 +247,7 @@ module ActiveRecord run_callbacks(:initialize) unless _initialize_callbacks.empty? @changed_attributes = {} - self.class.column_defaults.each do |attr, orig_value| - @changed_attributes[attr] = orig_value if _field_changed?(attr, orig_value, @attributes[attr]) - end + init_changed_attributes @aggregation_cache = {} @association_cache = {} @@ -434,5 +433,14 @@ module ActiveRecord @transaction_state = nil @reflects_state = [false] end + + def init_changed_attributes + # Intentionally avoid using #column_defaults since overriden defaults (as is done in + # optimistic locking) won't get written unless they get marked as changed + self.class.columns.each do |c| + attr, orig_value = c.name, c.default + @changed_attributes[attr] = orig_value if _field_changed?(attr, orig_value, @attributes[attr]) + end + end end end diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 7fb50e9617..5cbb758e4c 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -1024,7 +1024,7 @@ class BasicsTest < ActiveRecord::TestCase Joke.reset_sequence_name end - def test_dont_clear_inheritnce_column_when_setting_explicitly + def test_dont_clear_inheritance_column_when_setting_explicitly Joke.inheritance_column = "my_type" before_inherit = Joke.inheritance_column diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index 0c896beb1d..77891b9156 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -193,11 +193,19 @@ class OptimisticLockingTest < ActiveRecord::TestCase def test_lock_without_default_sets_version_to_zero t1 = LockWithoutDefault.new assert_equal 0, t1.lock_version + + t1.save + t1 = LockWithoutDefault.find(t1.id) + assert_equal 0, t1.lock_version end def test_lock_with_custom_column_without_default_sets_version_to_zero t1 = LockWithCustomColumnWithoutDefault.new assert_equal 0, t1.custom_lock_version + + t1.save + t1 = LockWithCustomColumnWithoutDefault.find(t1.id) + assert_equal 0, t1.custom_lock_version end def test_readonly_attributes diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index 7042d7f4b6..b6e140b912 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -806,7 +806,7 @@ module NestedAttributesOnACollectionAssociationTests assert_nothing_raised(NoMethodError) { @pirate.save! } end - def test_numeric_colum_changes_from_zero_to_no_empty_string + def test_numeric_column_changes_from_zero_to_no_empty_string Man.accepts_nested_attributes_for(:interests) repair_validations(Interest) do diff --git a/activesupport/lib/active_support/message_verifier.rb b/activesupport/lib/active_support/message_verifier.rb index a87383fe99..e0cd92ae3c 100644 --- a/activesupport/lib/active_support/message_verifier.rb +++ b/activesupport/lib/active_support/message_verifier.rb @@ -19,10 +19,10 @@ module ActiveSupport # end # # By default it uses Marshal to serialize the message. If you want to use - # another serialization method, you can set the serializer attribute to - # something that responds to dump and load, e.g.: + # another serialization method, you can set the serializer in the options + # hash upon initialization: # - # @verifier.serializer = YAML + # @verifier = ActiveSupport::MessageVerifier.new('s3Krit', serializer: YAML) class MessageVerifier class InvalidSignature < StandardError; end diff --git a/guides/code/getting_started/Gemfile b/guides/code/getting_started/Gemfile index b355c7d91a..dca00b43cd 100644 --- a/guides/code/getting_started/Gemfile +++ b/guides/code/getting_started/Gemfile @@ -23,7 +23,7 @@ gem 'jquery-rails' gem 'turbolinks' # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder -gem 'jbuilder', '~> 1.0.1' +gem 'jbuilder', '~> 1.2' # To use ActiveModel has_secure_password # gem 'bcrypt-ruby', '~> 3.0.0' diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md index 0941bc7e58..a384e74d28 100644 --- a/guides/source/upgrading_ruby_on_rails.md +++ b/guides/source/upgrading_ruby_on_rails.md @@ -78,7 +78,17 @@ Rails 4.0 extracted Active Resource to its own gem. If you still need the featur ### Action Pack -* Rails 4.0 introduces a new `UpgradeSignatureToEncryptionCookieStore` cookie store. This is useful for upgrading apps using the old default `CookieStore` to the new default `EncryptedCookieStore`. To use this transitional cookie store, you'll want to leave your existing `secret_token` in place, add a new `secret_key_base`, and change your `session_store` like so: +* Rails 4.0 introduces `ActiveSupport::KeyGenerator` and uses this as a base from which to generate and verify signed cookies (among other things). Existing signed cookies generated with Rails 3.x will be transparently upgraded if you leave your existing `secret_token` in place and add the new `secret_key_base`. + +```ruby + # config/initializers/secret_token.rb + Myapp::Application.config.secret_token = 'existing secret token' + Myapp::Application.config.secret_key_base = 'new secret key base' +``` + +Please note that you should wait to set `secret_key_base` until you have 100% of your userbase on Rails 4.x and are reasonably sure you will not need to rollback to Rails 3.x. This is because cookies signed based on the new `secret_key_base` in Rails 4.x are not backwards compatible with Rails 3.x. You are free to leave your existing `secret_token` in place, not set the new `secret_key_base`, and ignore the deprecation warnings until you are reasonably sure that your upgrade is otherwise complete. + +* Rails 4.0 introduces a new `UpgradeSignatureToEncryptionCookieStore` cookie store. This is useful for upgrading apps using the old default `CookieStore` to the new default `EncryptedCookieStore` which leverages the new `ActiveSupport::KeyGenerator`. To use this transitional cookie store, you'll want to leave your existing `secret_token` in place, add a new `secret_key_base`, and change your `session_store` like so: ```ruby # config/initializers/session_store.rb @@ -103,32 +113,20 @@ Rails 4.0 extracted Active Resource to its own gem. If you still need the featur * Rails 4.0 changed how `assert_generates`, `assert_recognizes`, and `assert_routing` work. Now all these assertions raise `Assertion` instead of `ActionController::RoutingError`. -* Rails 4.0 correctly prefers the first named route defined in `config/routes.rb` if a clashing route is found later. Check the output of `rake routes` before upgrading and remove unused named routes to avoid issues. +* Rails 4.0 raises an `ArgumentError` if clashing named routes are defined. This can be triggered by explicitly defined named routes or by the `resources` method. Here are two examples that clash with routes named `example_path`: ```ruby - # config/routes.rb get 'one' => 'test#example', as: :example get 'two' => 'test#example', as: :example - - # Rails 3 - <%= example_path %> # => '/two' - - # Rails 4 - <%= example_path %> # => '/one' ``` ```ruby - # config/routes.rb resources :examples get 'clashing/:id' => 'test#example', as: :example - - # Rails 3 - <%= example_path(1) %> # => '/clashing/1' - - # Rails 4 - <%= example_path(1) %> # => '/examples/1' ``` +In the first case, you can simply avoid using the same name for multiple routes. In the second, you can use the `only` or `except` options provided by the `resources` method to restrict the routes created as detailed in the [Routing Guide](http://guides.rubyonrails.org/routing.html#restricting-the-routes-created). + * Rails 4.0 also changed the way unicode character routes are drawn. Now you can draw unicode character routes directly. If you already draw such routes, you must change them, for example: ```ruby diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index fb030dab4a..2727f1a85d 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,5 +1,17 @@ ## Rails 4.0.0 (unreleased) ## +* Allow vanilla apps to render CoffeeScript templates in production + + Vanilla apps already render CoffeeScript templates in development and test + environments. With this change, the production behavior matches that of + the other environments. + + Effectively, this meant moving coffee-rails (and the JavaScript runtime on + which it is dependent) from the :assets group to the top-level of the + generated Gemfile. + + *Gabe Kopley* + * `Rails.version` now returns an instance of `Gem::Version` *Charlie Somerville* diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index 0de44984d7..563905e8b3 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -149,6 +149,7 @@ module Rails "action_dispatch.parameter_filter" => config.filter_parameters, "action_dispatch.redirect_filter" => config.filter_redirect, "action_dispatch.secret_token" => config.secret_token, + "action_dispatch.secret_key_base" => config.secret_key_base, "action_dispatch.show_exceptions" => config.action_dispatch.show_exceptions, "action_dispatch.show_detailed_exceptions" => config.consider_all_requests_local, "action_dispatch.logger" => Rails.logger, diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb index e312afa091..4e05c32f74 100644 --- a/railties/lib/rails/generators/app_base.rb +++ b/railties/lib/rails/generators/app_base.rb @@ -178,29 +178,25 @@ module Rails return if options[:skip_sprockets] gemfile = if options.dev? || options.edge? - <<-GEMFILE + <<-GEMFILE.gsub(/^ {12}/, '') # Gems used only for assets and not required # in production environments by default. group :assets do gem 'sprockets-rails', github: 'rails/sprockets-rails' gem 'sass-rails', github: 'rails/sass-rails' - gem 'coffee-rails', github: 'rails/coffee-rails' - - # See https://github.com/sstephenson/execjs#readme for more supported runtimes - #{javascript_runtime_gemfile_entry} + #{coffee_gemfile_entry if options[:skip_javascript]} + #{javascript_runtime_gemfile_entry(2) if options[:skip_javascript]} gem 'uglifier', '>= 1.0.3' end GEMFILE else - <<-GEMFILE + <<-GEMFILE.gsub(/^ {12}/, '') # Gems used only for assets and not required # in production environments by default. group :assets do gem 'sass-rails', '~> 4.0.0.beta1' - gem 'coffee-rails', '~> 4.0.0.beta1' - - # See https://github.com/sstephenson/execjs#readme for more supported runtimes - #{javascript_runtime_gemfile_entry} + #{coffee_gemfile_entry if options[:skip_javascript]} + #{javascript_runtime_gemfile_entry(2) if options[:skip_javascript]} gem 'uglifier', '>= 1.0.3' end GEMFILE @@ -209,10 +205,24 @@ module Rails gemfile.strip_heredoc.gsub(/^[ \t]*$/, '') end + def coffee_gemfile_entry + if options.dev? || options.edge? + "gem 'coffee-rails', github: 'rails/coffee-rails'" + else + "gem 'coffee-rails', '~> 4.0.0.beta1'" + end + end + def javascript_gemfile_entry + args = {'jquery' => ", github: 'rails/jquery-rails'"} + unless options[:skip_javascript] - <<-GEMFILE.strip_heredoc - gem '#{options[:javascript]}-rails' + <<-GEMFILE.gsub(/^ {12}/, '').strip_heredoc + #{javascript_runtime_gemfile_entry} + # Use CoffeeScript for .js.coffee assets and views + #{coffee_gemfile_entry} + + gem '#{options[:javascript]}-rails'#{args[options[:javascript]]} # Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks gem 'turbolinks' @@ -220,12 +230,16 @@ module Rails end end - def javascript_runtime_gemfile_entry - if defined?(JRUBY_VERSION) - "gem 'therubyrhino'\n" + def javascript_runtime_gemfile_entry(n_spaces=0) + runtime = if defined?(JRUBY_VERSION) + "gem 'therubyrhino'" else - "# gem 'therubyracer', platforms: :ruby\n" + "# gem 'therubyracer', platforms: :ruby" end + <<-GEMFILE.gsub(/^ {10}/, '') + # See https://github.com/sstephenson/execjs#readme for more supported runtimes + #{" "*n_spaces}#{runtime} + GEMFILE end def bundle_command(command) diff --git a/railties/test/application/asset_debugging_test.rb b/railties/test/application/asset_debugging_test.rb index 1eddfac664..b3b40448c0 100644 --- a/railties/test/application/asset_debugging_test.rb +++ b/railties/test/application/asset_debugging_test.rb @@ -48,7 +48,7 @@ module ApplicationTests assert_no_match(/<script src="\/assets\/xmlhr-([0-z]+)\.js"><\/script>/, last_response.body) end - test "assets aren't concatened when compile is true is on and debug_assets params is true" do + test "assets aren't concatenated when compile is true is on and debug_assets params is true" do add_to_env_config "production", "config.assets.compile = true" ENV["RAILS_ENV"] = "production" diff --git a/railties/test/application/assets_test.rb b/railties/test/application/assets_test.rb index 638df8ca23..e0cbe73fc4 100644 --- a/railties/test/application/assets_test.rb +++ b/railties/test/application/assets_test.rb @@ -86,8 +86,8 @@ module ApplicationTests def test_precompile_does_not_hit_the_database app_file "app/assets/javascripts/application.js", "alert();" app_file "app/assets/javascripts/foo/application.js", "alert();" - app_file "app/controllers/user_controller.rb", <<-eoruby - class UserController < ApplicationController; end + app_file "app/controllers/users_controller.rb", <<-eoruby + class UsersController < ApplicationController; end eoruby app_file "app/models/user.rb", <<-eoruby class User < ActiveRecord::Base; end diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb index 7b45623f6c..1acf03f35a 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -599,7 +599,7 @@ module ApplicationTests assert_equal :log, ActionController::Parameters.action_on_unpermitted_parameters end - test "config.action_controller.action_on_unpermitted_parameters is :log by defaul on test" do + test "config.action_controller.action_on_unpermitted_parameters is :log by default on test" do ENV["RAILS_ENV"] = "test" require "#{app_path}/config/environment" diff --git a/railties/test/application/initializers/i18n_test.rb b/railties/test/application/initializers/i18n_test.rb index 489b7ddb92..17d0b10b70 100644 --- a/railties/test/application/initializers/i18n_test.rb +++ b/railties/test/application/initializers/i18n_test.rb @@ -45,7 +45,7 @@ module ApplicationTests end # Load paths - test "no config locales dir present should return empty load path" do + test "no config locales directory present should return empty load path" do FileUtils.rm_rf "#{app_path}/config/locales" load_app assert_equal [], Rails.application.config.i18n.load_path diff --git a/railties/test/application/initializers/load_path_test.rb b/railties/test/application/initializers/load_path_test.rb index 31811e7f92..9b18c329ec 100644 --- a/railties/test/application/initializers/load_path_test.rb +++ b/railties/test/application/initializers/load_path_test.rb @@ -23,7 +23,7 @@ module ApplicationTests assert $:.include?("#{app_path}/app/models") end - test "initializing an application allows to load code on lib path inside application class definitation" do + test "initializing an application allows to load code on lib path inside application class definition" do app_file "lib/foo.rb", <<-RUBY module Foo; end RUBY diff --git a/railties/test/application/middleware/cookies_test.rb b/railties/test/application/middleware/cookies_test.rb index 18af7abafc..bbb7627be9 100644 --- a/railties/test/application/middleware/cookies_test.rb +++ b/railties/test/application/middleware/cookies_test.rb @@ -33,7 +33,7 @@ module ApplicationTests assert_equal false, ActionDispatch::Cookies::CookieJar.always_write_cookie end - test 'always_write_cookie can be overrided' do + test 'always_write_cookie can be overridden' do add_to_config <<-RUBY config.action_dispatch.always_write_cookie = false RUBY diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index 0697035871..b813a7f6bb 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -232,8 +232,8 @@ class AppGeneratorTest < Rails::Generators::TestCase end assert_file "Gemfile" do |content| assert_no_match(/sass-rails/, content) - assert_no_match(/coffee-rails/, content) assert_no_match(/uglifier/, content) + assert_match(/coffee-rails/, content) end assert_file "config/environments/development.rb" do |content| assert_no_match(/config\.assets\.debug = true/, content) @@ -276,7 +276,7 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_match %r{^//= require jquery}, contents assert_match %r{^//= require jquery_ujs}, contents end - assert_gem "jquery-rails" + assert_file "Gemfile", /^gem 'jquery-rails'/ end def test_other_javascript_libraries @@ -293,6 +293,9 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_file "app/assets/javascripts/application.js" do |contents| assert_no_match %r{^//=\s+require\s}, contents end + assert_file "Gemfile" do |content| + assert_match(/coffee-rails/, content) + end end def test_inclusion_of_debugger diff --git a/tasks/release.rb b/tasks/release.rb index 650b381e0f..cf5b6d6843 100644 --- a/tasks/release.rb +++ b/tasks/release.rb @@ -23,20 +23,8 @@ directory "pkg" file = Dir[glob].first ruby = File.read(file) - major, minor, tiny, pre = version.split('.') - pre = pre ? pre.inspect : "nil" - - ruby.gsub!(/^(\s*)MAJOR = .*?$/, "\\1MAJOR = #{major}") - raise "Could not insert MAJOR in #{file}" unless $1 - - ruby.gsub!(/^(\s*)MINOR = .*?$/, "\\1MINOR = #{minor}") - raise "Could not insert MINOR in #{file}" unless $1 - - ruby.gsub!(/^(\s*)TINY = .*?$/, "\\1TINY = #{tiny}") - raise "Could not insert TINY in #{file}" unless $1 - - ruby.gsub!(/^(\s*)PRE = .*?$/, "\\1PRE = #{pre}") - raise "Could not insert PRE in #{file}" unless $1 + ruby.gsub!(/^(\s*)Gem::Version\.new .*?$/, "\\1Gem::Version.new \"#{version}\"") + raise "Could not insert Gem::Version in #{file}" unless $1 File.open(file, 'w') { |f| f.write ruby } end |