diff options
118 files changed, 770 insertions, 443 deletions
diff --git a/Gemfile.lock b/Gemfile.lock index 22418fa5e6..e6640a116b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -148,10 +148,10 @@ GEM daemons (1.2.4) dalli (2.7.6) dante (0.2.0) - delayed_job (4.1.2) - activesupport (>= 3.0, < 5.1) - delayed_job_active_record (4.1.1) - activerecord (>= 3.0, < 5.1) + delayed_job (4.1.3) + activesupport (>= 3.0, < 5.2) + delayed_job_active_record (4.1.2) + activerecord (>= 3.0, < 5.2) delayed_job (>= 3.0, < 5) em-hiredis (0.3.1) eventmachine (~> 1.0) @@ -283,7 +283,7 @@ GEM redis (~> 3.3) resque (~> 1.26) rufus-scheduler (~> 3.2) - rubocop (0.49.0) + rubocop (0.49.1) parallel (~> 1.10) parser (>= 2.3.3.1, < 3.0) powerpack (~> 0.1) diff --git a/actioncable/lib/action_cable/channel/periodic_timers.rb b/actioncable/lib/action_cable/channel/periodic_timers.rb index c9daa0bcd3..90c68cfe84 100644 --- a/actioncable/lib/action_cable/channel/periodic_timers.rb +++ b/actioncable/lib/action_cable/channel/periodic_timers.rb @@ -4,8 +4,7 @@ module ActionCable extend ActiveSupport::Concern included do - class_attribute :periodic_timers, instance_reader: false - self.periodic_timers = [] + class_attribute :periodic_timers, instance_reader: false, default: [] after_subscribe :start_periodic_timers after_unsubscribe :stop_periodic_timers diff --git a/actioncable/lib/action_cable/connection/identification.rb b/actioncable/lib/action_cable/connection/identification.rb index c91a1d1fd7..ffab359429 100644 --- a/actioncable/lib/action_cable/connection/identification.rb +++ b/actioncable/lib/action_cable/connection/identification.rb @@ -6,8 +6,7 @@ module ActionCable extend ActiveSupport::Concern included do - class_attribute :identifiers - self.identifiers = Set.new + class_attribute :identifiers, default: Set.new end class_methods do diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index 6849f5c0f9..7133670b65 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -459,8 +459,7 @@ module ActionMailer helper ActionMailer::MailHelper - class_attribute :default_params - self.default_params = { + class_attribute :default_params, default: { mime_version: "1.0", charset: "UTF-8", content_type: "text/plain", diff --git a/actionmailer/lib/action_mailer/delivery_methods.rb b/actionmailer/lib/action_mailer/delivery_methods.rb index bcc4ef03cf..afdc28aa2a 100644 --- a/actionmailer/lib/action_mailer/delivery_methods.rb +++ b/actionmailer/lib/action_mailer/delivery_methods.rb @@ -7,8 +7,6 @@ module ActionMailer extend ActiveSupport::Concern included do - class_attribute :delivery_methods, :delivery_method - # Do not make this inheritable, because we always want it to propagate cattr_accessor :raise_delivery_errors self.raise_delivery_errors = true @@ -19,8 +17,8 @@ module ActionMailer cattr_accessor :deliver_later_queue_name self.deliver_later_queue_name = :mailers - self.delivery_methods = {}.freeze - self.delivery_method = :smtp + class_attribute :delivery_methods, default: {}.freeze + class_attribute :delivery_method, default: :smtp add_delivery_method :smtp, Mail::SMTP, address: "localhost", diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 86ea9a7ce6..54937617df 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,3 +1,13 @@ +* AEAD encrypted cookies and sessions with GCM + + Encrypted cookies now use AES-GCM which couples authentication and + encryption in one faster step and produces shorter ciphertexts. Cookies + encrypted using AES in CBC HMAC mode will be seamlessly upgraded when + this new mode is enabled via the + `action_dispatch.use_authenticated_cookie_encryption` configuration value. + + *Michael J Coyne* + * Change the cache key format for fragments to make it easier to debug key churn. The new format is: views/template/action.html.erb:7a1156131a6928cb0026877f8b749ac9/projects/123 diff --git a/actionpack/lib/abstract_controller/caching.rb b/actionpack/lib/abstract_controller/caching.rb index 26e3f08bc1..30e3d4426c 100644 --- a/actionpack/lib/abstract_controller/caching.rb +++ b/actionpack/lib/abstract_controller/caching.rb @@ -37,8 +37,7 @@ module AbstractController config_accessor :enable_fragment_cache_logging self.enable_fragment_cache_logging = false - class_attribute :_view_cache_dependencies - self._view_cache_dependencies = [] + class_attribute :_view_cache_dependencies, default: [] helper_method :view_cache_dependencies if respond_to?(:helper_method) end diff --git a/actionpack/lib/abstract_controller/helpers.rb b/actionpack/lib/abstract_controller/helpers.rb index ef3be7af83..2e50637c39 100644 --- a/actionpack/lib/abstract_controller/helpers.rb +++ b/actionpack/lib/abstract_controller/helpers.rb @@ -5,11 +5,8 @@ module AbstractController extend ActiveSupport::Concern included do - class_attribute :_helpers - self._helpers = Module.new - - class_attribute :_helper_methods - self._helper_methods = Array.new + class_attribute :_helpers, default: Module.new + class_attribute :_helper_methods, default: Array.new end class MissingHelperError < LoadError diff --git a/actionpack/lib/action_controller/metal.rb b/actionpack/lib/action_controller/metal.rb index 246644dcbd..96c708f45a 100644 --- a/actionpack/lib/action_controller/metal.rb +++ b/actionpack/lib/action_controller/metal.rb @@ -208,8 +208,7 @@ module ActionController @_request.reset_session end - class_attribute :middleware_stack - self.middleware_stack = ActionController::MiddlewareStack.new + class_attribute :middleware_stack, default: ActionController::MiddlewareStack.new def self.inherited(base) # :nodoc: base.middleware_stack = middleware_stack.dup diff --git a/actionpack/lib/action_controller/metal/conditional_get.rb b/actionpack/lib/action_controller/metal/conditional_get.rb index eb636fa3f6..0525252c7c 100644 --- a/actionpack/lib/action_controller/metal/conditional_get.rb +++ b/actionpack/lib/action_controller/metal/conditional_get.rb @@ -7,8 +7,7 @@ module ActionController include Head included do - class_attribute :etaggers - self.etaggers = [] + class_attribute :etaggers, default: [] end module ClassMethods diff --git a/actionpack/lib/action_controller/metal/etag_with_template_digest.rb b/actionpack/lib/action_controller/metal/etag_with_template_digest.rb index 798564db96..69c3979a0e 100644 --- a/actionpack/lib/action_controller/metal/etag_with_template_digest.rb +++ b/actionpack/lib/action_controller/metal/etag_with_template_digest.rb @@ -22,8 +22,7 @@ module ActionController include ActionController::ConditionalGet included do - class_attribute :etag_with_template_digest - self.etag_with_template_digest = true + class_attribute :etag_with_template_digest, default: true ActiveSupport.on_load :action_view, yield: true do etag do |options| diff --git a/actionpack/lib/action_controller/metal/flash.rb b/actionpack/lib/action_controller/metal/flash.rb index 347fbf0e74..24d1097ebe 100644 --- a/actionpack/lib/action_controller/metal/flash.rb +++ b/actionpack/lib/action_controller/metal/flash.rb @@ -3,8 +3,7 @@ module ActionController #:nodoc: extend ActiveSupport::Concern included do - class_attribute :_flash_types, instance_accessor: false - self._flash_types = [] + class_attribute :_flash_types, instance_accessor: false, default: [] delegate :flash, to: :request add_flash_types(:alert, :notice) diff --git a/actionpack/lib/action_controller/metal/helpers.rb b/actionpack/lib/action_controller/metal/helpers.rb index 476d081239..913a4b9a04 100644 --- a/actionpack/lib/action_controller/metal/helpers.rb +++ b/actionpack/lib/action_controller/metal/helpers.rb @@ -53,9 +53,8 @@ module ActionController include AbstractController::Helpers included do - class_attribute :helpers_path, :include_all_helpers - self.helpers_path ||= [] - self.include_all_helpers = true + class_attribute :helpers_path, default: [] + class_attribute :include_all_helpers, default: true end module ClassMethods diff --git a/actionpack/lib/action_controller/metal/params_wrapper.rb b/actionpack/lib/action_controller/metal/params_wrapper.rb index a89fc1678b..68881b8402 100644 --- a/actionpack/lib/action_controller/metal/params_wrapper.rb +++ b/actionpack/lib/action_controller/metal/params_wrapper.rb @@ -159,8 +159,7 @@ module ActionController end included do - class_attribute :_wrapper_options - self._wrapper_options = Options.from_hash(format: []) + class_attribute :_wrapper_options, default: Options.from_hash(format: []) end module ClassMethods diff --git a/actionpack/lib/action_controller/metal/renderers.rb b/actionpack/lib/action_controller/metal/renderers.rb index 733aca195d..23c21b0501 100644 --- a/actionpack/lib/action_controller/metal/renderers.rb +++ b/actionpack/lib/action_controller/metal/renderers.rb @@ -26,8 +26,7 @@ module ActionController RENDERERS = Set.new included do - class_attribute :_renderers - self._renderers = Set.new.freeze + class_attribute :_renderers, default: Set.new.freeze end # Used in <tt>ActionController::Base</tt> diff --git a/actionpack/lib/action_dispatch/journey/route.rb b/actionpack/lib/action_dispatch/journey/route.rb index 7bc15aa6b3..0acbac1d9d 100644 --- a/actionpack/lib/action_dispatch/journey/route.rb +++ b/actionpack/lib/action_dispatch/journey/route.rb @@ -10,11 +10,11 @@ module ActionDispatch module VerbMatchers VERBS = %w{ DELETE GET HEAD OPTIONS LINK PATCH POST PUT TRACE UNLINK } VERBS.each do |v| - class_eval <<-eoc - class #{v} - def self.verb; name.split("::").last; end - def self.call(req); req.#{v.downcase}?; end - end + class_eval <<-eoc, __FILE__, __LINE__ + 1 + class #{v} + def self.verb; name.split("::").last; end + def self.call(req); req.#{v.downcase}?; end + end eoc end diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index e565c03a8a..c0dda1bba5 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -43,6 +43,10 @@ module ActionDispatch get_header Cookies::ENCRYPTED_SIGNED_COOKIE_SALT end + def authenticated_encrypted_cookie_salt + get_header Cookies::AUTHENTICATED_ENCRYPTED_COOKIE_SALT + end + def secret_token get_header Cookies::SECRET_TOKEN end @@ -149,6 +153,7 @@ 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 + AUTHENTICATED_ENCRYPTED_COOKIE_SALT = "action_dispatch.authenticated_encrypted_cookie_salt".freeze SECRET_TOKEN = "action_dispatch.secret_token".freeze SECRET_KEY_BASE = "action_dispatch.secret_key_base".freeze COOKIES_SERIALIZER = "action_dispatch.cookies_serializer".freeze @@ -207,6 +212,9 @@ module ActionDispatch # If +secrets.secret_key_base+ and +secrets.secret_token+ (deprecated) are both set, # legacy cookies signed with the old key generator will be transparently upgraded. # + # If +config.action_dispatch.encrypted_cookie_salt+ and +config.action_dispatch.encrypted_signed_cookie_salt+ + # are both set, legacy cookies encrypted with HMAC AES-256-CBC will be transparently upgraded. + # # This jar requires that you set a suitable secret for the verification on your app's +secrets.secret_key_base+. # # Example: @@ -219,6 +227,8 @@ module ActionDispatch @encrypted ||= if upgrade_legacy_signed_cookies? UpgradeLegacyEncryptedCookieJar.new(self) + elsif upgrade_legacy_hmac_aes_cbc_cookies? + UpgradeLegacyHmacAesCbcCookieJar.new(self) else EncryptedCookieJar.new(self) end @@ -240,6 +250,13 @@ module ActionDispatch def upgrade_legacy_signed_cookies? request.secret_token.present? && request.secret_key_base.present? end + + def upgrade_legacy_hmac_aes_cbc_cookies? + request.secret_key_base.present? && + request.authenticated_encrypted_cookie_salt.present? && + request.encrypted_signed_cookie_salt.present? && + request.encrypted_cookie_salt.present? + end end # Passing the ActiveSupport::MessageEncryptor::NullSerializer downstream @@ -576,9 +593,11 @@ module ActionDispatch "Read the upgrade documentation to learn more about this new config option." end - secret = key_generator.generate_key(request.encrypted_cookie_salt || "")[0, ActiveSupport::MessageEncryptor.key_len] - sign_secret = key_generator.generate_key(request.encrypted_signed_cookie_salt || "") - @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer) + cipher = "aes-256-gcm" + key_len = ActiveSupport::MessageEncryptor.key_len(cipher) + secret = key_generator.generate_key(request.authenticated_encrypted_cookie_salt || "")[0, key_len] + + @encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: ActiveSupport::MessageEncryptor::NullSerializer) end private @@ -603,6 +622,32 @@ module ActionDispatch include VerifyAndUpgradeLegacySignedMessage end + # UpgradeLegacyHmacAesCbcCookieJar is used by ActionDispatch::Session::CookieStore + # to upgrade cookies encrypted with AES-256-CBC with HMAC to AES-256-GCM + class UpgradeLegacyHmacAesCbcCookieJar < EncryptedCookieJar + def initialize(parent_jar) + super + + secret = key_generator.generate_key(request.encrypted_cookie_salt || "")[0, ActiveSupport::MessageEncryptor.key_len] + sign_secret = key_generator.generate_key(request.encrypted_signed_cookie_salt || "") + + @legacy_encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer) + end + + def decrypt_and_verify_legacy_encrypted_message(name, signed_message) + deserialize(name, @legacy_encryptor.decrypt_and_verify(signed_message)).tap do |value| + self[name] = { value: value } + end + rescue ActiveSupport::MessageVerifier::InvalidSignature, ActiveSupport::MessageEncryptor::InvalidMessage + nil + end + + private + def parse(name, signed_message) + super || decrypt_and_verify_legacy_encrypted_message(name, signed_message) + end + end + def initialize(app) @app = app end diff --git a/actionpack/lib/action_dispatch/railtie.rb b/actionpack/lib/action_dispatch/railtie.rb index 16a18a7f25..7662e164b8 100644 --- a/actionpack/lib/action_dispatch/railtie.rb +++ b/actionpack/lib/action_dispatch/railtie.rb @@ -16,6 +16,7 @@ module ActionDispatch config.action_dispatch.signed_cookie_salt = "signed cookie" config.action_dispatch.encrypted_cookie_salt = "encrypted cookie" config.action_dispatch.encrypted_signed_cookie_salt = "signed encrypted cookie" + config.action_dispatch.use_authenticated_cookie_encryption = false config.action_dispatch.perform_deep_munge = true config.action_dispatch.default_headers = { @@ -36,6 +37,8 @@ module ActionDispatch ActionDispatch::ExceptionWrapper.rescue_responses.merge!(config.action_dispatch.rescue_responses) ActionDispatch::ExceptionWrapper.rescue_templates.merge!(config.action_dispatch.rescue_templates) + config.action_dispatch.authenticated_encrypted_cookie_salt = "authenticated encrypted cookie" if config.action_dispatch.use_authenticated_cookie_encryption + config.action_dispatch.always_write_cookie = Rails.env.development? if config.action_dispatch.always_write_cookie.nil? ActionDispatch::Cookies::CookieJar.always_write_cookie = config.action_dispatch.always_write_cookie diff --git a/actionpack/lib/action_dispatch/request/session.rb b/actionpack/lib/action_dispatch/request/session.rb index 74ba6466cf..3547a8604f 100644 --- a/actionpack/lib/action_dispatch/request/session.rb +++ b/actionpack/lib/action_dispatch/request/session.rb @@ -101,11 +101,13 @@ module ActionDispatch # Returns keys of the session as Array. def keys + load_for_read! @delegate.keys end # Returns values of the session as Array. def values + load_for_read! @delegate.values end diff --git a/actionpack/test/dispatch/cookies_test.rb b/actionpack/test/dispatch/cookies_test.rb index 664faa31bb..e5646de82e 100644 --- a/actionpack/test/dispatch/cookies_test.rb +++ b/actionpack/test/dispatch/cookies_test.rb @@ -288,8 +288,7 @@ class CookiesTest < ActionController::TestCase @request.env["action_dispatch.key_generator"] = ActiveSupport::KeyGenerator.new(SALT, iterations: 2) @request.env["action_dispatch.signed_cookie_salt"] = - @request.env["action_dispatch.encrypted_cookie_salt"] = - @request.env["action_dispatch.encrypted_signed_cookie_salt"] = SALT + @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] = SALT @request.host = "www.nextangle.com" end @@ -531,9 +530,7 @@ class CookiesTest < ActionController::TestCase get :set_encrypted_cookie cookies = @controller.send :cookies assert_not_equal "bar", cookies[:foo] - assert_raise TypeError do - cookies.signed[:foo] - end + assert_nil cookies.signed[:foo] assert_equal "bar", cookies.encrypted[:foo] end @@ -542,9 +539,7 @@ class CookiesTest < ActionController::TestCase get :set_encrypted_cookie cookies = @controller.send :cookies assert_not_equal "bar", cookies[:foo] - assert_raises TypeError do - cookies.signed[:foo] - end + assert_nil cookies.signed[:foo] assert_equal "bar", cookies.encrypted[:foo] end @@ -553,9 +548,7 @@ class CookiesTest < ActionController::TestCase get :set_encrypted_cookie cookies = @controller.send :cookies assert_not_equal "bar", cookies[:foo] - assert_raises ::JSON::ParserError do - cookies.signed[:foo] - end + assert_nil cookies.signed[:foo] assert_equal "bar", cookies.encrypted[:foo] end @@ -564,9 +557,7 @@ class CookiesTest < ActionController::TestCase 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_nil cookies.signed[:foo] assert_equal "wrapped: bar", cookies.encrypted[:foo] end @@ -577,38 +568,16 @@ class CookiesTest < ActionController::TestCase 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 - key_generator = @request.env["action_dispatch.key_generator"] - encrypted_cookie_salt = @request.env["action_dispatch.encrypted_cookie_salt"] - encrypted_signed_cookie_salt = @request.env["action_dispatch.encrypted_signed_cookie_salt"] - secret = key_generator.generate_key(encrypted_cookie_salt) - sign_secret = key_generator.generate_key(encrypted_signed_cookie_salt) + cipher = "aes-256-gcm" + salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] + secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len(cipher)] + encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: Marshal) - marshal_value = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len], sign_secret, serializer: Marshal).encrypt_and_sign("bar") - @request.headers["Cookie"] = "foo=#{marshal_value}" + marshal_value = encryptor.encrypt_and_sign("bar") + @request.headers["Cookie"] = "foo=#{::Rack::Utils.escape marshal_value}" get :get_encrypted_cookie @@ -616,40 +585,28 @@ class CookiesTest < ActionController::TestCase assert_not_equal "bar", cookies[:foo] assert_equal "bar", cookies.encrypted[:foo] - encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len], sign_secret, serializer: JSON) - assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"]) + json_encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: JSON) + assert_not_nil @response.cookies["foo"] + assert_equal "bar", json_encryptor.decrypt_and_verify(@response.cookies["foo"]) end def test_encrypted_cookie_using_hybrid_serializer_can_read_from_json_dumped_value @request.env["action_dispatch.cookies_serializer"] = :hybrid - key_generator = @request.env["action_dispatch.key_generator"] - encrypted_cookie_salt = @request.env["action_dispatch.encrypted_cookie_salt"] - encrypted_signed_cookie_salt = @request.env["action_dispatch.encrypted_signed_cookie_salt"] - secret = key_generator.generate_key(encrypted_cookie_salt) - sign_secret = key_generator.generate_key(encrypted_signed_cookie_salt) - json_value = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len], sign_secret, serializer: JSON).encrypt_and_sign("bar") - @request.headers["Cookie"] = "foo=#{json_value}" - - get :get_encrypted_cookie + cipher = "aes-256-gcm" + salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] + secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len(cipher)] + encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: JSON) - cookies = @controller.send :cookies - assert_not_equal "bar", cookies[:foo] - assert_equal "bar", cookies.encrypted[:foo] - - assert_nil @response.cookies["foo"] - end - - def test_compat_encrypted_cookie_using_64_byte_key - # Cookie generated with 64 bytes secret - message = ["566d4e75536d686e633246564e6b493062557079626c566d51574d30515430394c53315665564a694e4563786555744f57537454576b396a5a31566a626e52525054303d2d2d34663234333330623130623261306163363562316266323335396164666364613564643134623131"].pack("H*") - @request.headers["Cookie"] = "foo=#{message}" + json_value = encryptor.encrypt_and_sign("bar") + @request.headers["Cookie"] = "foo=#{::Rack::Utils.escape json_value}" get :get_encrypted_cookie cookies = @controller.send :cookies assert_not_equal "bar", cookies[:foo] assert_equal "bar", cookies.encrypted[:foo] + assert_nil @response.cookies["foo"] end @@ -813,10 +770,10 @@ class CookiesTest < ActionController::TestCase assert_equal "bar", @controller.send(:cookies).encrypted[:foo] - key_generator = @request.env["action_dispatch.key_generator"] - secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_cookie_salt"]) - sign_secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_signed_cookie_salt"]) - encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len], sign_secret) + cipher = "aes-256-gcm" + salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] + secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len(cipher)] + encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: Marshal) assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"]) end @@ -842,8 +799,6 @@ class CookiesTest < ActionController::TestCase @request.env["action_dispatch.cookies_serializer"] = :json @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" - @request.env["action_dispatch.encrypted_cookie_salt"] = "4433796b79d99a7735553e316522acee" - @request.env["action_dispatch.encrypted_signed_cookie_salt"] = "00646eb40062e1b1deff205a27cd30f9" legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33", serializer: JSON).generate("bar") @@ -852,10 +807,10 @@ class CookiesTest < ActionController::TestCase assert_equal "bar", @controller.send(:cookies).encrypted[:foo] - key_generator = @request.env["action_dispatch.key_generator"] - secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_cookie_salt"]) - sign_secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_signed_cookie_salt"]) - encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len], sign_secret, serializer: JSON) + cipher = "aes-256-gcm" + salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] + secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len(cipher)] + encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: JSON) assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"]) end @@ -881,8 +836,6 @@ class CookiesTest < ActionController::TestCase @request.env["action_dispatch.cookies_serializer"] = :hybrid @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" - @request.env["action_dispatch.encrypted_cookie_salt"] = "4433796b79d99a7735553e316522acee" - @request.env["action_dispatch.encrypted_signed_cookie_salt"] = "00646eb40062e1b1deff205a27cd30f9" legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33", serializer: JSON).generate("bar") @@ -891,10 +844,10 @@ class CookiesTest < ActionController::TestCase assert_equal "bar", @controller.send(:cookies).encrypted[:foo] - key_generator = @request.env["action_dispatch.key_generator"] - secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_cookie_salt"]) - sign_secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_signed_cookie_salt"]) - encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len], sign_secret, serializer: JSON) + cipher = "aes-256-gcm" + salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] + secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len(cipher)] + encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: JSON) assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"]) end @@ -920,8 +873,6 @@ class CookiesTest < ActionController::TestCase @request.env["action_dispatch.cookies_serializer"] = :hybrid @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" - @request.env["action_dispatch.encrypted_cookie_salt"] = "4433796b79d99a7735553e316522acee" - @request.env["action_dispatch.encrypted_signed_cookie_salt"] = "00646eb40062e1b1deff205a27cd30f9" legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33").generate("bar") @@ -930,10 +881,10 @@ class CookiesTest < ActionController::TestCase assert_equal "bar", @controller.send(:cookies).encrypted[:foo] - key_generator = @request.env["action_dispatch.key_generator"] - secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_cookie_salt"]) - sign_secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_signed_cookie_salt"]) - encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len], sign_secret, serializer: JSON) + cipher = "aes-256-gcm" + salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] + secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len(cipher)] + encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: JSON) assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"]) end @@ -959,6 +910,89 @@ class CookiesTest < ActionController::TestCase assert_nil @response.cookies["foo"] end + def test_legacy_hmac_aes_cbc_encrypted_marshal_cookie_is_upgraded_to_authenticated_encrypted_cookie + @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" + + @request.env["action_dispatch.encrypted_cookie_salt"] = + @request.env["action_dispatch.encrypted_signed_cookie_salt"] = SALT + + key_generator = @request.env["action_dispatch.key_generator"] + encrypted_cookie_salt = @request.env["action_dispatch.encrypted_cookie_salt"] + encrypted_signed_cookie_salt = @request.env["action_dispatch.encrypted_signed_cookie_salt"] + secret = key_generator.generate_key(encrypted_cookie_salt) + sign_secret = key_generator.generate_key(encrypted_signed_cookie_salt) + marshal_value = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len], sign_secret, serializer: Marshal).encrypt_and_sign("bar") + + @request.headers["Cookie"] = "foo=#{marshal_value}" + + get :get_encrypted_cookie + + cookies = @controller.send :cookies + assert_not_equal "bar", cookies[:foo] + assert_equal "bar", cookies.encrypted[:foo] + + aead_cipher = "aes-256-gcm" + aead_salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] + aead_secret = key_generator.generate_key(aead_salt)[0, ActiveSupport::MessageEncryptor.key_len(aead_cipher)] + aead_encryptor = ActiveSupport::MessageEncryptor.new(aead_secret, cipher: aead_cipher, serializer: Marshal) + + assert_equal "bar", aead_encryptor.decrypt_and_verify(@response.cookies["foo"]) + end + + def test_legacy_hmac_aes_cbc_encrypted_json_cookie_is_upgraded_to_authenticated_encrypted_cookie + @request.env["action_dispatch.cookies_serializer"] = :json + @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" + + @request.env["action_dispatch.encrypted_cookie_salt"] = + @request.env["action_dispatch.encrypted_signed_cookie_salt"] = SALT + + key_generator = @request.env["action_dispatch.key_generator"] + encrypted_cookie_salt = @request.env["action_dispatch.encrypted_cookie_salt"] + encrypted_signed_cookie_salt = @request.env["action_dispatch.encrypted_signed_cookie_salt"] + secret = key_generator.generate_key(encrypted_cookie_salt) + sign_secret = key_generator.generate_key(encrypted_signed_cookie_salt) + marshal_value = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len], sign_secret, serializer: JSON).encrypt_and_sign("bar") + + @request.headers["Cookie"] = "foo=#{marshal_value}" + + get :get_encrypted_cookie + + cookies = @controller.send :cookies + assert_not_equal "bar", cookies[:foo] + assert_equal "bar", cookies.encrypted[:foo] + + aead_cipher = "aes-256-gcm" + aead_salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] + aead_secret = key_generator.generate_key(aead_salt)[0, ActiveSupport::MessageEncryptor.key_len(aead_cipher)] + aead_encryptor = ActiveSupport::MessageEncryptor.new(aead_secret, cipher: aead_cipher, serializer: JSON) + + assert_equal "bar", aead_encryptor.decrypt_and_verify(@response.cookies["foo"]) + end + + def test_legacy_hmac_aes_cbc_encrypted_cookie_using_64_byte_key_is_upgraded_to_authenticated_encrypted_cookie + @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" + + @request.env["action_dispatch.encrypted_cookie_salt"] = + @request.env["action_dispatch.encrypted_signed_cookie_salt"] = SALT + + # Cookie generated with 64 bytes secret + message = ["566d4e75536d686e633246564e6b493062557079626c566d51574d30515430394c53315665564a694e4563786555744f57537454576b396a5a31566a626e52525054303d2d2d34663234333330623130623261306163363562316266323335396164666364613564643134623131"].pack("H*") + @request.headers["Cookie"] = "foo=#{message}" + + get :get_encrypted_cookie + + cookies = @controller.send :cookies + assert_not_equal "bar", cookies[:foo] + assert_equal "bar", cookies.encrypted[:foo] + cipher = "aes-256-gcm" + + salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] + secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len(cipher)] + encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: Marshal) + + assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"]) + end + def test_cookie_with_all_domain_option get :set_cookie_with_domain assert_response :success diff --git a/actionpack/test/dispatch/request/session_test.rb b/actionpack/test/dispatch/request/session_test.rb index 311b80ea0a..228135c547 100644 --- a/actionpack/test/dispatch/request/session_test.rb +++ b/actionpack/test/dispatch/request/session_test.rb @@ -54,6 +54,11 @@ module ActionDispatch assert_equal %w[rails adequate], s.keys end + def test_keys_with_deferred_loading + s = Session.create(store_with_data, req, {}) + assert_equal %w[sample_key], s.keys + end + def test_values s = Session.create(store, req, {}) s["rails"] = "ftw" @@ -61,6 +66,11 @@ module ActionDispatch assert_equal %w[ftw awesome], s.values end + def test_values_with_deferred_loading + s = Session.create(store_with_data, req, {}) + assert_equal %w[sample_value], s.values + end + def test_clear s = Session.create(store, req, {}) s["rails"] = "ftw" @@ -113,6 +123,14 @@ module ActionDispatch def delete_session(env, id, options); 123; end }.new end + + def store_with_data + Class.new { + def load_session(env); [1, { "sample_key" => "sample_value" }]; end + def session_exists?(env); true; end + def delete_session(env, id, options); 123; end + }.new + end end class SessionIntegrationTest < ActionDispatch::IntegrationTest diff --git a/actionview/app/assets/javascripts/README.md b/actionview/app/assets/javascripts/README.md index 0819d5da5f..f321b9f720 100644 --- a/actionview/app/assets/javascripts/README.md +++ b/actionview/app/assets/javascripts/README.md @@ -30,7 +30,7 @@ Run `yarn add rails-ujs` to install the rails-ujs package. Usage ------------ -Require `rails-ujs` into your application.js manifest. +Require `rails-ujs` in your application.js manifest. ```javascript //= require rails-ujs @@ -39,7 +39,8 @@ Require `rails-ujs` into your application.js manifest. Usage with yarn ------------ -When using with Webpacker gem or your preferred JavaScript bundler. Just add the following to your main JS file and compile. +When using with the Webpacker gem or your preferred JavaScript bundler, just +add the following to your main JS file and compile. ```javascript import Rails from 'rails-ujs'; diff --git a/actionview/app/assets/javascripts/rails-ujs/start.coffee b/actionview/app/assets/javascripts/rails-ujs/start.coffee index 5746a22287..55595ac96f 100644 --- a/actionview/app/assets/javascripts/rails-ujs/start.coffee +++ b/actionview/app/assets/javascripts/rails-ujs/start.coffee @@ -9,7 +9,7 @@ } = Rails # For backward compatibility -if jQuery? and not jQuery.rails +if jQuery? and jQuery.ajax? and not jQuery.rails jQuery.rails = Rails jQuery.ajaxPrefilter (options, originalOptions, xhr) -> CSRFProtection(xhr) unless options.crossDomain diff --git a/actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee b/actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee index 26df7b9a3f..a653d3af3d 100644 --- a/actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee +++ b/actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee @@ -14,7 +14,7 @@ AcceptHeaders = Rails.ajax = (options) -> options = prepareOptions(options) xhr = createXHR options, -> - response = processResponse(xhr.response, xhr.getResponseHeader('Content-Type')) + response = processResponse(xhr.response ? xhr.responseText, xhr.getResponseHeader('Content-Type')) if xhr.status // 100 == 2 options.success?(response, xhr.statusText, xhr) else diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb index 3eafe0028e..672269b811 100644 --- a/actionview/lib/action_view/helpers/form_helper.rb +++ b/actionview/lib/action_view/helpers/form_helper.rb @@ -1606,14 +1606,15 @@ module ActionView include ModelNaming # The methods which wrap a form helper call. - class_attribute :field_helpers - self.field_helpers = [:fields_for, :fields, :label, :text_field, :password_field, - :hidden_field, :file_field, :text_area, :check_box, - :radio_button, :color_field, :search_field, - :telephone_field, :phone_field, :date_field, - :time_field, :datetime_field, :datetime_local_field, - :month_field, :week_field, :url_field, :email_field, - :number_field, :range_field] + class_attribute :field_helpers, default: [ + :fields_for, :fields, :label, :text_field, :password_field, + :hidden_field, :file_field, :text_area, :check_box, + :radio_button, :color_field, :search_field, + :telephone_field, :phone_field, :date_field, + :time_field, :datetime_field, :datetime_local_field, + :month_field, :week_field, :url_field, :email_field, + :number_field, :range_field + ] attr_accessor :object_name, :object, :options diff --git a/actionview/lib/action_view/layouts.rb b/actionview/lib/action_view/layouts.rb index 81feb90486..ab8409e8d0 100644 --- a/actionview/lib/action_view/layouts.rb +++ b/actionview/lib/action_view/layouts.rb @@ -204,9 +204,9 @@ module ActionView include ActionView::Rendering included do - class_attribute :_layout, :_layout_conditions, instance_accessor: false - self._layout = nil - self._layout_conditions = {} + class_attribute :_layout, instance_accessor: false + class_attribute :_layout_conditions, instance_accessor: false, default: {} + _write_layout_method end diff --git a/actionview/lib/action_view/template/handlers/builder.rb b/actionview/lib/action_view/template/handlers/builder.rb index e99b921cb7..67ad78133d 100644 --- a/actionview/lib/action_view/template/handlers/builder.rb +++ b/actionview/lib/action_view/template/handlers/builder.rb @@ -1,9 +1,7 @@ module ActionView module Template::Handlers class Builder - # Default format used by Builder. - class_attribute :default_format - self.default_format = :xml + class_attribute :default_format, default: :xml def call(template) require_engine @@ -14,7 +12,6 @@ module ActionView end private - def require_engine # :doc: @required ||= begin require "builder" diff --git a/actionview/lib/action_view/template/handlers/erb.rb b/actionview/lib/action_view/template/handlers/erb.rb index 58c7fd1a88..48c2e22a89 100644 --- a/actionview/lib/action_view/template/handlers/erb.rb +++ b/actionview/lib/action_view/template/handlers/erb.rb @@ -9,16 +9,13 @@ module ActionView # Specify trim mode for the ERB compiler. Defaults to '-'. # See ERB documentation for suitable values. - class_attribute :erb_trim_mode - self.erb_trim_mode = "-" + class_attribute :erb_trim_mode, default: "-" # Default implementation used. - class_attribute :erb_implementation - self.erb_implementation = Erubi + class_attribute :erb_implementation, default: Erubi # Do not escape templates of these mime types. - class_attribute :escape_whitelist - self.escape_whitelist = ["text/plain"] + class_attribute :escape_whitelist, default: ["text/plain"] ENCODING_TAG = Regexp.new("\\A(<%#{ENCODING_FLAG}-?%>)[ \\t]*") diff --git a/actionview/lib/action_view/test_case.rb b/actionview/lib/action_view/test_case.rb index ae4fec4337..80403799ab 100644 --- a/actionview/lib/action_view/test_case.rb +++ b/actionview/lib/action_view/test_case.rb @@ -71,7 +71,7 @@ module ActionView def helper_method(*methods) # Almost a duplicate from ActionController::Helpers methods.flatten.each do |method| - _helpers.module_eval <<-end_eval + _helpers.module_eval <<-end_eval, __FILE__, __LINE__ + 1 def #{method}(*args, &block) # def current_user(*args, &block) _test_case.send(%(#{method}), *args, &block) # _test_case.send(%(current_user), *args, &block) end # end diff --git a/actionview/lib/action_view/view_paths.rb b/actionview/lib/action_view/view_paths.rb index f0fe6831fa..938f0fc17f 100644 --- a/actionview/lib/action_view/view_paths.rb +++ b/actionview/lib/action_view/view_paths.rb @@ -3,9 +3,7 @@ module ActionView extend ActiveSupport::Concern included do - class_attribute :_view_paths - self._view_paths = ActionView::PathSet.new - _view_paths.freeze + class_attribute :_view_paths, default: ActionView::PathSet.new.freeze end delegate :template_exists?, :any_templates?, :view_paths, :formats, :formats=, diff --git a/activejob/Rakefile b/activejob/Rakefile index 43e284d090..dd03ab0b8f 100644 --- a/activejob/Rakefile +++ b/activejob/Rakefile @@ -1,8 +1,7 @@ require "rake/testtask" #TODO: add qu back to the list after it support Rails 5.1 -#TODO: add delayed_job back to the list after it support Rails 5.1 -ACTIVEJOB_ADAPTERS = %w(async inline que queue_classic resque sidekiq sneakers sucker_punch backburner test) +ACTIVEJOB_ADAPTERS = %w(async inline delayed_job que queue_classic resque sidekiq sneakers sucker_punch backburner test) ACTIVEJOB_ADAPTERS.delete("queue_classic") if defined?(JRUBY_VERSION) task default: :test diff --git a/activejob/lib/active_job/queue_name.rb b/activejob/lib/active_job/queue_name.rb index 352cf62424..d83113af60 100644 --- a/activejob/lib/active_job/queue_name.rb +++ b/activejob/lib/active_job/queue_name.rb @@ -32,11 +32,8 @@ module ActiveJob end included do - class_attribute :queue_name, instance_accessor: false - class_attribute :queue_name_delimiter, instance_accessor: false - - self.queue_name = default_queue_name - self.queue_name_delimiter = "_" # set default delimiter to '_' + class_attribute :queue_name, instance_accessor: false, default: default_queue_name + class_attribute :queue_name_delimiter, instance_accessor: false, default: "_" end # Returns the name of the queue the job will be run on. diff --git a/activejob/lib/active_job/queue_priority.rb b/activejob/lib/active_job/queue_priority.rb index b02202fcc8..db8d9178a4 100644 --- a/activejob/lib/active_job/queue_priority.rb +++ b/activejob/lib/active_job/queue_priority.rb @@ -27,9 +27,7 @@ module ActiveJob end included do - class_attribute :priority, instance_accessor: false - - self.priority = default_priority + class_attribute :priority, instance_accessor: false, default: default_priority end # Returns the priority that the job will be created with diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index e707a65147..7483704212 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -1,3 +1,8 @@ +* Fix regression in numericality validator when comparing Decimal and Float input + values with more scale than the schema. + + *Bradley Priest* + * Fix methods `#keys`, `#values` in `ActiveModel::Errors`. Change `#keys` to only return the keys that don't have empty messages. diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb index b5c0b43b61..b3b39bf7ae 100644 --- a/activemodel/lib/active_model/attribute_methods.rb +++ b/activemodel/lib/active_model/attribute_methods.rb @@ -68,9 +68,8 @@ module ActiveModel CALL_COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?]?\z/ included do - class_attribute :attribute_aliases, :attribute_method_matchers, instance_writer: false - self.attribute_aliases = {} - self.attribute_method_matchers = [ClassMethods::AttributeMethodMatcher.new] + class_attribute :attribute_aliases, instance_writer: false, default: {} + class_attribute :attribute_method_matchers, instance_writer: false, default: [ ClassMethods::AttributeMethodMatcher.new ] end module ClassMethods diff --git a/activemodel/lib/active_model/serializers/json.rb b/activemodel/lib/active_model/serializers/json.rb index a9d92eb92a..205b84ddb4 100644 --- a/activemodel/lib/active_model/serializers/json.rb +++ b/activemodel/lib/active_model/serializers/json.rb @@ -10,8 +10,7 @@ module ActiveModel included do extend ActiveModel::Naming - class_attribute :include_root_in_json, instance_writer: false - self.include_root_in_json = false + class_attribute :include_root_in_json, instance_writer: false, default: false end # Returns a hash representing the model. Some configuration can be diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index 1f14a068d1..ae1d69f685 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -49,8 +49,7 @@ module ActiveModel private :validation_context= define_callbacks :validate, scope: :name - class_attribute :_validators, instance_writer: false - self._validators = Hash.new { |h, k| h[k] = [] } + class_attribute :_validators, instance_writer: false, default: Hash.new { |h, k| h[k] = [] } end module ClassMethods diff --git a/activemodel/lib/active_model/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb index b82c85ddf4..fb053a4c4e 100644 --- a/activemodel/lib/active_model/validations/numericality.rb +++ b/activemodel/lib/active_model/validations/numericality.rb @@ -36,7 +36,9 @@ module ActiveModel return end - unless raw_value.is_a?(Numeric) + if raw_value.is_a?(Numeric) + value = raw_value + else value = parse_raw_value_as_a_number(raw_value) end diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index d17bbf80ca..1f8163db12 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,7 @@ +* Deprecate passing arguments and block at the same time to `count` and `sum` in `ActiveRecord::Calculations`. + + *Ryuta Kamizono* + * Loading model schema from database is now thread-safe. Fixes #28589. diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index 0f330af2f6..ee2e79889f 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -162,14 +162,6 @@ module ActiveRecord reset end - def interpolate(sql, record = nil) - if sql.respond_to?(:to_proc) - owner.instance_exec(record, &sql) - else - sql - end - end - # We can't dump @reflection since it contains the scope proc def marshal_dump ivars = (instance_variables - [:@reflection]).map { |name| [name, instance_variable_get(name)] } diff --git a/activerecord/lib/active_record/attribute_decorators.rb b/activerecord/lib/active_record/attribute_decorators.rb index c39e9ce4c5..5bc8527745 100644 --- a/activerecord/lib/active_record/attribute_decorators.rb +++ b/activerecord/lib/active_record/attribute_decorators.rb @@ -3,8 +3,7 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :attribute_type_decorations, instance_accessor: false # :internal: - self.attribute_type_decorations = TypeDecorator.new + class_attribute :attribute_type_decorations, instance_accessor: false, default: TypeDecorator.new # :internal: end module ClassMethods # :nodoc: diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index ebe06566cc..83c61fad19 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -62,7 +62,6 @@ module ActiveRecord super(attribute_names) @attribute_methods_generated = true end - true end def undefine_attribute_methods # :nodoc: diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index bd5003d63a..76987fb8f4 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -14,8 +14,7 @@ module ActiveRecord raise "You cannot include Dirty after Timestamp" end - class_attribute :partial_writes, instance_writer: false - self.partial_writes = true + class_attribute :partial_writes, instance_writer: false, default: true after_create { changes_internally_applied } after_update { changes_internally_applied } diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb index 321d039ed4..4a8d231503 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -57,11 +57,8 @@ module ActiveRecord mattr_accessor :time_zone_aware_attributes, instance_writer: false self.time_zone_aware_attributes = false - class_attribute :skip_time_zone_conversion_for_attributes, instance_writer: false - self.skip_time_zone_conversion_for_attributes = [] - - class_attribute :time_zone_aware_types, instance_writer: false - self.time_zone_aware_types = [:datetime, :time] + class_attribute :skip_time_zone_conversion_for_attributes, instance_writer: false, default: [] + class_attribute :time_zone_aware_types, instance_writer: false, default: [ :datetime, :time ] end module ClassMethods diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb index 75f5ba3a96..475b9beec4 100644 --- a/activerecord/lib/active_record/attributes.rb +++ b/activerecord/lib/active_record/attributes.rb @@ -6,8 +6,7 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :attributes_to_define_after_schema_loads, instance_accessor: false # :internal: - self.attributes_to_define_after_schema_loads = {} + class_attribute :attributes_to_define_after_schema_loads, instance_accessor: false, default: {} # :internal: end module ClassMethods diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index ebe92e7878..829a4f6e86 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -216,13 +216,7 @@ module ActiveRecord method = :validate_single_association end - define_non_cyclic_method(validation_method) do - send(method, reflection) - # TODO: remove the following line as soon as the return value of - # callbacks is ignored, that is, returning `false` does not - # display a deprecation warning or halts the callback chain. - true - end + define_non_cyclic_method(validation_method) { send(method, reflection) } validate validation_method after_validation :_ensure_no_duplicate_errors end @@ -369,7 +363,6 @@ module ActiveRecord # association whether or not the parent was a new record before saving. def before_save_collection_association @new_record_before_save = new_record? - true end def after_save_collection_association diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index abc15f595f..01599985ca 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -29,8 +29,7 @@ module ActiveRecord # to your application.rb file: # # ActiveRecord::ConnectionAdapters::Mysql2Adapter.emulate_booleans = false - class_attribute :emulate_booleans - self.emulate_booleans = true + class_attribute :emulate_booleans, default: true NATIVE_DATABASE_TYPES = { primary_key: "bigint auto_increment PRIMARY KEY", @@ -839,11 +838,6 @@ module ActiveRecord end class MysqlJson < Type::Internal::AbstractJson # :nodoc: - def changed_in_place?(raw_old_value, new_value) - # Normalization is required because MySQL JSON data format includes - # the space between the elements. - super(serialize(deserialize(raw_old_value)), new_value) - end end class MysqlString < Type::String # :nodoc: diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb index 87391b5dc7..705cb7f0b3 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb @@ -6,16 +6,6 @@ module ActiveRecord def type :jsonb end - - def changed_in_place?(raw_old_value, new_value) - # Postgres does not preserve insignificant whitespaces when - # round-tripping jsonb columns. This causes some false positives for - # the comparison here. Therefore, we need to parse and re-dump the - # raw value here to ensure the insignificant whitespaces are - # consistent with our encoder's output. - raw_old_value = serialize(deserialize(raw_old_value)) - super(raw_old_value, new_value) - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index da8d0c6992..44eb666965 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -62,7 +62,7 @@ module ActiveRecord def quote_default_expression(value, column) # :nodoc: if value.is_a?(Proc) value.call - elsif column.type == :uuid && value.include?("()") + elsif column.type == :uuid && /\(\)/.match?(value) value # Does not quote function default values for UUID columns elsif column.respond_to?(:array?) value = type_cast_from_column(column, value) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index 5b483ad4ab..55566f1e34 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -186,17 +186,17 @@ module ActiveRecord # Returns the current database encoding format. def encoding - select_value("SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname LIKE '#{current_database}'", "SCHEMA") + select_value("SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname = current_database()", "SCHEMA") end # Returns the current database collation. def collation - select_value("SELECT datcollate FROM pg_database WHERE datname LIKE '#{current_database}'", "SCHEMA") + select_value("SELECT datcollate FROM pg_database WHERE datname = current_database()", "SCHEMA") end # Returns the current database ctype. def ctype - select_value("SELECT datctype FROM pg_database WHERE datname LIKE '#{current_database}'", "SCHEMA") + select_value("SELECT datctype FROM pg_database WHERE datname = current_database()", "SCHEMA") end # Returns an array of schema names. @@ -377,14 +377,15 @@ module ActiveRecord clear_cache! execute "ALTER TABLE #{quote_table_name(table_name)} RENAME TO #{quote_table_name(new_name)}" pk, seq = pk_and_sequence_for(new_name) - if seq && seq.identifier == "#{table_name}_#{pk}_seq" - new_seq = "#{new_name}_#{pk}_seq" + if pk idx = "#{table_name}_pkey" new_idx = "#{new_name}_pkey" - execute "ALTER TABLE #{seq.quoted} RENAME TO #{quote_table_name(new_seq)}" execute "ALTER INDEX #{quote_table_name(idx)} RENAME TO #{quote_table_name(new_idx)}" + if seq && seq.identifier == "#{table_name}_#{pk}_seq" + new_seq = "#{new_name}_#{pk}_seq" + execute "ALTER TABLE #{seq.quoted} RENAME TO #{quote_table_name(new_seq)}" + end end - rename_table_indexes(table_name, new_name) end diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb index 2ede92feff..b8fbb489b6 100644 --- a/activerecord/lib/active_record/connection_handling.rb +++ b/activerecord/lib/active_record/connection_handling.rb @@ -1,6 +1,6 @@ module ActiveRecord module ConnectionHandling - RAILS_ENV = -> { (Rails.env if defined?(Rails.env)) || ENV["RAILS_ENV"] || ENV["RACK_ENV"] } + RAILS_ENV = -> { (Rails.env if defined?(Rails.env)) || ENV["RAILS_ENV"].presence || ENV["RACK_ENV"].presence } DEFAULT_ENV = -> { RAILS_ENV.call || "default_env" } # Establishes the connection to the database. Accepts a hash as input where diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index 0ab03b2ab3..12ef58a941 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -95,8 +95,7 @@ module ActiveRecord module Enum def self.extended(base) # :nodoc: - base.class_attribute(:defined_enums, instance_writer: false) - base.defined_enums = {} + base.class_attribute(:defined_enums, instance_writer: false, default: {}) end def inherited(base) # :nodoc: @@ -154,11 +153,12 @@ module ActiveRecord definitions.each do |name, values| # statuses = { } enum_values = ActiveSupport::HashWithIndifferentAccess.new - name = name.to_sym + name = name.to_s # def self.statuses() statuses end - detect_enum_conflict!(name, name.to_s.pluralize, true) - klass.singleton_class.send(:define_method, name.to_s.pluralize) { enum_values } + detect_enum_conflict!(name, name.pluralize, true) + singleton_class.send(:define_method, name.pluralize) { enum_values } + defined_enums[name] = enum_values detect_enum_conflict!(name, name) detect_enum_conflict!(name, "#{name}=") @@ -170,7 +170,7 @@ module ActiveRecord _enum_methods_module.module_eval do pairs = values.respond_to?(:each_pair) ? values.each_pair : values.each_with_index - pairs.each do |value, i| + pairs.each do |label, value| if enum_prefix == true prefix = "#{name}_" elsif enum_prefix @@ -182,23 +182,23 @@ module ActiveRecord suffix = "_#{enum_suffix}" end - value_method_name = "#{prefix}#{value}#{suffix}" - enum_values[value] = i + value_method_name = "#{prefix}#{label}#{suffix}" + enum_values[label] = value + label = label.to_s - # def active?() status == 0 end + # def active?() status == "active" end klass.send(:detect_enum_conflict!, name, "#{value_method_name}?") - define_method("#{value_method_name}?") { self[attr] == value.to_s } + define_method("#{value_method_name}?") { self[attr] == label } - # def active!() update! status: :active end + # def active!() update!(status: 0) end klass.send(:detect_enum_conflict!, name, "#{value_method_name}!") define_method("#{value_method_name}!") { update!(attr => value) } - # scope :active, -> { where status: 0 } + # scope :active, -> { where(status: 0) } klass.send(:detect_enum_conflict!, name, value_method_name, true) klass.scope value_method_name, -> { where(attr => value) } end end - defined_enums[name.to_s] = enum_values end end diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index c19216702c..bad6542be2 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -878,20 +878,12 @@ module ActiveRecord included do class_attribute :fixture_path, instance_writer: false - class_attribute :fixture_table_names - class_attribute :fixture_class_names - class_attribute :use_transactional_tests - class_attribute :use_instantiated_fixtures # true, false, or :no_instances - class_attribute :pre_loaded_fixtures - class_attribute :config - - self.fixture_table_names = [] - self.use_instantiated_fixtures = false - self.pre_loaded_fixtures = false - self.config = ActiveRecord::Base - - self.fixture_class_names = {} - self.use_transactional_tests = true + class_attribute :fixture_table_names, default: [] + class_attribute :fixture_class_names, default: {} + class_attribute :use_transactional_tests, default: true + class_attribute :use_instantiated_fixtures, default: false # true, false, or :no_instances + class_attribute :pre_loaded_fixtures, default: false + class_attribute :config, default: ActiveRecord::Base end module ClassMethods diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index 236a65eba7..5776807507 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -38,8 +38,7 @@ module ActiveRecord included do # Determines whether to store the full constant name including namespace when using STI. # This is true, by default. - class_attribute :store_full_sti_class, instance_writer: false - self.store_full_sti_class = true + class_attribute :store_full_sti_class, instance_writer: false, default: true end module ClassMethods diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb index 32362fa86f..cf954852bc 100644 --- a/activerecord/lib/active_record/integration.rb +++ b/activerecord/lib/active_record/integration.rb @@ -11,8 +11,7 @@ module ActiveRecord # versioning is off. Accepts any of the symbols in <tt>Time::DATE_FORMATS</tt>. # # This is +:usec+, by default. - class_attribute :cache_timestamp_format, instance_writer: false - self.cache_timestamp_format = :usec + class_attribute :cache_timestamp_format, instance_writer: false, default: :usec ## # :singleton-method: @@ -20,8 +19,7 @@ module ActiveRecord # by a changing version in the #cache_version method. # # This is +false+, by default until Rails 6.0. - class_attribute :cache_versioning, instance_writer: false - self.cache_versioning = false + class_attribute :cache_versioning, instance_writer: false, default: false end # Returns a +String+, which Action Pack uses for constructing a URL to this diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index 78ce9f8291..3c7110369b 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -51,8 +51,7 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :lock_optimistically, instance_writer: false - self.lock_optimistically = true + class_attribute :lock_optimistically, instance_writer: false, default: true end def locking_enabled? #:nodoc: diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 013562708c..1179a60e9b 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -130,26 +130,13 @@ module ActiveRecord included do mattr_accessor :primary_key_prefix_type, instance_writer: false - class_attribute :table_name_prefix, instance_writer: false - self.table_name_prefix = "" - - class_attribute :table_name_suffix, instance_writer: false - self.table_name_suffix = "" - - class_attribute :schema_migrations_table_name, instance_accessor: false - self.schema_migrations_table_name = "schema_migrations" - - class_attribute :internal_metadata_table_name, instance_accessor: false - self.internal_metadata_table_name = "ar_internal_metadata" - - class_attribute :protected_environments, instance_accessor: false - self.protected_environments = ["production"] - - class_attribute :pluralize_table_names, instance_writer: false - self.pluralize_table_names = true - - class_attribute :ignored_columns, instance_accessor: false - self.ignored_columns = [].freeze + class_attribute :table_name_prefix, instance_writer: false, default: "" + class_attribute :table_name_suffix, instance_writer: false, default: "" + class_attribute :schema_migrations_table_name, instance_accessor: false, default: "schema_migrations" + class_attribute :internal_metadata_table_name, instance_accessor: false, default: "ar_internal_metadata" + class_attribute :protected_environments, instance_accessor: false, default: [ "production" ] + class_attribute :pluralize_table_names, instance_writer: false, default: true + class_attribute :ignored_columns, instance_accessor: false, default: [].freeze self.inheritance_column = "type" diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 3f39fb84e8..917bc76993 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -10,8 +10,7 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :nested_attributes_options, instance_writer: false - self.nested_attributes_options = {} + class_attribute :nested_attributes_options, instance_writer: false, default: {} end # = Active Record Nested Attributes diff --git a/activerecord/lib/active_record/readonly_attributes.rb b/activerecord/lib/active_record/readonly_attributes.rb index 6274996ab8..af6473d250 100644 --- a/activerecord/lib/active_record/readonly_attributes.rb +++ b/activerecord/lib/active_record/readonly_attributes.rb @@ -3,8 +3,7 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :_attr_readonly, instance_accessor: false - self._attr_readonly = [] + class_attribute :_attr_readonly, instance_accessor: false, default: [] end module ClassMethods diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 0bfd59d7bd..429f9a257a 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -8,10 +8,8 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :_reflections, instance_writer: false - class_attribute :aggregate_reflections, instance_writer: false - self._reflections = {} - self.aggregate_reflections = {} + class_attribute :_reflections, instance_writer: false, default: {} + class_attribute :aggregate_reflections, instance_writer: false, default: {} end def self.create(macro, name, scope, options, ar) diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 86b77a7cec..7a8f9abb36 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -18,6 +18,7 @@ module ActiveRecord attr_reader :table, :klass, :loaded, :predicate_builder alias :model :klass alias :loaded? :loaded + alias :locked? :locked def initialize(klass, table, predicate_builder, values = {}) @klass = klass diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 861b2125d4..c562f214c9 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -37,7 +37,16 @@ module ActiveRecord # Note: not all valid {Relation#select}[rdoc-ref:QueryMethods#select] expressions are valid #count expressions. The specifics differ # between databases. In invalid cases, an error from the database is thrown. def count(column_name = nil) - return super() if block_given? + if block_given? + unless column_name.nil? + ActiveSupport::Deprecation.warn \ + "When `count' is called with a block, it ignores other arguments. " \ + "This behavior is now deprecated and will result in an ArgumentError in Rails 5.3." + end + + return super() + end + calculate(:count, column_name) end @@ -73,7 +82,16 @@ module ActiveRecord # # Person.sum(:age) # => 4562 def sum(column_name = nil) - return super() if block_given? + if block_given? + unless column_name.nil? + ActiveSupport::Deprecation.warn \ + "When `sum' is called with a block, it ignores other arguments. " \ + "This behavior is now deprecated and will result in an ArgumentError in Rails 5.3." + end + + return super() + end + calculate(:sum, column_name) end diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index 8b4dd25689..dada0fddf8 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -44,6 +44,8 @@ module ActiveRecord delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key, :connection, :columns_hash, to: :klass + delegate :ast, :locked, to: :arel + module ClassSpecificRelation # :nodoc: extend ActiveSupport::Concern diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb index 2daa48859a..ba5cc29ac1 100644 --- a/activerecord/lib/active_record/scoping/default.rb +++ b/activerecord/lib/active_record/scoping/default.rb @@ -5,11 +5,8 @@ module ActiveRecord included do # Stores the default scope for the class. - class_attribute :default_scopes, instance_writer: false, instance_predicate: false - class_attribute :default_scope_override, instance_writer: false, instance_predicate: false - - self.default_scopes = [] - self.default_scope_override = nil + class_attribute :default_scopes, instance_writer: false, instance_predicate: false, default: [] + class_attribute :default_scope_override, instance_writer: false, instance_predicate: false, default: nil end module ClassMethods diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb index 93c6f1d87e..a61fdd6454 100644 --- a/activerecord/lib/active_record/scoping/named.rb +++ b/activerecord/lib/active_record/scoping/named.rb @@ -159,17 +159,17 @@ module ActiveRecord if body.respond_to?(:to_proc) singleton_class.send(:define_method, name) do |*args| - scope = all.scoping { instance_exec(*args, &body) } + scope = all + scope = scope.scoping { instance_exec(*args, &body) || scope } scope = scope.extending(extension) if extension - - scope || all + scope end else singleton_class.send(:define_method, name) do |*args| - scope = all.scoping { body.call(*args) } + scope = all + scope = scope.scoping { body.call(*args) || scope } scope = scope.extending(extension) if extension - - scope || all + scope end end end diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index 09d8d1cdd4..55f3a194a9 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -43,8 +43,7 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :record_timestamps - self.record_timestamps = true + class_attribute :record_timestamps, default: true end def initialize_dup(other) # :nodoc: diff --git a/activerecord/lib/active_record/type/internal/abstract_json.rb b/activerecord/lib/active_record/type/internal/abstract_json.rb index e19c5a14da..a8d6a63465 100644 --- a/activerecord/lib/active_record/type/internal/abstract_json.rb +++ b/activerecord/lib/active_record/type/internal/abstract_json.rb @@ -24,6 +24,10 @@ module ActiveRecord end end + def changed_in_place?(raw_old_value, new_value) + deserialize(raw_old_value) != new_value + end + def accessor ActiveRecord::Store::StringKeyedHashAccessor end diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb index 3deb007513..32afe331fa 100644 --- a/activerecord/test/cases/adapters/postgresql/connection_test.rb +++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb @@ -31,15 +31,21 @@ module ActiveRecord end def test_encoding - assert_not_nil @connection.encoding + assert_queries(1) do + assert_not_nil @connection.encoding + end end def test_collation - assert_not_nil @connection.collation + assert_queries(1) do + assert_not_nil @connection.collation + end end def test_ctype - assert_not_nil @connection.ctype + assert_queries(1) do + assert_not_nil @connection.ctype + end end def test_default_client_min_messages diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb index 52e4a38cae..6ebe9d82a7 100644 --- a/activerecord/test/cases/adapters/postgresql/uuid_test.rb +++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb @@ -63,6 +63,16 @@ class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase UUIDType.reset_column_information end + def test_add_column_with_null_true_and_default_nil + assert_nothing_raised do + connection.add_column :uuid_data_type, :thingy, :uuid, null: true, default: nil + end + UUIDType.reset_column_information + column = UUIDType.columns_hash["thingy"] + assert column.null + assert_nil column.default + end + def test_data_type_of_uuid_types column = UUIDType.columns_hash["guid"] assert_equal :uuid, column.type diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 36238dd088..4271a09c9b 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -1094,12 +1094,6 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_equal authors(:david), assert_no_queries { posts[0].author } posts = assert_queries(2) do - Post.all.merge!(select: "distinct posts.*", includes: :author, joins: [:comments], where: "comments.body like 'Thank you%'", order: "posts.id").to_a - end - assert_equal [posts(:welcome)], posts - assert_equal authors(:david), assert_no_queries { posts[0].author } - - posts = assert_queries(2) do Post.all.merge!(includes: :author, joins: { taggings: :tag }, where: "tags.name = 'General'", order: "posts.id").to_a end assert_equal posts(:welcome, :thinking), posts diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index 3214d778d4..93f8ab18c2 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -809,4 +809,16 @@ class CalculationsTest < ActiveRecord::TestCase def test_group_by_attribute_with_custom_type assert_equal({ "proposed" => 2, "published" => 2 }, Book.group(:status).count) end + + def test_deprecate_count_with_block_and_column_name + assert_deprecated do + assert_equal 6, Account.count(:firm_id) { true } + end + end + + def test_deprecate_sum_with_block_and_column_name + assert_deprecated do + assert_equal 6, Account.sum(:firm_id) { 1 } + end + end end diff --git a/activerecord/test/cases/connection_adapters/connection_handler_test.rb b/activerecord/test/cases/connection_adapters/connection_handler_test.rb index 681399c8bb..2a71f08d90 100644 --- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb @@ -9,6 +9,17 @@ module ActiveRecord @pool = @handler.establish_connection(ActiveRecord::Base.configurations["arunit"]) end + def test_default_env_fall_back_to_default_env_when_rails_env_or_rack_env_is_empty_string + original_rails_env = ENV["RAILS_ENV"] + original_rack_env = ENV["RACK_ENV"] + ENV["RAILS_ENV"] = ENV["RACK_ENV"] = "" + + assert_equal "default_env", ActiveRecord::ConnectionHandling::DEFAULT_ENV.call + ensure + ENV["RAILS_ENV"] = original_rails_env + ENV["RACK_ENV"] = original_rack_env + end + def test_establish_connection_uses_spec_name config = { "readonly" => { "adapter" => "sqlite3" } } resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(config) diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb index 7f63bb2473..db3da53487 100644 --- a/activerecord/test/cases/enum_test.rb +++ b/activerecord/test/cases/enum_test.rb @@ -6,7 +6,6 @@ class EnumTest < ActiveRecord::TestCase fixtures :books, :authors setup do - @author = authors(:david) @book = books(:awdr) end @@ -39,6 +38,8 @@ class EnumTest < ActiveRecord::TestCase assert_equal @book, Book.author_visibility_visible.first assert_equal @book, Book.illustrator_visibility_visible.first assert_equal @book, Book.medium_to_read.first + assert_equal books(:ddd), Book.forgotten.first + assert_equal books(:rfr), authors(:david).unpublished_books.first end test "find via where with values" do @@ -57,7 +58,6 @@ class EnumTest < ActiveRecord::TestCase assert_not_equal @book, Book.where(status: :written).first assert_equal @book, Book.where(status: [:published]).first assert_not_equal @book, Book.where(status: [:written]).first - assert_not @author.unpublished_books.include?(@book) assert_not_equal @book, Book.where.not(status: :published).first assert_equal @book, Book.where.not(status: :written).first end diff --git a/activerecord/test/cases/json_shared_test_cases.rb b/activerecord/test/cases/json_shared_test_cases.rb index d190b027bf..ef5ca86874 100644 --- a/activerecord/test/cases/json_shared_test_cases.rb +++ b/activerecord/test/cases/json_shared_test_cases.rb @@ -160,6 +160,17 @@ module JSONSharedTestCases assert_not json.changed? end + def test_changes_in_place_with_ruby_object + time = Time.now.utc + json = JsonDataType.create!(payload: time) + + json.reload + assert_not json.changed? + + json.payload = time + assert_not json.changed? + end + def test_assigning_string_literal json = JsonDataType.create!(payload: "foo") assert_equal "foo", json.payload diff --git a/activerecord/test/cases/migration/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb index 802a969cb7..007926f1b9 100644 --- a/activerecord/test/cases/migration/command_recorder_test.rb +++ b/activerecord/test/cases/migration/command_recorder_test.rb @@ -211,11 +211,6 @@ module ActiveRecord assert_equal [:remove_index, [:table, { name: "new_index" }]], remove end - def test_invert_add_index_with_no_options - remove = @recorder.inverse_of :add_index, [:table, [:one, :two]] - assert_equal [:remove_index, [:table, { column: [:one, :two] }]], remove - end - def test_invert_remove_index add = @recorder.inverse_of :remove_index, [:table, :one] assert_equal [:add_index, [:table, :one]], add diff --git a/activerecord/test/cases/migration/rename_table_test.rb b/activerecord/test/cases/migration/rename_table_test.rb index 7bcabd0cc6..5da3ad33a3 100644 --- a/activerecord/test/cases/migration/rename_table_test.rb +++ b/activerecord/test/cases/migration/rename_table_test.rb @@ -79,10 +79,33 @@ module ActiveRecord assert_equal ConnectionAdapters::PostgreSQL::Name.new("public", "octopi_#{pk}_seq"), seq end + def test_renaming_table_renames_primary_key + connection.create_table :cats, id: :uuid, default: "uuid_generate_v4()" + rename_table :cats, :felines + + assert connection.table_exists? :felines + refute connection.table_exists? :cats + + primary_key_name = connection.select_values(<<-SQL.strip_heredoc, "SCHEMA")[0] + SELECT c.relname + FROM pg_class c + JOIN pg_index i + ON c.oid = i.indexrelid + WHERE i.indisprimary + AND i.indrelid = 'felines'::regclass + SQL + + assert_equal "felines_pkey", primary_key_name + ensure + connection.drop_table :cats, if_exists: true + connection.drop_table :felines, if_exists: true + end + def test_renaming_table_doesnt_attempt_to_rename_non_existent_sequences connection.create_table :cats, id: :uuid, default: "uuid_generate_v4()" assert_nothing_raised { rename_table :cats, :felines } assert connection.table_exists? :felines + refute connection.table_exists? :cats ensure connection.drop_table :cats, if_exists: true connection.drop_table :felines, if_exists: true diff --git a/activerecord/test/cases/relation/merging_test.rb b/activerecord/test/cases/relation/merging_test.rb index 64866eaf2d..c3b39a9295 100644 --- a/activerecord/test/cases/relation/merging_test.rb +++ b/activerecord/test/cases/relation/merging_test.rb @@ -56,7 +56,7 @@ class RelationMergingTest < ActiveRecord::TestCase def test_relation_merging_with_locks devs = Developer.lock.where("salary >= 80000").order("id DESC").merge(Developer.limit(2)) - assert devs.locked.present? + assert devs.locked? end def test_relation_merging_with_preload diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb index 5d9aa99497..a305aa295a 100644 --- a/activerecord/test/cases/validations_test.rb +++ b/activerecord/test/cases/validations_test.rb @@ -167,6 +167,20 @@ class ValidationsTest < ActiveRecord::TestCase assert topic.valid? end + def test_numericality_validation_checks_against_raw_value + klass = Class.new(Topic) do + def self.model_name + ActiveModel::Name.new(self, nil, "Topic") + end + attribute :wibble, :decimal, scale: 2, precision: 9 + validates_numericality_of :wibble, greater_than_or_equal_to: BigDecimal.new("97.18") + end + + assert_not klass.new(wibble: "97.179").valid? + assert_not klass.new(wibble: 97.179).valid? + assert_not klass.new(wibble: BigDecimal.new("97.179")).valid? + end + def test_acceptance_validator_doesnt_require_db_connection klass = Class.new(ActiveRecord::Base) do self.table_name = "posts" diff --git a/activerecord/test/fixtures/books.yml b/activerecord/test/fixtures/books.yml index b3625ee72e..699623a6f9 100644 --- a/activerecord/test/fixtures/books.yml +++ b/activerecord/test/fixtures/books.yml @@ -25,6 +25,7 @@ ddd: name: "Domain-Driven Design" format: "hardcover" status: 2 + read_status: "forgotten" tlg: author_id: 1 diff --git a/activerecord/test/models/book.rb b/activerecord/test/models/book.rb index 5f8a8a96dd..6466e1b341 100644 --- a/activerecord/test/models/book.rb +++ b/activerecord/test/models/book.rb @@ -8,7 +8,7 @@ class Book < ActiveRecord::Base has_many :subscribers, through: :subscriptions enum status: [:proposed, :written, :published] - enum read_status: { unread: 0, reading: 2, read: 3 } + enum read_status: { unread: 0, reading: 2, read: 3, forgotten: nil } enum nullable_status: [:single, :married] enum language: [:english, :spanish, :french], _prefix: :in enum author_visibility: [:visible, :invisible], _prefix: true diff --git a/activerecord/test/models/category.rb b/activerecord/test/models/category.rb index e8654dca01..4b2840c653 100644 --- a/activerecord/test/models/category.rb +++ b/activerecord/test/models/category.rb @@ -29,6 +29,15 @@ class Category < ActiveRecord::Base has_many :authors_with_select, -> { select "authors.*, categorizations.post_id" }, through: :categorizations, source: :author scope :general, -> { where(name: "General") } + + # Should be delegated `ast` and `locked` to `arel`. + def self.ast + raise + end + + def self.locked + raise + end end class SpecialCategory < Category diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb index 0420e2d15c..d9381ac9cf 100644 --- a/activerecord/test/models/topic.rb +++ b/activerecord/test/models/topic.rb @@ -14,7 +14,7 @@ class Topic < ActiveRecord::Base scope :replied, -> { where "replies_count > 0" } scope "approved_as_string", -> { where(approved: true) } - scope :anonymous_extension, -> { all } do + scope :anonymous_extension, -> {} do def one 1 end diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index b5db0693fe..fd07187e15 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,6 +1,21 @@ -* Add ActiveSupport::CurrentAttributes to provide a thread-isolated attributes singleton. +* Add default option to class_attribute. Before: + + class_attribute :settings + self.settings = {} + + Now: + + class_attribute :settings, default: {} + + *DHH* + +* `#singularize` and `#pluralize` now respect uncountables for the specified locale. + + *Eilis Hamilton* + +* Add `ActiveSupport::CurrentAttributes` to provide a thread-isolated attributes singleton. Primary use case is keeping all the per-request attributes easily available to the whole system. - + *DHH* * Fix implicit coercion calculations with scalars and durations diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb index d771cab68b..ddfa91a342 100644 --- a/activesupport/lib/active_support/callbacks.rb +++ b/activesupport/lib/active_support/callbacks.rb @@ -62,8 +62,7 @@ module ActiveSupport included do extend ActiveSupport::DescendantsTracker - class_attribute :__callbacks, instance_writer: false - self.__callbacks ||= {} + class_attribute :__callbacks, instance_writer: false, default: {} end CALLBACK_FILTER_TYPES = [:before, :after, :around] diff --git a/activesupport/lib/active_support/core_ext/class/attribute.rb b/activesupport/lib/active_support/core_ext/class/attribute.rb index ba422f9071..8caddcd5c3 100644 --- a/activesupport/lib/active_support/core_ext/class/attribute.rb +++ b/activesupport/lib/active_support/core_ext/class/attribute.rb @@ -68,11 +68,16 @@ class Class # object.setting = false # => NoMethodError # # To opt out of both instance methods, pass <tt>instance_accessor: false</tt>. + # + # To set a default value for the attribute, pass <tt>default:</tt>, like so: + # + # class_attribute :settings, default: {} def class_attribute(*attrs) options = attrs.extract_options! - instance_reader = options.fetch(:instance_accessor, true) && options.fetch(:instance_reader, true) - instance_writer = options.fetch(:instance_accessor, true) && options.fetch(:instance_writer, true) + instance_reader = options.fetch(:instance_accessor, true) && options.fetch(:instance_reader, true) + instance_writer = options.fetch(:instance_accessor, true) && options.fetch(:instance_writer, true) instance_predicate = options.fetch(:instance_predicate, true) + default_value = options.fetch(:default, nil) attrs.each do |name| remove_possible_singleton_method(name) @@ -123,6 +128,10 @@ class Class remove_possible_method "#{name}=" attr_writer name end + + unless default_value.nil? + self.send("#{name}=", default_value) + end end end end diff --git a/activesupport/lib/active_support/current_attributes.rb b/activesupport/lib/active_support/current_attributes.rb index 9921241c23..872b0663c7 100644 --- a/activesupport/lib/active_support/current_attributes.rb +++ b/activesupport/lib/active_support/current_attributes.rb @@ -87,9 +87,7 @@ module ActiveSupport class << self # Returns singleton instance for this class in this thread. If none exists, one is created. def instance - Thread.current[:"current_attributes_for_#{name}"] ||= new.tap do |instance| - current_instances << instance - end + current_instances[name] ||= new end # Declares one or more attributes that will be given both class and instance accessor methods. @@ -125,7 +123,12 @@ module ActiveSupport delegate :set, :reset, to: :instance def reset_all # :nodoc: - current_instances.each(&:reset) + current_instances.each_value(&:reset) + end + + def clear_all # :nodoc: + reset_all + current_instances.clear end private @@ -134,7 +137,7 @@ module ActiveSupport end def current_instances - Thread.current[:current_attributes_instances] ||= [] + Thread.current[:current_attributes_instances] ||= {} end def method_missing(name, *args, &block) diff --git a/activesupport/lib/active_support/i18n_railtie.rb b/activesupport/lib/active_support/i18n_railtie.rb index f05c707ccd..51fe6f3418 100644 --- a/activesupport/lib/active_support/i18n_railtie.rb +++ b/activesupport/lib/active_support/i18n_railtie.rb @@ -66,10 +66,6 @@ module I18n app.reloaders << reloader app.reloader.to_run do reloader.execute_if_updated { require_unload_lock! } - # TODO: remove the following line as soon as the return value of - # callbacks is ignored, that is, returning `false` does not - # display a deprecation warning or halts the callback chain. - true end reloader.execute diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb index 1b089a7538..ff1a0cb8c7 100644 --- a/activesupport/lib/active_support/inflector/methods.rb +++ b/activesupport/lib/active_support/inflector/methods.rb @@ -28,7 +28,7 @@ module ActiveSupport # pluralize('CamelOctopus') # => "CamelOctopi" # pluralize('ley', :es) # => "leyes" def pluralize(word, locale = :en) - apply_inflections(word, inflections(locale).plurals) + apply_inflections(word, inflections(locale).plurals, locale) end # The reverse of #pluralize, returns the singular form of a word in a @@ -45,7 +45,7 @@ module ActiveSupport # singularize('CamelOctopi') # => "CamelOctopus" # singularize('leyes', :es) # => "ley" def singularize(word, locale = :en) - apply_inflections(word, inflections(locale).singulars) + apply_inflections(word, inflections(locale).singulars, locale) end # Converts strings to UpperCamelCase. @@ -387,12 +387,15 @@ module ActiveSupport # Applies inflection rules for +singularize+ and +pluralize+. # - # apply_inflections('post', inflections.plurals) # => "posts" - # apply_inflections('posts', inflections.singulars) # => "post" - def apply_inflections(word, rules) + # If passed an optional +locale+ parameter, the uncountables will be + # found for that locale. + # + # apply_inflections('post', inflections.plurals, :en) # => "posts" + # apply_inflections('posts', inflections.singulars, :en) # => "post" + def apply_inflections(word, rules, locale = :en) result = word.to_s.dup - if word.empty? || inflections.uncountables.uncountable?(result) + if word.empty? || inflections(locale).uncountables.uncountable?(result) result else rules.each { |(rule, replacement)| break if result.sub!(rule, replacement) } diff --git a/activesupport/lib/active_support/number_helper.rb b/activesupport/lib/active_support/number_helper.rb index 880340ca86..9cb2821cb6 100644 --- a/activesupport/lib/active_support/number_helper.rb +++ b/activesupport/lib/active_support/number_helper.rb @@ -4,6 +4,7 @@ module ActiveSupport eager_autoload do autoload :NumberConverter + autoload :RoundingHelper autoload :NumberToRoundedConverter autoload :NumberToDelimitedConverter autoload :NumberToHumanConverter diff --git a/activesupport/lib/active_support/number_helper/number_to_human_converter.rb b/activesupport/lib/active_support/number_helper/number_to_human_converter.rb index 56185ddf4b..040343b5dd 100644 --- a/activesupport/lib/active_support/number_helper/number_to_human_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_human_converter.rb @@ -9,6 +9,7 @@ module ActiveSupport self.validate_float = true def convert # :nodoc: + @number = RoundingHelper.new(options).round(number) @number = Float(number) # for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files @@ -20,10 +21,7 @@ module ActiveSupport exponent = calculate_exponent(units) @number = number / (10**exponent) - until (rounded_number = NumberToRoundedConverter.convert(number, options)) != NumberToRoundedConverter.convert(1000, options) - @number = number / 1000.0 - exponent += 3 - end + rounded_number = NumberToRoundedConverter.convert(number, options) unit = determine_unit(units, exponent) format.gsub("%n".freeze, rounded_number).gsub("%u".freeze, unit).strip end diff --git a/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb b/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb index 1f013990ea..c32d85a45f 100644 --- a/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb @@ -5,26 +5,14 @@ module ActiveSupport self.validate_float = true def convert - precision = options.delete :precision + helper = RoundingHelper.new(options) + rounded_number = helper.round(number) - if precision - case number - when Float, String - @number = BigDecimal(number.to_s) - when Rational - @number = BigDecimal(number, digit_count(number.to_i) + precision) - else - @number = number.to_d - end - - if options.delete(:significant) && precision > 0 - digits, rounded_number = digits_and_rounded_number(precision) + if precision = options[:precision] + if options[:significant] && precision > 0 + digits = helper.digit_count(rounded_number) precision -= digits precision = 0 if precision < 0 # don't let it be negative - else - rounded_number = number.round(precision) - rounded_number = rounded_number.to_i if precision == 0 && rounded_number.finite? - rounded_number = rounded_number.abs if rounded_number.zero? # prevent showing negative zeros end formatted_string = @@ -38,7 +26,7 @@ module ActiveSupport "%00.#{precision}f" % rounded_number end else - formatted_string = number + formatted_string = rounded_number end delimited_number = NumberToDelimitedConverter.convert(formatted_string, options) @@ -79,14 +67,6 @@ module ActiveSupport number end end - - def absolute_number(number) - number.respond_to?(:abs) ? number.abs : number.to_d.abs - end - - def zero? - number.respond_to?(:zero?) ? number.zero? : number.to_d.zero? - end end end end diff --git a/activesupport/lib/active_support/number_helper/rounding_helper.rb b/activesupport/lib/active_support/number_helper/rounding_helper.rb new file mode 100644 index 0000000000..d9644df17d --- /dev/null +++ b/activesupport/lib/active_support/number_helper/rounding_helper.rb @@ -0,0 +1,64 @@ +module ActiveSupport + module NumberHelper + class RoundingHelper # :nodoc: + attr_reader :options + + def initialize(options) + @options = options + end + + def round(number) + return number unless precision + number = convert_to_decimal(number) + if significant && precision > 0 + round_significant(number) + else + round_without_significant(number) + end + end + + def digit_count(number) + return 1 if number.zero? + (Math.log10(absolute_number(number)) + 1).floor + end + + private + def round_without_significant(number) + number = number.round(precision) + number = number.to_i if precision == 0 && number.finite? + number = number.abs if number.zero? # prevent showing negative zeros + number + end + + def round_significant(number) + return 0 if number.zero? + digits = digit_count(number) + multiplier = 10**(digits - precision) + (number / BigDecimal.new(multiplier.to_f.to_s)).round * multiplier + end + + def convert_to_decimal(number) + case number + when Float, String + number = BigDecimal(number.to_s) + when Rational + number = BigDecimal(number, digit_count(number.to_i) + precision) + else + number = number.to_d + end + end + + def precision + options[:precision] + end + + def significant + options[:significant] + end + + def absolute_number(number) + number.respond_to?(:abs) ? number.abs : number.to_d.abs + end + end + end +end diff --git a/activesupport/lib/active_support/railtie.rb b/activesupport/lib/active_support/railtie.rb index 39c83f65a3..1b4ecf4d72 100644 --- a/activesupport/lib/active_support/railtie.rb +++ b/activesupport/lib/active_support/railtie.rb @@ -8,8 +8,9 @@ module ActiveSupport config.eager_load_namespaces << ActiveSupport initializer "active_support.reset_all_current_attributes_instances" do |app| - app.executor.to_run { ActiveSupport::CurrentAttributes.reset_all } - app.executor.to_complete { ActiveSupport::CurrentAttributes.reset_all } + app.reloader.before_class_unload { ActiveSupport::CurrentAttributes.clear_all } + app.executor.to_run { ActiveSupport::CurrentAttributes.reset_all } + app.executor.to_complete { ActiveSupport::CurrentAttributes.reset_all } end initializer "active_support.deprecation_behavior" do |app| diff --git a/activesupport/lib/active_support/reloader.rb b/activesupport/lib/active_support/reloader.rb index 121c621751..9558146201 100644 --- a/activesupport/lib/active_support/reloader.rb +++ b/activesupport/lib/active_support/reloader.rb @@ -69,11 +69,8 @@ module ActiveSupport end end - class_attribute :executor - class_attribute :check - - self.executor = Executor - self.check = lambda { false } + class_attribute :executor, default: Executor + class_attribute :check, default: lambda { false } def self.check! # :nodoc: @should_reload ||= check.call diff --git a/activesupport/lib/active_support/rescuable.rb b/activesupport/lib/active_support/rescuable.rb index 12ec8bf1b8..826832ba7d 100644 --- a/activesupport/lib/active_support/rescuable.rb +++ b/activesupport/lib/active_support/rescuable.rb @@ -8,8 +8,7 @@ module ActiveSupport extend Concern included do - class_attribute :rescue_handlers - self.rescue_handlers = [] + class_attribute :rescue_handlers, default: [] end module ClassMethods diff --git a/activesupport/lib/active_support/testing/assertions.rb b/activesupport/lib/active_support/testing/assertions.rb index 28cf2953bf..28e1df8870 100644 --- a/activesupport/lib/active_support/testing/assertions.rb +++ b/activesupport/lib/active_support/testing/assertions.rb @@ -167,7 +167,7 @@ module ActiveSupport retval end - # Assertion that the result of evaluating an expression is changed before + # Assertion that the result of evaluating an expression is not changed before # and after invoking the passed in block. # # assert_no_changes 'Status.all_good?' do diff --git a/activesupport/test/core_ext/class/attribute_test.rb b/activesupport/test/core_ext/class/attribute_test.rb index 5a9ec78cc1..f16043c612 100644 --- a/activesupport/test/core_ext/class/attribute_test.rb +++ b/activesupport/test/core_ext/class/attribute_test.rb @@ -3,7 +3,11 @@ require "active_support/core_ext/class/attribute" class ClassAttributeTest < ActiveSupport::TestCase def setup - @klass = Class.new { class_attribute :setting } + @klass = Class.new do + class_attribute :setting + class_attribute :timeout, default: 5 + end + @sub = Class.new(@klass) end @@ -12,6 +16,10 @@ class ClassAttributeTest < ActiveSupport::TestCase assert_nil @sub.setting end + test "custom default" do + assert_equal 5, @klass.timeout + end + test "inheritable" do @klass.setting = 1 assert_equal 1, @sub.setting diff --git a/activesupport/test/inflector_test.rb b/activesupport/test/inflector_test.rb index 14bc10513b..ef956eda90 100644 --- a/activesupport/test/inflector_test.rb +++ b/activesupport/test/inflector_test.rb @@ -420,6 +420,8 @@ class InflectorTest < ActiveSupport::TestCase inflect.singular(/es$/, "") inflect.irregular("el", "los") + + inflect.uncountable("agua") end assert_equal("hijos", "hijo".pluralize(:es)) @@ -432,12 +434,17 @@ class InflectorTest < ActiveSupport::TestCase assert_equal("los", "el".pluralize(:es)) assert_equal("els", "el".pluralize) + assert_equal("agua", "agua".pluralize(:es)) + assert_equal("aguas", "agua".pluralize) + ActiveSupport::Inflector.inflections(:es) { |inflect| inflect.clear } assert ActiveSupport::Inflector.inflections(:es).plurals.empty? assert ActiveSupport::Inflector.inflections(:es).singulars.empty? + assert ActiveSupport::Inflector.inflections(:es).uncountables.empty? assert !ActiveSupport::Inflector.inflections.plurals.empty? assert !ActiveSupport::Inflector.inflections.singulars.empty? + assert !ActiveSupport::Inflector.inflections.uncountables.empty? end def test_clear_all diff --git a/activesupport/test/number_helper_test.rb b/activesupport/test/number_helper_test.rb index dc0c34d4e2..4caf1428ea 100644 --- a/activesupport/test/number_helper_test.rb +++ b/activesupport/test/number_helper_test.rb @@ -321,12 +321,18 @@ module ActiveSupport gangster = { hundred: "hundred bucks", million: "thousand quids" } assert_equal "1 hundred bucks", number_helper.number_to_human(100, units: gangster) assert_equal "25 hundred bucks", number_helper.number_to_human(2500, units: gangster) + assert_equal "1000 hundred bucks", number_helper.number_to_human(100_000, units: gangster) + assert_equal "1 thousand quids", number_helper.number_to_human(999_999, units: gangster) + assert_equal "1 thousand quids", number_helper.number_to_human(1_000_000, units: gangster) assert_equal "25 thousand quids", number_helper.number_to_human(25000000, units: gangster) assert_equal "12300 thousand quids", number_helper.number_to_human(12345000000, units: gangster) #Spaces are stripped from the resulting string assert_equal "4", number_helper.number_to_human(4, units: { unit: "", ten: "tens " }) assert_equal "4.5 tens", number_helper.number_to_human(45, units: { unit: "", ten: " tens " }) + + #Uses only the provided units and does not try to use larger ones + assert_equal "1000 kilometers", number_helper.number_to_human(1_000_000, units: { unit: "meter", thousand: "kilometers" }) end end diff --git a/guides/source/active_record_querying.md b/guides/source/active_record_querying.md index aea7515974..3676462788 100644 --- a/guides/source/active_record_querying.md +++ b/guides/source/active_record_querying.md @@ -559,8 +559,8 @@ SELECT * FROM clients WHERE (clients.locked != 1) ### OR Conditions -`OR` condition between two relations can be build by calling `or` on the first relation -and passing the second one as an argument. +`OR` conditions between two relations can be built by calling `or` on the first +relation, and passing the second one as an argument. ```ruby Client.where(locked: true).or(Client.where(orders_count: [1,3,5])) diff --git a/guides/source/configuring.md b/guides/source/configuring.md index bf9456a482..1234e1f192 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -456,10 +456,14 @@ to `'http authentication'`. Defaults to `'signed cookie'`. * `config.action_dispatch.encrypted_cookie_salt` sets the encrypted cookies salt -value. Defaults to `'encrypted cookie'`. + value. Defaults to `'encrypted cookie'`. * `config.action_dispatch.encrypted_signed_cookie_salt` sets the signed -encrypted cookies salt value. Defaults to `'signed encrypted cookie'`. + encrypted cookies salt value. Defaults to `'signed encrypted cookie'`. + +* `config.action_dispatch.authenticated_encrypted_cookie_salt` sets the + authenticated encrypted cookie salt. Defaults to `'authenticated encrypted + cookie'`. * `config.action_dispatch.perform_deep_munge` configures whether `deep_munge` method should be performed on the parameters. See [Security Guide](security.html#unsafe-query-generation) @@ -493,8 +497,6 @@ encrypted cookies salt value. Defaults to `'signed encrypted cookie'`. * `ActionDispatch::Callbacks.before` takes a block of code to run before the request. -* `ActionDispatch::Callbacks.to_prepare` takes a block to run after `ActionDispatch::Callbacks.before`, but before the request. Runs for every request in `development` mode, but only once for `production` or environments with `cache_classes` set to `true`. - * `ActionDispatch::Callbacks.after` takes a block of code to run after the request. ### Configuring Action View @@ -1188,7 +1190,7 @@ Below is a comprehensive list of all the initializers found in Rails in the orde * `finisher_hook`: Provides a hook for after the initialization of process of the application is complete, as well as running all the `config.after_initialize` blocks for the application, railties and engines. -* `set_routes_reloader_hook`: Configures Action Dispatch to reload the routes file using `ActionDispatch::Callbacks.to_prepare`. +* `set_routes_reloader_hook`: Configures Action Dispatch to reload the routes file using `ActiveSupport::Callbacks.to_run`. * `disable_dependency_loading`: Disables the automatic dependency loading if the `config.eager_load` is set to `true`. diff --git a/guides/source/security.md b/guides/source/security.md index 75522834df..f69a0c72b0 100644 --- a/guides/source/security.md +++ b/guides/source/security.md @@ -95,16 +95,23 @@ Rails 2 introduced a new default session storage, CookieStore. CookieStore saves * The client can see everything you store in a session, because it is stored in clear-text (actually Base64-encoded, so not encrypted). So, of course, _you don't want to store any secrets here_. To prevent session hash tampering, a digest is calculated from the session with a server-side secret (`secrets.secret_token`) and inserted into the end of the cookie. -However, since Rails 4, the default store is EncryptedCookieStore. With -EncryptedCookieStore the session is encrypted before being stored in a cookie. -This prevents the user from accessing and tampering the content of the cookie. -Thus the session becomes a more secure place to store data. The encryption is -done using a server-side secret key `secrets.secret_key_base` stored in -`config/secrets.yml`. +In Rails 4, encrypted cookies through AES in CBC mode with HMAC using SHA1 for +verification was introduced. This prevents the user from accessing and tampering +the content of the cookie. Thus the session becomes a more secure place to store +data. The encryption is performed using a server-side `secrets.secret_key_base`. +Two salts are used when deriving keys for encryption and verification. These +salts are set via the `config.action_dispatch.encrypted_cookie_salt` and +`config.action_dispatch.encrypted_signed_cookie_salt` configuration values. -That means the security of this storage depends on this secret (and on the digest algorithm, which defaults to SHA1, for compatibility). So _don't use a trivial secret, i.e. a word from a dictionary, or one which is shorter than 30 characters, use `rails secret` instead_. +Rails 5.2 uses AES-GCM for the encryption which couples authentication +and encryption in one faster step and produces shorter ciphertexts. -`secrets.secret_key_base` is used for specifying a key which allows sessions for the application to be verified against a known secure key to prevent tampering. Applications get `secrets.secret_key_base` initialized to a random key present in `config/secrets.yml`, e.g.: +Encrypted cookies are automatically upgraded if the +`config.action_dispatch.use_authenticated_cookie_encryption` is enabled. + +_Do not use a trivial secret, i.e. a word from a dictionary, or one which is shorter than 30 characters! Instead use `rails secret` to generate secret keys!_ + +Applications get `secrets.secret_key_base` initialized to a random key present in `config/secrets.yml`, e.g.: development: secret_key_base: a75d... diff --git a/guides/source/testing.md b/guides/source/testing.md index c0394f927e..e4fc8c7b6e 100644 --- a/guides/source/testing.md +++ b/guides/source/testing.md @@ -350,6 +350,8 @@ Rails adds some custom assertions of its own to the `minitest` framework: | --------------------------------------------------------------------------------- | ------- | | [`assert_difference(expressions, difference = 1, message = nil) {...}`](http://api.rubyonrails.org/classes/ActiveSupport/Testing/Assertions.html#method-i-assert_difference) | Test numeric difference between the return value of an expression as a result of what is evaluated in the yielded block.| | [`assert_no_difference(expressions, message = nil, &block)`](http://api.rubyonrails.org/classes/ActiveSupport/Testing/Assertions.html#method-i-assert_no_difference) | Asserts that the numeric result of evaluating an expression is not changed before and after invoking the passed in block.| +| [`assert_changes(expressions, message = nil, from:, to:, &block)`](http://api.rubyonrails.org/classes/ActiveSupport/Testing/Assertions.html#method-i-assert_changes) | Test that the result of evaluating an expression is changed after invoking the passed in block.| +| [`assert_no_changes(expressions, message = nil, &block)`](http://api.rubyonrails.org/classes/ActiveSupport/Testing/Assertions.html#method-i-assert_no_changes) | Test the result of evaluating an expression is not changed after invoking the passed in block.| | [`assert_nothing_raised { block }`](http://api.rubyonrails.org/classes/ActiveSupport/Testing/Assertions.html#method-i-assert_nothing_raised) | Ensures that the given block doesn't raise any exceptions.| | [`assert_recognizes(expected_options, path, extras={}, message=nil)`](http://api.rubyonrails.org/classes/ActionDispatch/Assertions/RoutingAssertions.html#method-i-assert_recognizes) | Asserts that the routing of the given path was handled correctly and that the parsed options (given in the expected_options hash) match path. Basically, it asserts that Rails recognizes the route given by expected_options.| | [`assert_generates(expected_path, options, defaults={}, extras = {}, message=nil)`](http://api.rubyonrails.org/classes/ActionDispatch/Assertions/RoutingAssertions.html#method-i-assert_generates) | Asserts that the provided options can be used to generate the provided path. This is the inverse of assert_recognizes. The extras parameter is used to tell the request the names and values of additional request parameters that would be in a query string. The message parameter allows you to specify a custom error message for assertion failures.| diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index f8a923141d..39ca2db8e1 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -260,6 +260,7 @@ module Rails "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.authenticated_encrypted_cookie_salt" => config.action_dispatch.authenticated_encrypted_cookie_salt, "action_dispatch.cookies_serializer" => config.action_dispatch.cookies_serializer, "action_dispatch.cookies_digest" => config.action_dispatch.cookies_digest ) diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index 4dc9a431f6..4ffde6198a 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -88,6 +88,10 @@ module Rails active_record.cache_versioning = true end + if respond_to?(:action_dispatch) + action_dispatch.use_authenticated_cookie_encryption = true + end + else raise "Unknown version #{target_version.to_s.inspect}" end diff --git a/railties/lib/rails/command.rb b/railties/lib/rails/command.rb index 0d4e6dc5a1..ee020b58f9 100644 --- a/railties/lib/rails/command.rb +++ b/railties/lib/rails/command.rb @@ -23,7 +23,7 @@ module Rails end def environment # :nodoc: - ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development" + ENV["RAILS_ENV"].presence || ENV["RACK_ENV"].presence || "development" end # Receives a namespace, arguments and the behavior to invoke the command. diff --git a/railties/lib/rails/commands/server/server_command.rb b/railties/lib/rails/commands/server/server_command.rb index cf3903f3ae..ebb4ae795a 100644 --- a/railties/lib/rails/commands/server/server_command.rb +++ b/railties/lib/rails/commands/server/server_command.rb @@ -155,9 +155,16 @@ module Rails def user_supplied_options @user_supplied_options ||= begin # Convert incoming options array to a hash of flags - # ["-p", "3001", "-c", "foo"] # => {"-p" => true, "-c" => true} + # ["-p3001", "-C", "--binding", "127.0.0.1"] # => {"-p"=>true, "-C"=>true, "--binding"=>true} user_flag = {} - @original_options.each_with_index { |command, i| user_flag[command] = true if i.even? } + @original_options.each do |command| + if command.to_s.start_with?("--") + option = command.split("=")[0] + user_flag[option] = true + elsif command =~ /\A(-.)/ + user_flag[Regexp.last_match[0]] = true + end + end # Collect all options that the user has explicitly defined so we can # differentiate them from defaults diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb index 20ee4b108d..45b9e7bdff 100644 --- a/railties/lib/rails/generators/rails/app/app_generator.rb +++ b/railties/lib/rails/generators/rails/app/app_generator.rb @@ -205,7 +205,7 @@ module Rails RESERVED_NAMES = %w[application destroy plugin runner test] class AppGenerator < AppBase # :nodoc: - WEBPACKS = %w( react vue angular ) + WEBPACKS = %w( react vue angular elm ) add_shared_options_for "application" diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_5_2.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_5_2.rb.tt index 52c08500d8..900baa607a 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_5_2.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_5_2.rb.tt @@ -9,3 +9,7 @@ # Make Active Record use stable #cache_key alongside new #cache_version method. # This is needed for recyclable cache keys. # Rails.application.config.active_record.cache_versioning = true + +# Use AES 256 GCM authenticated encryption for encrypted cookies. +# Existing cookies will be converted on read then written with the new scheme. +# Rails.application.config.action_dispatch.use_authenticated_cookie_encryption = true diff --git a/railties/lib/rails/generators/test_unit/system/system_generator.rb b/railties/lib/rails/generators/test_unit/system/system_generator.rb index aec415a4e5..0514957d9c 100644 --- a/railties/lib/rails/generators/test_unit/system/system_generator.rb +++ b/railties/lib/rails/generators/test_unit/system/system_generator.rb @@ -10,7 +10,7 @@ module TestUnit # :nodoc: template "application_system_test_case.rb", File.join("test", "application_system_test_case.rb") end - template "system_test.rb", File.join("test/system", "#{file_name.pluralize}_test.rb") + template "system_test.rb", File.join("test/system", class_path, "#{file_name.pluralize}_test.rb") end end end diff --git a/railties/lib/rails/generators/testing/behaviour.rb b/railties/lib/rails/generators/testing/behaviour.rb index 7a954a791d..ce0e42e60d 100644 --- a/railties/lib/rails/generators/testing/behaviour.rb +++ b/railties/lib/rails/generators/testing/behaviour.rb @@ -14,12 +14,12 @@ module Rails include ActiveSupport::Testing::Stream included do - class_attribute :destination_root, :current_path, :generator_class, :default_arguments - # Generators frequently change the current path using +FileUtils.cd+. # So we need to store the path at file load and revert back to it after each test. - self.current_path = File.expand_path(Dir.pwd) - self.default_arguments = [] + class_attribute :current_path, default: File.expand_path(Dir.pwd) + class_attribute :default_arguments, default: [] + class_attribute :destination_root + class_attribute :generator_class end module ClassMethods diff --git a/railties/lib/rails/secrets.rb b/railties/lib/rails/secrets.rb index 20c20cb9f1..c7a8676d7b 100644 --- a/railties/lib/rails/secrets.rb +++ b/railties/lib/rails/secrets.rb @@ -42,7 +42,7 @@ module Rails <<-end_of_template.strip_heredoc # See `secrets.yml` for tips on generating suitable keys. # production: - # external_api_key: 1466aac22e6a869134be3d09b9e89232fc2c2289… + # external_api_key: 1466aac22e6a869134be3d09b9e89232fc2c2289 end_of_template end diff --git a/railties/lib/rails/test_unit/reporter.rb b/railties/lib/rails/test_unit/reporter.rb index fe11664d5e..1cc27f7b6c 100644 --- a/railties/lib/rails/test_unit/reporter.rb +++ b/railties/lib/rails/test_unit/reporter.rb @@ -3,8 +3,7 @@ require "minitest" module Rails class TestUnitReporter < Minitest::StatisticsReporter - class_attribute :executable - self.executable = "bin/rails test" + class_attribute :executable, default: "bin/rails test" def record(result) super diff --git a/railties/test/application/current_attributes_integration_test.rb b/railties/test/application/current_attributes_integration_test.rb index b6659f296a..5653ec0be1 100644 --- a/railties/test/application/current_attributes_integration_test.rb +++ b/railties/test/application/current_attributes_integration_test.rb @@ -52,19 +52,6 @@ class CurrentAttributesIntegrationTest < ActiveSupport::TestCase <%= Current.customer.try(:name) || 'noone' %>,<%= Time.zone.name %> RUBY - app_file "app/executor_intercept.rb", <<-RUBY - check_state = -> { puts [ Current.customer.try(:name) || "noone", Time.zone.name ].join(",") } - - check_state.call - - Rails.application.executor.wrap do - Current.customer = Customer.new("david") - check_state.call - end - - check_state.call - RUBY - require "#{app_path}/config/environment" end @@ -81,8 +68,17 @@ class CurrentAttributesIntegrationTest < ActiveSupport::TestCase end test "resets after execution" do - Dir.chdir(app_path) do - assert_equal "noone,UTC\ndavid,Copenhagen\nnoone,UTC\n", `bin/rails runner app/executor_intercept.rb` + assert_nil Current.customer + assert_equal "UTC", Time.zone.name + + Rails.application.executor.wrap do + Current.customer = Customer.new("david") + + assert_equal "david", Current.customer.name + assert_equal "Copenhagen", Time.zone.name end + + assert_nil Current.customer + assert_equal "UTC", Time.zone.name end end diff --git a/railties/test/application/middleware/session_test.rb b/railties/test/application/middleware/session_test.rb index 959a629ede..a14ea589ed 100644 --- a/railties/test/application/middleware/session_test.rb +++ b/railties/test/application/middleware/session_test.rb @@ -162,6 +162,11 @@ module ApplicationTests end RUBY + add_to_config <<-RUBY + # Enable AEAD cookies + config.action_dispatch.use_authenticated_cookie_encryption = true + RUBY + require "#{app_path}/config/environment" get "/foo/write_session" @@ -171,9 +176,9 @@ module ApplicationTests get "/foo/read_encrypted_cookie" assert_equal "1", last_response.body - secret = app.key_generator.generate_key("encrypted cookie") - sign_secret = app.key_generator.generate_key("signed encrypted cookie") - encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len], sign_secret) + cipher = "aes-256-gcm" + secret = app.key_generator.generate_key("authenticated encrypted cookie") + encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len(cipher)], cipher: cipher) get "/foo/read_raw_cookie" assert_equal 1, encryptor.decrypt_and_verify(last_response.body)["foo"] @@ -209,6 +214,9 @@ module ApplicationTests add_to_config <<-RUBY secrets.secret_token = "3b7cd727ee24e8444053437c36cc66c4" + + # Enable AEAD cookies + config.action_dispatch.use_authenticated_cookie_encryption = true RUBY require "#{app_path}/config/environment" @@ -220,9 +228,9 @@ module ApplicationTests get "/foo/read_encrypted_cookie" assert_equal "1", last_response.body - secret = app.key_generator.generate_key("encrypted cookie") - sign_secret = app.key_generator.generate_key("signed encrypted cookie") - encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len], sign_secret) + cipher = "aes-256-gcm" + secret = app.key_generator.generate_key("authenticated encrypted cookie") + encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len(cipher)], cipher: cipher) get "/foo/read_raw_cookie" assert_equal 1, encryptor.decrypt_and_verify(last_response.body)["foo"] @@ -264,6 +272,73 @@ module ApplicationTests add_to_config <<-RUBY secrets.secret_token = "3b7cd727ee24e8444053437c36cc66c4" + + # Enable AEAD cookies + config.action_dispatch.use_authenticated_cookie_encryption = true + RUBY + + require "#{app_path}/config/environment" + + get "/foo/write_raw_session" + get "/foo/read_session" + assert_equal "1", last_response.body + + get "/foo/write_session" + get "/foo/read_session" + assert_equal "2", last_response.body + + get "/foo/read_encrypted_cookie" + assert_equal "2", last_response.body + + cipher = "aes-256-gcm" + secret = app.key_generator.generate_key("authenticated encrypted cookie") + encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len(cipher)], cipher: cipher) + + get "/foo/read_raw_cookie" + assert_equal 2, encryptor.decrypt_and_verify(last_response.body)["foo"] + end + + test "session upgrading from AES-CBC-HMAC encryption to AES-GCM encryption" do + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + get ':controller(/:action)' + end + RUBY + + controller :foo, <<-RUBY + class FooController < ActionController::Base + def write_raw_session + # AES-256-CBC with SHA1 HMAC + # {"session_id"=>"1965d95720fffc123941bdfb7d2e6870", "foo"=>1} + cookies[:_myapp_session] = "TlgrdS85aUpDd1R2cDlPWlR6K0FJeGExckwySjZ2Z0pkR3d2QnRObGxZT25aalJWYWVvbFVLcHF4d0VQVDdSaFF2QjFPbG9MVjJzeWp3YjcyRUlKUUU2ZlR4bXlSNG9ZUkJPRUtld0E3dVU9LS0xNDZXbGpRZ3NjdW43N2haUEZJSUNRPT0=--3639b5ce54c09495cfeaae928cd5634e0c4b2e96" + head :ok + end + + def write_session + session[:foo] = session[:foo] + 1 + head :ok + end + + def read_session + render plain: session[:foo] + end + + def read_encrypted_cookie + render plain: cookies.encrypted[:_myapp_session]['foo'] + end + + def read_raw_cookie + render plain: cookies[:_myapp_session] + end + end + RUBY + + add_to_config <<-RUBY + # Use a static key + secrets.secret_key_base = "known key base" + + # Enable AEAD cookies + config.action_dispatch.use_authenticated_cookie_encryption = true RUBY require "#{app_path}/config/environment" @@ -279,9 +354,9 @@ module ApplicationTests get "/foo/read_encrypted_cookie" assert_equal "2", last_response.body - secret = app.key_generator.generate_key("encrypted cookie") - sign_secret = app.key_generator.generate_key("signed encrypted cookie") - encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len], sign_secret) + cipher = "aes-256-gcm" + secret = app.key_generator.generate_key("authenticated encrypted cookie") + encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len(cipher)], cipher: cipher) get "/foo/read_raw_cookie" assert_equal 2, encryptor.decrypt_and_verify(last_response.body)["foo"] diff --git a/railties/test/commands/secrets_test.rb b/railties/test/commands/secrets_test.rb index fb8fd2325e..be610f3b47 100644 --- a/railties/test/commands/secrets_test.rb +++ b/railties/test/commands/secrets_test.rb @@ -23,7 +23,7 @@ class Rails::Command::SecretsCommandTest < ActiveSupport::TestCase # Run twice to ensure encrypted secrets can be reread after first edit pass. 2.times do - assert_match(/external_api_key: 1466aac22e6a869134be3d09b9e89232fc2c2289…/, run_edit_command) + assert_match(/external_api_key: 1466aac22e6a869134be3d09b9e89232fc2c2289/, run_edit_command) end end diff --git a/railties/test/commands/server_test.rb b/railties/test/commands/server_test.rb index 7731d10d9b..722323efdc 100644 --- a/railties/test/commands/server_test.rb +++ b/railties/test/commands/server_test.rb @@ -165,6 +165,12 @@ class Rails::ServerTest < ActiveSupport::TestCase server_options = parse_arguments(["--port", 3001]) assert_equal [:Port], server_options[:user_supplied_options] + + server_options = parse_arguments(["-p3001", "-C", "--binding", "127.0.0.1"]) + assert_equal [:Port, :Host, :caching], server_options[:user_supplied_options] + + server_options = parse_arguments(["--port=3001"]) + assert_equal [:Port], server_options[:user_supplied_options] end def test_default_options diff --git a/railties/test/generators/system_test_generator_test.rb b/railties/test/generators/system_test_generator_test.rb index e8e561ec49..4622360244 100644 --- a/railties/test/generators/system_test_generator_test.rb +++ b/railties/test/generators/system_test_generator_test.rb @@ -9,4 +9,9 @@ class SystemTestGeneratorTest < Rails::Generators::TestCase run_generator assert_file "test/system/users_test.rb", /class UsersTest < ApplicationSystemTestCase/ end + + def test_namespaced_system_test_skeleton_is_created + run_generator %w(admin/user) + assert_file "test/system/admin/users_test.rb", /class Admin::UsersTest < ApplicationSystemTestCase/ + end end |