activesupport 7.0.0.alpha2 → 7.0.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of activesupport might be problematic. Click here for more details.

Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +103 -0
  3. data/lib/active_support/cache/mem_cache_store.rb +9 -5
  4. data/lib/active_support/cache/memory_store.rb +2 -2
  5. data/lib/active_support/cache/redis_cache_store.rb +3 -8
  6. data/lib/active_support/cache/strategy/local_cache.rb +6 -12
  7. data/lib/active_support/callbacks.rb +145 -50
  8. data/lib/active_support/code_generator.rb +65 -0
  9. data/lib/active_support/core_ext/array/conversions.rb +3 -1
  10. data/lib/active_support/core_ext/array/deprecated_conversions.rb +25 -0
  11. data/lib/active_support/core_ext/array.rb +1 -0
  12. data/lib/active_support/core_ext/class/subclasses.rb +4 -2
  13. data/lib/active_support/core_ext/date/calculations.rb +2 -2
  14. data/lib/active_support/core_ext/date/conversions.rb +3 -3
  15. data/lib/active_support/core_ext/date/deprecated_conversions.rb +26 -0
  16. data/lib/active_support/core_ext/date.rb +1 -0
  17. data/lib/active_support/core_ext/date_and_time/compatibility.rb +1 -1
  18. data/lib/active_support/core_ext/date_time/conversions.rb +5 -5
  19. data/lib/active_support/core_ext/date_time/deprecated_conversions.rb +22 -0
  20. data/lib/active_support/core_ext/date_time.rb +1 -0
  21. data/lib/active_support/core_ext/digest/uuid.rb +26 -1
  22. data/lib/active_support/core_ext/module/attribute_accessors.rb +2 -0
  23. data/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb +19 -10
  24. data/lib/active_support/core_ext/numeric/conversions.rb +78 -75
  25. data/lib/active_support/core_ext/numeric/deprecated_conversions.rb +60 -0
  26. data/lib/active_support/core_ext/numeric.rb +1 -0
  27. data/lib/active_support/core_ext/object/with_options.rb +20 -1
  28. data/lib/active_support/core_ext/pathname/existence.rb +21 -0
  29. data/lib/active_support/core_ext/pathname.rb +3 -0
  30. data/lib/active_support/core_ext/range/conversions.rb +8 -8
  31. data/lib/active_support/core_ext/range/deprecated_conversions.rb +26 -0
  32. data/lib/active_support/core_ext/range/include_time_with_zone.rb +4 -25
  33. data/lib/active_support/core_ext/range.rb +1 -1
  34. data/lib/active_support/core_ext/time/calculations.rb +1 -1
  35. data/lib/active_support/core_ext/time/conversions.rb +4 -3
  36. data/lib/active_support/core_ext/time/deprecated_conversions.rb +22 -0
  37. data/lib/active_support/core_ext/time/zones.rb +2 -2
  38. data/lib/active_support/core_ext/time.rb +1 -0
  39. data/lib/active_support/core_ext/uri.rb +3 -13
  40. data/lib/active_support/core_ext.rb +1 -0
  41. data/lib/active_support/current_attributes.rb +26 -25
  42. data/lib/active_support/descendants_tracker.rb +175 -69
  43. data/lib/active_support/duration.rb +4 -3
  44. data/lib/active_support/error_reporter.rb +117 -0
  45. data/lib/active_support/execution_context/test_helper.rb +13 -0
  46. data/lib/active_support/execution_context.rb +53 -0
  47. data/lib/active_support/execution_wrapper.rb +30 -4
  48. data/lib/active_support/executor/test_helper.rb +7 -0
  49. data/lib/active_support/fork_tracker.rb +18 -9
  50. data/lib/active_support/gem_version.rb +1 -1
  51. data/lib/active_support/html_safe_translation.rb +43 -0
  52. data/lib/active_support/i18n_railtie.rb +1 -1
  53. data/lib/active_support/inflector/inflections.rb +12 -3
  54. data/lib/active_support/inflector/methods.rb +2 -2
  55. data/lib/active_support/isolated_execution_state.rb +56 -0
  56. data/lib/active_support/logger_thread_safe_level.rb +2 -3
  57. data/lib/active_support/message_encryptor.rb +5 -0
  58. data/lib/active_support/message_verifier.rb +42 -10
  59. data/lib/active_support/multibyte/unicode.rb +0 -12
  60. data/lib/active_support/notifications/fanout.rb +61 -55
  61. data/lib/active_support/notifications/instrumenter.rb +15 -15
  62. data/lib/active_support/notifications.rb +5 -21
  63. data/lib/active_support/option_merger.rb +4 -0
  64. data/lib/active_support/per_thread_registry.rb +4 -0
  65. data/lib/active_support/railtie.rb +38 -11
  66. data/lib/active_support/ruby_features.rb +7 -0
  67. data/lib/active_support/subscriber.rb +2 -18
  68. data/lib/active_support/tagged_logging.rb +1 -1
  69. data/lib/active_support/testing/deprecation.rb +52 -1
  70. data/lib/active_support/testing/isolation.rb +1 -1
  71. data/lib/active_support/time_with_zone.rb +34 -6
  72. data/lib/active_support/values/time_zone.rb +5 -0
  73. data/lib/active_support/xml_mini.rb +3 -3
  74. data/lib/active_support.rb +7 -4
  75. metadata +25 -8
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveSupport
4
+ # +ActiveSupport::ErrorReporter+ is a common interface for error reporting services.
5
+ #
6
+ # To rescue and report any unhandled error, you can use the +handle+ method:
7
+ #
8
+ # Rails.error.handle do
9
+ # do_something!
10
+ # end
11
+ #
12
+ # If an error is raised, it will be reported and swallowed.
13
+ #
14
+ # Alternatively if you want to report the error but not swallow it, you can use +record+
15
+ #
16
+ # Rails.error.record do
17
+ # do_something!
18
+ # end
19
+ #
20
+ # Both methods can be restricted to only handle a specific exception class
21
+ #
22
+ # maybe_tags = Rails.error.handle(Redis::BaseError) { redis.get("tags") }
23
+ #
24
+ # You can also pass some extra context information that may be used by the error subscribers:
25
+ #
26
+ # Rails.error.handle(context: { section: "admin" }) do
27
+ # # ...
28
+ # end
29
+ #
30
+ # Additionally a +severity+ can be passed along to communicate how important the error report is.
31
+ # +severity+ can be one of +:error+, +:warning+ or +:info+. Handled errors default to the +:warning+
32
+ # severity, and unhandled ones to +error+.
33
+ #
34
+ # Both +handle+ and +record+ pass through the return value from the block. In the case of +handle+
35
+ # rescuing an error, a fallback can be provided. The fallback must be a callable whose result will
36
+ # be returned when the block raises and is handled:
37
+ #
38
+ # user = Rails.error.handle(fallback: -> { User.anonymous }) do
39
+ # User.find_by(params)
40
+ # end
41
+ class ErrorReporter
42
+ SEVERITIES = %i(error warning info)
43
+
44
+ attr_accessor :logger
45
+
46
+ def initialize(*subscribers, logger: nil)
47
+ @subscribers = subscribers.flatten
48
+ @logger = logger
49
+ end
50
+
51
+ # Report any unhandled exception, and swallow it.
52
+ #
53
+ # Rails.error.handle do
54
+ # 1 + '1'
55
+ # end
56
+ #
57
+ def handle(error_class = StandardError, severity: :warning, context: {}, fallback: nil)
58
+ yield
59
+ rescue error_class => error
60
+ report(error, handled: true, severity: severity, context: context)
61
+ fallback.call if fallback
62
+ end
63
+
64
+ def record(error_class = StandardError, severity: :error, context: {})
65
+ yield
66
+ rescue error_class => error
67
+ report(error, handled: false, severity: severity, context: context)
68
+ raise
69
+ end
70
+
71
+ # Register a new error subscriber. The subscriber must respond to
72
+ #
73
+ # report(Exception, handled: Boolean, context: Hash)
74
+ #
75
+ # The +report+ method +should+ never raise an error.
76
+ def subscribe(subscriber)
77
+ unless subscriber.respond_to?(:report)
78
+ raise ArgumentError, "Error subscribers must respond to #report"
79
+ end
80
+ @subscribers << subscriber
81
+ end
82
+
83
+ # Update the execution context that is accessible to error subscribers
84
+ #
85
+ # Rails.error.set_context(section: "checkout", user_id: @user.id)
86
+ #
87
+ # See +ActiveSupport::ExecutionContext.set+
88
+ def set_context(...)
89
+ ActiveSupport::ExecutionContext.set(...)
90
+ end
91
+
92
+ # When the block based +handle+ and +record+ methods are not suitable, you can directly use +report+
93
+ #
94
+ # Rails.error.report(error, handled: true)
95
+ def report(error, handled:, severity: handled ? :warning : :error, context: {})
96
+ unless SEVERITIES.include?(severity)
97
+ raise ArgumentError, "severity must be one of #{SEVERITIES.map(&:inspect).join(", ")}, got: #{severity.inspect}"
98
+ end
99
+
100
+ full_context = ActiveSupport::ExecutionContext.to_h.merge(context)
101
+ @subscribers.each do |subscriber|
102
+ subscriber.report(error, handled: handled, severity: severity, context: full_context)
103
+ rescue => subscriber_error
104
+ if logger
105
+ logger.fatal(
106
+ "Error subscriber raised an error: #{subscriber_error.message} (#{subscriber_error.class})\n" +
107
+ subscriber_error.backtrace.join("\n")
108
+ )
109
+ else
110
+ raise
111
+ end
112
+ end
113
+
114
+ nil
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveSupport::ExecutionContext::TestHelper # :nodoc:
4
+ def before_setup
5
+ ActiveSupport::ExecutionContext.clear
6
+ super
7
+ end
8
+
9
+ def after_teardown
10
+ super
11
+ ActiveSupport::ExecutionContext.clear
12
+ end
13
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveSupport
4
+ module ExecutionContext # :nodoc:
5
+ @after_change_callbacks = []
6
+ class << self
7
+ def after_change(&block)
8
+ @after_change_callbacks << block
9
+ end
10
+
11
+ # Updates the execution context. If a block is given, it resets the provided keys to their
12
+ # previous value once the block exits.
13
+ def set(**options)
14
+ options.symbolize_keys!
15
+ keys = options.keys
16
+
17
+ store = self.store
18
+
19
+ previous_context = keys.zip(store.values_at(*keys)).to_h
20
+
21
+ store.merge!(options)
22
+ @after_change_callbacks.each(&:call)
23
+
24
+ if block_given?
25
+ begin
26
+ yield
27
+ ensure
28
+ store.merge!(previous_context)
29
+ @after_change_callbacks.each(&:call)
30
+ end
31
+ end
32
+ end
33
+
34
+ def []=(key, value)
35
+ store[key.to_sym] = value
36
+ @after_change_callbacks.each(&:call)
37
+ end
38
+
39
+ def to_h
40
+ store.dup
41
+ end
42
+
43
+ def clear
44
+ store.clear
45
+ end
46
+
47
+ private
48
+ def store
49
+ IsolatedExecutionState[:active_support_execution_context] ||= {}
50
+ end
51
+ end
52
+ end
53
+ end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support/error_reporter"
3
4
  require "active_support/callbacks"
