diff options
Diffstat (limited to 'activesupport')
16 files changed, 159 insertions, 75 deletions
diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index 938d4f9d31..ac1043df78 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,8 +1,30 @@ +* `Module#delegate_missing_to` now raises `DelegationError` if target is nil, + similar to `Module#delegate`. + + *Anton Khamets* + +* Update `String#camelize` to provide feedback when wrong option is passed + + `String#camelize` was returning nil without any feedback when an + invalid option was passed as a parameter. + + Previously: + + 'one_two'.camelize(true) + => nil + + Now: + + 'one_two'.camelize(true) + => ArgumentError: Invalid option, use either :upper or :lower. + + *Ricardo Díaz* + * Fix modulo operations involving durations - Rails 5.1 introduce an `ActiveSupport::Duration::Scalar` class as a wrapper - around a numeric value as a way of ensuring a duration was the outcome of - an expression. However the implementation was missing support for modulo + Rails 5.1 introduced `ActiveSupport::Duration::Scalar` as a wrapper + around numeric values as a way of ensuring a duration was the outcome of + an expression. However, the implementation was missing support for modulo operations. This support has now been added and should result in a duration being returned from expressions involving modulo operations. @@ -118,15 +140,15 @@ * Fix implicit coercion calculations with scalars and durations - Previously calculations where the scalar is first would be converted to a duration - of seconds but this causes issues with dates being converted to times, e.g: + Previously, calculations where the scalar is first would be converted to a duration + of seconds, but this causes issues with dates being converted to times, e.g: Time.zone = "Beijing" # => Asia/Shanghai date = Date.civil(2017, 5, 20) # => Mon, 20 May 2017 2 * 1.day # => 172800 seconds date + 2 * 1.day # => Mon, 22 May 2017 00:00:00 CST +08:00 - Now the `ActiveSupport::Duration::Scalar` calculation methods will try to maintain + Now, the `ActiveSupport::Duration::Scalar` calculation methods will try to maintain the part structure of the duration where possible, e.g: Time.zone = "Beijing" # => Asia/Shanghai diff --git a/activesupport/lib/active_support/core_ext/load_error.rb b/activesupport/lib/active_support/core_ext/load_error.rb index c3939b5d0b..750f858fcc 100644 --- a/activesupport/lib/active_support/core_ext/load_error.rb +++ b/activesupport/lib/active_support/core_ext/load_error.rb @@ -1,13 +1,6 @@ # frozen_string_literal: true class LoadError - REGEXPS = [ - /^no such file to load -- (.+)$/i, - /^Missing \w+ (?:file\s*)?([^\s]+.rb)$/i, - /^Missing API definition file in (.+)$/i, - /^cannot load such file -- (.+)$/i, - ] - # Returns true if the given path name (except perhaps for the ".rb" # extension) is the missing file which caused the exception to be raised. def is_missing?(location) diff --git a/activesupport/lib/active_support/core_ext/module/delegation.rb b/activesupport/lib/active_support/core_ext/module/delegation.rb index 6565d53a06..1840dc942f 100644 --- a/activesupport/lib/active_support/core_ext/module/delegation.rb +++ b/activesupport/lib/active_support/core_ext/module/delegation.rb @@ -274,7 +274,15 @@ class Module if #{target}.respond_to?(method) #{target}.public_send(method, *args, &block) else - super + begin + super + rescue NoMethodError + if #{target}.nil? + raise DelegationError, "\#{method} delegated to #{target}, but #{target} is nil" + else + raise + end + end end end RUBY diff --git a/activesupport/lib/active_support/core_ext/string/inflections.rb b/activesupport/lib/active_support/core_ext/string/inflections.rb index 846600c623..b5bb385033 100644 --- a/activesupport/lib/active_support/core_ext/string/inflections.rb +++ b/activesupport/lib/active_support/core_ext/string/inflections.rb @@ -94,6 +94,8 @@ class String ActiveSupport::Inflector.camelize(self, true) when :lower ActiveSupport::Inflector.camelize(self, false) + else + raise ArgumentError, "Invalid option, use either :upper or :lower." end end alias_method :camelcase, :camelize diff --git a/activesupport/lib/active_support/current_attributes.rb b/activesupport/lib/active_support/current_attributes.rb index 9ab1546064..4e6d8e4585 100644 --- a/activesupport/lib/active_support/current_attributes.rb +++ b/activesupport/lib/active_support/current_attributes.rb @@ -33,7 +33,7 @@ module ActiveSupport # # private # def authenticate - # if authenticated_user = User.find_by(id: cookies.signed[:user_id]) + # if authenticated_user = User.find_by(id: cookies.encrypted[:user_id]) # Current.user = authenticated_user # else # redirect_to new_session_url diff --git a/activesupport/lib/active_support/hash_with_indifferent_access.rb b/activesupport/lib/active_support/hash_with_indifferent_access.rb index 44e95f58a1..12291af443 100644 --- a/activesupport/lib/active_support/hash_with_indifferent_access.rb +++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb @@ -236,7 +236,7 @@ module ActiveSupport # dup = hash.dup # dup[:a][:c] = 'c' # - # hash[:a][:c] # => nil + # hash[:a][:c] # => "c" # dup[:a][:c] # => "c" def dup self.class.new(self).tap do |new_hash| diff --git a/activesupport/lib/active_support/message_encryptor.rb b/activesupport/lib/active_support/message_encryptor.rb index 090d51933a..952306b482 100644 --- a/activesupport/lib/active_support/message_encryptor.rb +++ b/activesupport/lib/active_support/message_encryptor.rb @@ -121,14 +121,13 @@ module ActiveSupport # Encrypt and sign a message. We need to sign the message in order to avoid # padding attacks. Reference: http://www.limited-entropy.com/padding-oracle-attacks. def encrypt_and_sign(value, expires_at: nil, expires_in: nil, purpose: nil) - data = Messages::Metadata.wrap(value, expires_at: expires_at, expires_in: expires_in, purpose: purpose) - verifier.generate(_encrypt(data)) + verifier.generate(_encrypt(value, expires_at: expires_at, expires_in: expires_in, purpose: purpose)) end # Decrypt and verify a message. We need to verify the message in order to # avoid padding attacks. Reference: http://www.limited-entropy.com/padding-oracle-attacks. def decrypt_and_verify(data, purpose: nil) - Messages::Metadata.verify(_decrypt(verifier.verify(data)), purpose) + _decrypt(verifier.verify(data), purpose) end # Given a cipher, returns the key length of the cipher to help generate the key of desired size @@ -137,7 +136,7 @@ module ActiveSupport end private - def _encrypt(value) + def _encrypt(value, **metadata_options) cipher = new_cipher cipher.encrypt cipher.key = @secret @@ -146,7 +145,7 @@ module ActiveSupport iv = cipher.random_iv cipher.auth_data = "" if aead_mode? - encrypted_data = cipher.update(@serializer.dump(value)) + encrypted_data = cipher.update(Messages::Metadata.wrap(@serializer.dump(value), metadata_options)) encrypted_data << cipher.final blob = "#{::Base64.strict_encode64 encrypted_data}--#{::Base64.strict_encode64 iv}" @@ -154,7 +153,7 @@ module ActiveSupport blob end - def _decrypt(encrypted_message) + def _decrypt(encrypted_message, purpose) cipher = new_cipher encrypted_data, iv, auth_tag = encrypted_message.split("--".freeze).map { |v| ::Base64.strict_decode64(v) } @@ -174,7 +173,8 @@ module ActiveSupport decrypted_data = cipher.update(encrypted_data) decrypted_data << cipher.final - @serializer.load(decrypted_data) + message = Messages::Metadata.verify(decrypted_data, purpose) + @serializer.load(message) if message rescue OpenSSLCipherError, TypeError, ArgumentError raise InvalidMessage end diff --git a/activesupport/lib/active_support/message_verifier.rb b/activesupport/lib/active_support/message_verifier.rb index fdd2185f7f..7110d6d2c9 100644 --- a/activesupport/lib/active_support/message_verifier.rb +++ b/activesupport/lib/active_support/message_verifier.rb @@ -124,7 +124,8 @@ module ActiveSupport if valid_message?(signed_message) begin data = signed_message.split("--".freeze)[0] - Messages::Metadata.verify(@serializer.load(decode(data)), purpose) + message = Messages::Metadata.verify(decode(data), purpose) + @serializer.load(message) if message rescue ArgumentError => argument_error return if argument_error.message.include?("invalid base64") raise @@ -156,7 +157,7 @@ module ActiveSupport # verifier = ActiveSupport::MessageVerifier.new 's3Krit' # verifier.generate 'a private message' # => "BAhJIhRwcml2YXRlLW1lc3NhZ2UGOgZFVA==--e2d724331ebdee96a10fb99b089508d1c72bd772" def generate(value, expires_at: nil, expires_in: nil, purpose: nil) - data = encode(@serializer.dump(Messages::Metadata.wrap(value, expires_at: expires_at, expires_in: expires_in, purpose: purpose))) + data = encode(Messages::Metadata.wrap(@serializer.dump(value), expires_at: expires_at, expires_in: expires_in, purpose: purpose)) "#{data}--#{generate_digest(data)}" end diff --git a/activesupport/lib/active_support/messages/metadata.rb b/activesupport/lib/active_support/messages/metadata.rb index db14ac0b1c..e97caac766 100644 --- a/activesupport/lib/active_support/messages/metadata.rb +++ b/activesupport/lib/active_support/messages/metadata.rb @@ -1,30 +1,29 @@ # frozen_string_literal: true + require "time" module ActiveSupport module Messages #:nodoc: class Metadata #:nodoc: - def initialize(expires_at, purpose) - @expires_at, @purpose = expires_at, purpose.to_s + def initialize(message, expires_at = nil, purpose = nil) + @message, @expires_at, @purpose = message, expires_at, purpose + end + + def as_json(options = {}) + { _rails: { message: @message, exp: @expires_at, pur: @purpose } } end class << self def wrap(message, expires_at: nil, expires_in: nil, purpose: nil) if expires_at || expires_in || purpose - { "value" => message, "_rails" => { "exp" => pick_expiry(expires_at, expires_in), "pur" => purpose } } + JSON.encode new(encode(message), pick_expiry(expires_at, expires_in), purpose) else message end end def verify(message, purpose) - metadata = extract_metadata(message) - - if metadata.nil? - message if purpose.nil? - elsif metadata.match?(purpose) && metadata.fresh? - message["value"] - end + extract_metadata(message).verify(purpose) end private @@ -37,19 +36,36 @@ module ActiveSupport end def extract_metadata(message) - if message.is_a?(Hash) && message.key?("_rails") - new(message["_rails"]["exp"], message["_rails"]["pur"]) + data = JSON.decode(message) rescue nil + + if data.is_a?(Hash) && data.key?("_rails") + new(decode(data["_rails"]["message"]), data["_rails"]["exp"], data["_rails"]["pur"]) + else + new(message) end end - end - def match?(purpose) - @purpose == purpose.to_s + def encode(message) + ::Base64.strict_encode64(message) + end + + def decode(message) + ::Base64.strict_decode64(message) + end end - def fresh? - @expires_at.nil? || Time.now.utc < Time.iso8601(@expires_at) + def verify(purpose) + @message if match?(purpose) && fresh? end + + private + def match?(purpose) + @purpose.to_s == purpose.to_s + end + + def fresh? + @expires_at.nil? || Time.now.utc < Time.iso8601(@expires_at) + end end end end diff --git a/activesupport/lib/active_support/testing/assertions.rb b/activesupport/lib/active_support/testing/assertions.rb index f6366bfd39..e2bc51ff7a 100644 --- a/activesupport/lib/active_support/testing/assertions.rb +++ b/activesupport/lib/active_support/testing/assertions.rb @@ -157,7 +157,7 @@ module ActiveSupport after = exp.call if to == UNTRACKED - error = "#{expression.inspect} didn't changed" + error = "#{expression.inspect} didn't change" error = "#{message}.\n#{error}" if message assert_not_equal before, after, error else diff --git a/activesupport/test/cache/stores/mem_cache_store_test.rb b/activesupport/test/cache/stores/mem_cache_store_test.rb index becc1f7fbf..1b73fb65eb 100644 --- a/activesupport/test/cache/stores/mem_cache_store_test.rb +++ b/activesupport/test/cache/stores/mem_cache_store_test.rb @@ -24,7 +24,7 @@ class MemCacheStoreTest < ActiveSupport::TestCase @data = @cache.instance_variable_get(:@data) @cache.clear @cache.silence! - @cache.logger = ActiveSupport::Logger.new("/dev/null") + @cache.logger = ActiveSupport::Logger.new(File::NULL) end include CacheStoreBehavior diff --git a/activesupport/test/core_ext/module_test.rb b/activesupport/test/core_ext/module_test.rb index 69c8a312d8..e918823074 100644 --- a/activesupport/test/core_ext/module_test.rb +++ b/activesupport/test/core_ext/module_test.rb @@ -374,6 +374,14 @@ class ModuleTest < ActiveSupport::TestCase assert_match(/undefined method `my_fake_method' for/, e.message) end + def test_delegate_missing_to_raises_delegation_error_if_target_nil + e = assert_raises(Module::DelegationError) do + DecoratedTester.new(nil).name + end + + assert_equal "name delegated to client, but client is nil", e.message + end + def test_delegate_missing_to_affects_respond_to assert DecoratedTester.new(@david).respond_to?(:name) assert_not DecoratedTester.new(@david).respond_to?(:private_name) diff --git a/activesupport/test/core_ext/string_ext_test.rb b/activesupport/test/core_ext/string_ext_test.rb index 0afff5aa50..9fd6d8ac0f 100644 --- a/activesupport/test/core_ext/string_ext_test.rb +++ b/activesupport/test/core_ext/string_ext_test.rb @@ -108,6 +108,13 @@ class StringInflectionsTest < ActiveSupport::TestCase assert_equal("capital", "Capital".camelize(:lower)) end + def test_camelize_invalid_option + e = assert_raise ArgumentError do + "Capital".camelize(nil) + end + assert_equal("Invalid option, use either :upper or :lower.", e.message) + end + def test_dasherize UnderscoresToDashes.each do |underscored, dasherized| assert_equal(dasherized, underscored.dasherize) diff --git a/activesupport/test/current_attributes_test.rb b/activesupport/test/current_attributes_test.rb index d333ffc8f5..1669f08f68 100644 --- a/activesupport/test/current_attributes_test.rb +++ b/activesupport/test/current_attributes_test.rb @@ -30,7 +30,14 @@ class CurrentAttributesTest < ActiveSupport::TestCase end end - setup { Current.reset } + setup do + @original_time_zone = Time.zone + Current.reset + end + + teardown do + Time.zone = @original_time_zone + end test "read and write attribute" do Current.world = "world/1" diff --git a/activesupport/test/message_verifier_test.rb b/activesupport/test/message_verifier_test.rb index f626ab745f..fbeafca203 100644 --- a/activesupport/test/message_verifier_test.rb +++ b/activesupport/test/message_verifier_test.rb @@ -101,12 +101,12 @@ class MessageVerifierMetadataTest < ActiveSupport::TestCase def test_verify_raises_when_purpose_differs assert_raise(ActiveSupport::MessageVerifier::InvalidSignature) do - @verifier.verify(@verifier.generate(@message, purpose: "payment"), purpose: "shipping") + @verifier.verify(generate(data, purpose: "payment"), purpose: "shipping") end end def test_verify_raises_when_expired - signed_message = @verifier.generate(@message, expires_in: 1.month) + signed_message = generate(data, expires_in: 1.month) travel 2.months assert_raise(ActiveSupport::MessageVerifier::InvalidSignature) do @@ -141,3 +141,18 @@ class MessageVerifierMetadataJSONTest < MessageVerifierMetadataTest { serializer: MessageVerifierTest::JSONSerializer.new } end end + +class MessageEncryptorMetadataNullSerializerTest < MessageVerifierMetadataTest + private + def data + "string message" + end + + def null_serializing? + true + end + + def verifier_options + { serializer: ActiveSupport::MessageEncryptor::NullSerializer } + end +end diff --git a/activesupport/test/metadata/shared_metadata_tests.rb b/activesupport/test/metadata/shared_metadata_tests.rb index 7d88e255c7..08bb0c648e 100644 --- a/activesupport/test/metadata/shared_metadata_tests.rb +++ b/activesupport/test/metadata/shared_metadata_tests.rb @@ -1,57 +1,57 @@ # frozen_string_literal: true module SharedMessageMetadataTests - def setup - @message = { "credit_card_no" => "5012-6784-9087-5678", "card_holder" => { "name" => "Donald" } } - - super - end - def teardown travel_back - super end + def null_serializing? + false + end + def test_encryption_and_decryption_with_same_purpose - assert_equal @message, parse(generate(@message, purpose: "checkout"), purpose: "checkout") - assert_equal @message, parse(generate(@message)) + assert_equal data, parse(generate(data, purpose: "checkout"), purpose: "checkout") + assert_equal data, parse(generate(data)) string_message = "address: #23, main street" assert_equal string_message, parse(generate(string_message, purpose: "shipping"), purpose: "shipping") + end - array_message = ["credit_card_no: 5012-6748-9087-5678", { "card_holder" => "Donald", "issued_on" => Time.local(2017) }, 12345] - assert_equal array_message, parse(generate(array_message, purpose: "registration"), purpose: "registration") + def test_verifies_array_when_purpose_matches + unless null_serializing? + data = [ "credit_card_no: 5012-6748-9087-5678", { "card_holder" => "Donald", "issued_on" => Time.local(2017) }, 12345 ] + assert_equal data, parse(generate(data, purpose: :registration), purpose: :registration) + end end def test_encryption_and_decryption_with_different_purposes_returns_nil - assert_nil parse(generate(@message, purpose: "payment"), purpose: "sign up") - assert_nil parse(generate(@message, purpose: "payment")) - assert_nil parse(generate(@message), purpose: "sign up") - assert_nil parse(generate(@message), purpose: "") + assert_nil parse(generate(data, purpose: "payment"), purpose: "sign up") + assert_nil parse(generate(data, purpose: "payment")) + assert_nil parse(generate(data), purpose: "sign up") end def test_purpose_using_symbols - assert_equal @message, parse(generate(@message, purpose: :checkout), purpose: :checkout) - assert_equal @message, parse(generate(@message, purpose: :checkout), purpose: "checkout") - assert_equal @message, parse(generate(@message, purpose: "checkout"), purpose: :checkout) + assert_equal data, parse(generate(data, purpose: :checkout), purpose: :checkout) + assert_equal data, parse(generate(data, purpose: :checkout), purpose: "checkout") + assert_equal data, parse(generate(data, purpose: "checkout"), purpose: :checkout) end def test_passing_expires_at_sets_expiration_date - encrypted_message = generate(@message, expires_at: 1.hour.from_now) + encrypted_message = generate(data, expires_at: 1.hour.from_now) travel 59.minutes - assert_equal @message, parse(encrypted_message) + assert_equal data, parse(encrypted_message) travel 2.minutes assert_nil parse(encrypted_message) end def test_set_relative_expiration_date_by_passing_expires_in - encrypted_message = generate(@message, expires_in: 2.hours) + encrypted_message = generate(data, expires_in: 2.hours) travel 1.hour - assert_equal @message, parse(encrypted_message) + assert_equal data, parse(encrypted_message) travel 1.hour + 1.second assert_nil parse(encrypted_message) @@ -59,10 +59,10 @@ module SharedMessageMetadataTests def test_passing_expires_in_less_than_a_second_is_not_expired freeze_time do - encrypted_message = generate(@message, expires_in: 1.second) + encrypted_message = generate(data, expires_in: 1.second) travel 0.5.seconds - assert_equal @message, parse(encrypted_message) + assert_equal data, parse(encrypted_message) travel 1.second assert_nil parse(encrypted_message) @@ -70,19 +70,24 @@ module SharedMessageMetadataTests end def test_favor_expires_at_over_expires_in - payment_related_message = generate(@message, purpose: "payment", expires_at: 2.year.from_now, expires_in: 1.second) + payment_related_message = generate(data, purpose: "payment", expires_at: 2.year.from_now, expires_in: 1.second) travel 1.year - assert_equal @message, parse(payment_related_message, purpose: :payment) + assert_equal data, parse(payment_related_message, purpose: :payment) travel 1.year + 1.day assert_nil parse(payment_related_message, purpose: "payment") end def test_skip_expires_at_and_expires_in_to_disable_expiration_check - payment_related_message = generate(@message, purpose: "payment") + payment_related_message = generate(data, purpose: "payment") travel 100.years - assert_equal @message, parse(payment_related_message, purpose: "payment") + assert_equal data, parse(payment_related_message, purpose: "payment") end + + private + def data + { "credit_card_no" => "5012-6784-9087-5678", "card_holder" => { "name" => "Donald" } } + end end |