4
5
  require "concurrent/hash"
5
6
 
@@ -86,15 +87,32 @@ module ActiveSupport
86
87
  instance = run!
87
88
  begin
88
89
  yield
90
+ rescue => error
91
+ error_reporter.report(error, handled: false)
92
+ raise
89
93
  ensure
90
94
  instance.complete!
91
95
  end
92
96
  end
93
97
 
98
+ def self.perform # :nodoc:
99
+ instance = new
100
+ instance.run
101
+ begin
102
+ yield
103
+ ensure
104
+ instance.complete
105
+ end
106
+ end
107
+
94
108
  class << self # :nodoc:
95
109
  attr_accessor :active
96
110
  end
97
111
 
112
+ def self.error_reporter
113
+ @error_reporter ||= ActiveSupport::ErrorReporter.new
114
+ end
115
+
98
116
  def self.inherited(other) # :nodoc:
99
117
  super
100
118
  other.active = Concurrent::Hash.new
@@ -103,11 +121,15 @@ module ActiveSupport
103
121
  self.active = Concurrent::Hash.new
104
122
 
105
123
  def self.active? # :nodoc:
106
- @active[Thread.current]
124
+ @active[IsolatedExecutionState.unique_id]
107
125
  end
108
126
 
109
127
  def run! # :nodoc:
110
- self.class.active[Thread.current] = true
128
+ self.class.active[IsolatedExecutionState.unique_id] = true
129
+ run
130
+ end
131
+
132
+ def run # :nodoc:
111
133
  run_callbacks(:run)
112
134
  end
113
135
 
@@ -116,9 +138,13 @@ module ActiveSupport
116
138
  #
117
139
  # Where possible, prefer +wrap+.
118
140
  def complete!
119
- run_callbacks(:complete)
141
+ complete
120
142
  ensure
121
- self.class.active.delete Thread.current
143
+ self.class.active.delete(IsolatedExecutionState.unique_id)
144
+ end
145
+
146
+ def complete # :nodoc:
147
+ run_callbacks(:complete)
122
148
  end
123
149
 
124
150
  private
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveSupport::Executor::TestHelper # :nodoc:
4
+ def run(...)
5
+ Rails.application.executor.perform { super }
6
+ end
7
+ end
@@ -2,6 +2,16 @@
2
2
 
3
3
  module ActiveSupport
4
4
  module ForkTracker # :nodoc:
5
+ module ModernCoreExt
6
+ def _fork
7
+ pid = super
8
+ if pid == 0
9
+ ForkTracker.check!
10
+ end
11
+ pid
12
+ end
13
+ end
14
+
5
15
  module CoreExt
6
16
  def fork(...)
7
17
  if block_given?
@@ -20,11 +30,7 @@ module ActiveSupport
20
30
 
21
31
  module CoreExtPrivate
22
32
  include CoreExt
23
-
24
- private
25
- def fork(...)
26
- super
27
- end
33
+ private :fork
28
34
  end
29
35
 
30
36
  @pid = Process.pid
@@ -32,15 +38,18 @@ module ActiveSupport
32
38
 
33
39
  class << self
34
40
  def check!
35
- if @pid != Process.pid
41
+ new_pid = Process.pid
42
+ if @pid != new_pid
36
43
  @callbacks.each(&:call)
37
- @pid = Process.pid
44
+ @pid = new_pid
38
45
  end
39
46
  end
40
47
 
41
48
  def hook!
42
- if Process.respond_to?(:fork)
43
- ::Object.prepend(CoreExtPrivate)
49
+ if Process.respond_to?(:_fork) # Ruby 3.1+
50
+ ::Process.singleton_class.prepend(ModernCoreExt)
51
+ elsif Process.respond_to?(:fork)
52
+ ::Object.prepend(CoreExtPrivate) if RUBY_VERSION < "3.0"
44
53
  ::Kernel.prepend(CoreExtPrivate)
45
54
  ::Kernel.singleton_class.prepend(CoreExt)
46
55
  ::Process.singleton_class.prepend(CoreExt)
@@ -10,7 +10,7 @@ module ActiveSupport
10
10
  MAJOR = 7
11
11
  MINOR = 0
12
12
  TINY = 0
13
- PRE = "alpha2"
13
+ PRE = nil
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
16
  end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveSupport
4
+ module HtmlSafeTranslation # :nodoc:
5
+ extend self
6
+
7
+ def translate(key, **options)
8
+ if html_safe_translation_key?(key)
9
+ html_safe_options = html_escape_translation_options(options)
10
+ translation = I18n.translate(key, **html_safe_options)
11
+ html_safe_translation(translation)
12
+ else
13
+ I18n.translate(key, **options)
14
+ end
15
+ end
16
+
17
+ private
18
+ def html_safe_translation_key?(key)
19
+ /(?:_|\b)html\z/.match?(key)
20
+ end
21
+
22
+ def html_escape_translation_options(options)
23
+ options.each do |name, value|
24
+ unless i18n_option?(name) || (name == :count && value.is_a?(Numeric))
25
+ options[name] = ERB::Util.html_escape(value.to_s)
26
+ end
27
+ end
28
+ end
29
+
30
+ def i18n_option?(name)
31
+ (@i18n_option_names ||= I18n::RESERVED_KEYS.to_set).include?(name)
32
+ end
33
+
34
+
35
+ def html_safe_translation(translation)
36
+ if translation.respond_to?(:map)
37
+ translation.map { |element| element.respond_to?(:html_safe) ? element.html_safe : element }
38
+ else
39
+ translation.respond_to?(:html_safe) ? translation.html_safe : translation
40
+ end
41
+ end
42
+ end
43
+ end
@@ -77,7 +77,7 @@ module I18n
77
77
 
78
78
  def self.forward_raise_on_missing_translations_config(app)
79
79
  ActiveSupport.on_load(:action_view) do
80
- self.raise_on_missing_translations = app.config.i18n.raise_on_missing_translations
80
+ ActionView::Helpers::TranslationHelper.raise_on_missing_translations = app.config.i18n.raise_on_missing_translations
81
81
  end
82
82
 
83
83
  ActiveSupport.on_load(:action_controller) do
@@ -222,15 +222,24 @@ module ActiveSupport
222
222
  # Clears the loaded inflections within a given scope (default is
223
223
  # <tt>:all</tt>). Give the scope as a symbol of the inflection type, the
224
224
  # options are: <tt>:plurals</tt>, <tt>:singulars</tt>, <tt>:uncountables</tt>,
225
- # <tt>:humans</tt>.
225
+ # <tt>:humans</tt>, <tt>:acronyms</tt>.
226
226
  #
227
227
  # clear :all
228
228
  # clear :plurals
229
229
  def clear(scope = :all)
230
230
  case scope
231
231
  when :all
232
- @plurals, @singulars, @uncountables, @humans = [], [], Uncountables.new, []
233
- else
232
+ clear(:acronyms)
233
+ clear(:plurals)
234
+ clear(:singulars)
235
+ clear(:uncountables)
236
+ clear(:humans)
237
+ when :acronyms
238
+ @acronyms = {}
239
+ define_acronym_regex_patterns
240
+ when :uncountables
241
+ @uncountables = Uncountables.new
242
+ when :plurals, :singulars, :humans
234
243
  instance_variable_set "@#{scope}", []
235
244
  end
236
245
  end
@@ -97,7 +97,7 @@ module ActiveSupport
97
97
  return camel_cased_word.to_s unless /[A-Z-]|::/.match?(camel_cased_word)
98
98
  word = camel_cased_word.to_s.gsub("::", "/")
99
99
  word.gsub!(inflections.acronyms_underscore_regex) { "#{$1 && '_' }#{$2.downcase}" }
100
- word.gsub!(/([A-Z\d]+)(?=[A-Z][a-z])|([a-z\d])(?=[A-Z])/) { ($1 || $2) << "_" }
100
+ word.gsub!(/([A-Z]+)(?=[A-Z][a-z])|([a-z\d])(?=[A-Z])/) { ($1 || $2) << "_" }
101
101
  word.tr!("-", "_")
102
102
  word.downcase!
103
103
  word
@@ -109,7 +109,7 @@ module ActiveSupport
109
109
  #
110
110
  # * Applies human inflection rules to the argument.
111
111
  # * Deletes leading underscores, if any.
112
- # * Removes a "_id" suffix if present.
112
+ # * Removes an "_id" suffix if present.
113
113
  # * Replaces underscores with spaces, if any.
114
114
  # * Downcases all words except acronyms.
115
115
  # * Capitalizes the first word.
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fiber"
4
+
5
+ module ActiveSupport
6
+ module IsolatedExecutionState # :nodoc:
7
+ @isolation_level = :thread
8
+
9
+ Thread.attr_accessor :active_support_execution_state
10
+ Fiber.attr_accessor :active_support_execution_state
11
+
12
+ class << self
13
+ attr_reader :isolation_level
14
+
15
+ def isolation_level=(level)
16
+ unless %i(thread fiber).include?(level)
17
+ raise ArgumentError, "isolation_level must be `:thread` or `:fiber`, got: `#{level.inspect}`"
18
+ end
19
+
20
+ if level != isolation_level
21
+ clear
22
+ singleton_class.alias_method(:current, "current_#{level}")
23
+ singleton_class.send(:private, :current)
24
+ @isolation_level = level
25
+ end
26
+ end
27
+
28
+ def unique_id
29
+ self[:__id__] ||= Object.new
30
+ end
31
+
32
+ def [](key)
33
+ current[key]
34
+ end
35
+
36
+ def []=(key, value)
37
+ current[key] = value
38
+ end
39
+
40
+ def clear
41
+ current.clear
42
+ end
43
+
44
+ private
45
+ def current_thread
46
+ Thread.current.active_support_execution_state ||= {}
47
+ end
48
+
49
+ def current_fiber
50
+ Fiber.current.active_support_execution_state ||= {}
51
+ end
52
+
53
+ alias_method :current, :current_thread
54
+ end
55
+ end
56
+ end
@@ -18,8 +18,7 @@ module ActiveSupport
18
18
  end
19
19
 
20
20
  def local_level
21
- # Note: Thread#[] is fiber-local
22
- Thread.current[:logger_thread_safe_level]
21
+ IsolatedExecutionState[:logger_thread_safe_level]
23
22
  end
24
23
 
25
24
  def local_level=(level)
@@ -31,7 +30,7 @@ module ActiveSupport
31
30
  else
32
31
  raise ArgumentError, "Invalid log level: #{level.inspect}"
33
32
  end
34
- Thread.current[:logger_thread_safe_level] = level
33
+ IsolatedExecutionState[:logger_thread_safe_level] = level
35
34
  end
36
35
 
37
36
  def level
@@ -22,6 +22,11 @@ module ActiveSupport
22
22
  # crypt = ActiveSupport::MessageEncryptor.new(key) # => #<ActiveSupport::MessageEncryptor ...>
23
23
  # encrypted_data = crypt.encrypt_and_sign('my secret data') # => "NlFBTTMwOUV5UlA1QlNEN2xkY2d6eThYWWh..."
24
24
  # crypt.decrypt_and_verify(encrypted_data) # => "my secret data"
25
+ # The +decrypt_and_verify+ method will raise an
26
+ # <tt>ActiveSupport::MessageEncryptor::InvalidMessage</tt> exception if the data
27
+ # provided cannot be decrypted or verified.
28
+ #
29
+ # crypt.decrypt_and_verify('not encrypted data') # => ActiveSupport::MessageEncryptor::InvalidMessage
25
30
  #
26
31
  # === Confining messages to a specific purpose
27
32
  #
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "openssl"
3
4
  require "base64"
4
5
  require "active_support/core_ext/object/blank"
5
6
  require "active_support/security_utils"
@@ -103,10 +104,13 @@ module ActiveSupport
103
104
 
104
105
  class InvalidSignature < StandardError; end
105
106
 
107
+ SEPARATOR = "--" # :nodoc:
108
+ SEPARATOR_LENGTH = SEPARATOR.length # :nodoc:
109
+
106
110
  def initialize(secret, digest: nil, serializer: nil)
107
111
  raise ArgumentError, "Secret should not be nil." unless secret
108
112
  @secret = secret
109
- @digest = digest || "SHA1"
113
+ @digest = digest&.to_s || "SHA1"
110
114
  @serializer = serializer || Marshal
111
115
  end
112
116
 
@@ -120,10 +124,8 @@ module ActiveSupport
120
124
  # tampered_message = signed_message.chop # editing the message invalidates the signature
121
125
  # verifier.valid_message?(tampered_message) # => false
122
126
  def valid_message?(signed_message)
123
- return if signed_message.nil? || !signed_message.valid_encoding? || signed_message.blank?
124
-
125
- data, digest = signed_message.split("--")
126
- data.present? && digest.present? && ActiveSupport::SecurityUtils.secure_compare(digest, generate_digest(data))
127
+ data, digest = get_data_and_digest_from(signed_message)
128
+ digest_matches_data?(digest, data)
127
129
  end
128
130
 
129
131
  # Decodes the signed message using the +MessageVerifier+'s secret.
@@ -148,9 +150,9 @@ module ActiveSupport
148
150
  # incompatible_message = "test--dad7b06c94abba8d46a15fafaef56c327665d5ff"
149
151
  # verifier.verified(incompatible_message) # => TypeError: incompatible marshal file format
150
152
  def verified(signed_message, purpose: nil, **)
151
- if valid_message?(signed_message)
153
+ data, digest = get_data_and_digest_from(signed_message)
154
+ if digest_matches_data?(digest, data)
152
155
  begin
153
- data = signed_message.split("--")[0]
154
156
  message = Messages::Metadata.verify(decode(data), purpose)
155
157
  @serializer.load(message) if message
156
158
  rescue ArgumentError => argument_error
@@ -185,7 +187,7 @@ module ActiveSupport
185
187
  # verifier.generate 'a private message' # => "BAhJIhRwcml2YXRlLW1lc3NhZ2UGOgZFVA==--e2d724331ebdee96a10fb99b089508d1c72bd772"
186
188
  def generate(value, expires_at: nil, expires_in: nil, purpose: nil)
187
189
  data = encode(Messages::Metadata.wrap(@serializer.dump(value), expires_at: expires_at, expires_in: expires_in, purpose: purpose))
188
- "#{data}--#{generate_digest(data)}"
190
+ "#{data}#{SEPARATOR}#{generate_digest(data)}"
189
191
  end
190
192
 
191
193
  private
@@ -198,8 +200,38 @@ module ActiveSupport
198
200
  end
199
201
 
200
202
  def generate_digest(data)
201
- require "openssl" unless defined?(OpenSSL)
202
- OpenSSL::HMAC.hexdigest(OpenSSL::Digest.const_get(@digest).new, @secret, data)
203
+ OpenSSL::HMAC.hexdigest(@digest, @secret, data)
204
+ end
205
+
206
+ def digest_length_in_hex
207
+ # In hexadecimal (AKA base16) it takes 4 bits to represent a character,
208
+ # hence we multiply the digest's length (in bytes) by 8 to get it in
209
+ # bits and divide by 4 to get its number of characters it hex. Well, 8
210
+ # divided by 4 is 2.
211
+ @digest_length_in_hex ||= OpenSSL::Digest.new(@digest).digest_length * 2
212
+ end
213
+
214
+ def separator_index_for(signed_message)
215
+ index = signed_message.length - digest_length_in_hex - SEPARATOR_LENGTH
216
+ return if index.negative? || signed_message[index, SEPARATOR_LENGTH] != SEPARATOR
217
+
218
+ index
219
+ end
220
+
221
+ def get_data_and_digest_from(signed_message)
222
+ return if signed_message.nil? || !signed_message.valid_encoding? || signed_message.empty?
223
+
224
+ separator_index = separator_index_for(signed_message)
225
+ return if separator_index.nil?
226
+
227
+ data = signed_message[0...separator_index]
228
+ digest = signed_message[separator_index + SEPARATOR_LENGTH..-1]
229
+
230
+ [data, digest]
231
+ end
232
+
233
+ def digest_matches_data?(digest, data)
234
+ data.present? && digest.present? && ActiveSupport::SecurityUtils.secure_compare(digest, generate_digest(data))
203
235
  end
204
236
  end
205
237
  end
@@ -8,18 +8,6 @@ module ActiveSupport
8
8
  # The Unicode version that is supported by the implementation
9
9
  UNICODE_VERSION = RbConfig::CONFIG["UNICODE_VERSION"]
10
10
 
11
- def default_normalization_form
12
- ActiveSupport::Deprecation.warn(
13
- "ActiveSupport::Multibyte::Unicode.default_normalization_form is deprecated and will be removed in Rails 7.0."
14
- )
15
- end
16
-
17
- def default_normalization_form=(_)
18
- ActiveSupport::Deprecation.warn(
19
- "ActiveSupport::Multibyte::Unicode.default_normalization_form= is deprecated and will be removed in Rails 7.0."
20
- )
21
- end
22
-
23
11
  # Decompose composed characters to the decomposed form.
24
12
  def decompose(type, codepoints)
25
13
  if type == :compatibility