activesupport 7.0.4 → 7.1.3.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (187) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +996 -232
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +6 -6
  5. data/lib/active_support/actionable_error.rb +3 -1
  6. data/lib/active_support/array_inquirer.rb +2 -0
  7. data/lib/active_support/backtrace_cleaner.rb +25 -5
  8. data/lib/active_support/benchmarkable.rb +1 -0
  9. data/lib/active_support/broadcast_logger.rb +250 -0
  10. data/lib/active_support/builder.rb +1 -1
  11. data/lib/active_support/cache/coder.rb +153 -0
  12. data/lib/active_support/cache/entry.rb +134 -0
  13. data/lib/active_support/cache/file_store.rb +37 -10
  14. data/lib/active_support/cache/mem_cache_store.rb +100 -76
  15. data/lib/active_support/cache/memory_store.rb +78 -24
  16. data/lib/active_support/cache/null_store.rb +6 -0
  17. data/lib/active_support/cache/redis_cache_store.rb +153 -141
  18. data/lib/active_support/cache/serializer_with_fallback.rb +175 -0
  19. data/lib/active_support/cache/strategy/local_cache.rb +29 -14
  20. data/lib/active_support/cache.rb +331 -252
  21. data/lib/active_support/callbacks.rb +44 -21
  22. data/lib/active_support/concern.rb +4 -2
  23. data/lib/active_support/concurrency/load_interlock_aware_monitor.rb +42 -3
  24. data/lib/active_support/concurrency/null_lock.rb +13 -0
  25. data/lib/active_support/configurable.rb +10 -0
  26. data/lib/active_support/core_ext/array/conversions.rb +2 -1
  27. data/lib/active_support/core_ext/array.rb +0 -1
  28. data/lib/active_support/core_ext/class/subclasses.rb +13 -10
  29. data/lib/active_support/core_ext/date/calculations.rb +15 -0
  30. data/lib/active_support/core_ext/date/conversions.rb +2 -1
  31. data/lib/active_support/core_ext/date.rb +0 -1
  32. data/lib/active_support/core_ext/date_and_time/calculations.rb +10 -0
  33. data/lib/active_support/core_ext/date_time/calculations.rb +4 -0
  34. data/lib/active_support/core_ext/date_time/conversions.rb +6 -2
  35. data/lib/active_support/core_ext/date_time.rb +0 -1
  36. data/lib/active_support/core_ext/digest/uuid.rb +1 -10
  37. data/lib/active_support/core_ext/enumerable.rb +8 -75
  38. data/lib/active_support/core_ext/erb/util.rb +196 -0
  39. data/lib/active_support/core_ext/hash/conversions.rb +1 -1
  40. data/lib/active_support/core_ext/hash/deep_merge.rb +22 -14
  41. data/lib/active_support/core_ext/hash/deep_transform_values.rb +3 -3
  42. data/lib/active_support/core_ext/hash/keys.rb +3 -3
  43. data/lib/active_support/core_ext/integer/inflections.rb +12 -12
  44. data/lib/active_support/core_ext/module/attribute_accessors.rb +6 -0
  45. data/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb +34 -16
  46. data/lib/active_support/core_ext/module/concerning.rb +6 -6
  47. data/lib/active_support/core_ext/module/delegation.rb +40 -11
  48. data/lib/active_support/core_ext/module/deprecation.rb +15 -12
  49. data/lib/active_support/core_ext/module/introspection.rb +0 -1
  50. data/lib/active_support/core_ext/numeric/bytes.rb +9 -0
  51. data/lib/active_support/core_ext/numeric/conversions.rb +2 -0
  52. data/lib/active_support/core_ext/numeric.rb +0 -1
  53. data/lib/active_support/core_ext/object/deep_dup.rb +16 -0
  54. data/lib/active_support/core_ext/object/duplicable.rb +5 -5
  55. data/lib/active_support/core_ext/object/inclusion.rb +13 -5
  56. data/lib/active_support/core_ext/object/instance_variables.rb +22 -12
  57. data/lib/active_support/core_ext/object/json.rb +11 -3
  58. data/lib/active_support/core_ext/object/to_query.rb +0 -2
  59. data/lib/active_support/core_ext/object/with.rb +44 -0
  60. data/lib/active_support/core_ext/object/with_options.rb +9 -9
  61. data/lib/active_support/core_ext/object.rb +1 -0
  62. data/lib/active_support/core_ext/pathname/blank.rb +16 -0
  63. data/lib/active_support/core_ext/pathname/existence.rb +2 -0
  64. data/lib/active_support/core_ext/pathname.rb +1 -0
  65. data/lib/active_support/core_ext/range/conversions.rb +28 -7
  66. data/lib/active_support/core_ext/range/overlap.rb +40 -0
  67. data/lib/active_support/core_ext/range.rb +1 -2
  68. data/lib/active_support/core_ext/securerandom.rb +24 -12
  69. data/lib/active_support/core_ext/string/filters.rb +20 -14
  70. data/lib/active_support/core_ext/string/indent.rb +1 -1
  71. data/lib/active_support/core_ext/string/inflections.rb +16 -9
  72. data/lib/active_support/core_ext/string/output_safety.rb +42 -174
  73. data/lib/active_support/core_ext/thread/backtrace/location.rb +12 -0
  74. data/lib/active_support/core_ext/time/calculations.rb +22 -2
  75. data/lib/active_support/core_ext/time/conversions.rb +2 -2
  76. data/lib/active_support/core_ext/time/zones.rb +7 -8
  77. data/lib/active_support/core_ext/time.rb +0 -1
  78. data/lib/active_support/current_attributes.rb +15 -6
  79. data/lib/active_support/deep_mergeable.rb +53 -0
  80. data/lib/active_support/dependencies/autoload.rb +17 -12
  81. data/lib/active_support/deprecation/behaviors.rb +65 -42
  82. data/lib/active_support/deprecation/constant_accessor.rb +5 -4
  83. data/lib/active_support/deprecation/deprecators.rb +104 -0
  84. data/lib/active_support/deprecation/disallowed.rb +6 -8
  85. data/lib/active_support/deprecation/instance_delegator.rb +31 -4
  86. data/lib/active_support/deprecation/method_wrappers.rb +6 -23
  87. data/lib/active_support/deprecation/proxy_wrappers.rb +37 -22
  88. data/lib/active_support/deprecation/reporting.rb +42 -25
  89. data/lib/active_support/deprecation.rb +32 -5
  90. data/lib/active_support/deprecator.rb +7 -0
  91. data/lib/active_support/descendants_tracker.rb +104 -132
  92. data/lib/active_support/duration/iso8601_serializer.rb +0 -2
  93. data/lib/active_support/duration.rb +2 -1
  94. data/lib/active_support/encrypted_configuration.rb +63 -11
  95. data/lib/active_support/encrypted_file.rb +16 -12
  96. data/lib/active_support/environment_inquirer.rb +22 -2
  97. data/lib/active_support/error_reporter/test_helper.rb +15 -0
  98. data/lib/active_support/error_reporter.rb +121 -35
  99. data/lib/active_support/evented_file_update_checker.rb +17 -2
  100. data/lib/active_support/execution_wrapper.rb +4 -4
  101. data/lib/active_support/file_update_checker.rb +4 -2
  102. data/lib/active_support/fork_tracker.rb +10 -2
  103. data/lib/active_support/gem_version.rb +4 -4
  104. data/lib/active_support/gzip.rb +2 -0
  105. data/lib/active_support/hash_with_indifferent_access.rb +35 -17
  106. data/lib/active_support/html_safe_translation.rb +12 -2
  107. data/lib/active_support/i18n.rb +1 -1
  108. data/lib/active_support/i18n_railtie.rb +20 -13
  109. data/lib/active_support/inflector/inflections.rb +2 -0
  110. data/lib/active_support/inflector/methods.rb +28 -18
  111. data/lib/active_support/inflector/transliterate.rb +3 -1
  112. data/lib/active_support/isolated_execution_state.rb +26 -22
  113. data/lib/active_support/json/decoding.rb +2 -1
  114. data/lib/active_support/json/encoding.rb +25 -43
  115. data/lib/active_support/key_generator.rb +9 -1
  116. data/lib/active_support/lazy_load_hooks.rb +7 -5
  117. data/lib/active_support/locale/en.yml +2 -0
  118. data/lib/active_support/log_subscriber.rb +84 -33
  119. data/lib/active_support/logger.rb +9 -60
  120. data/lib/active_support/logger_thread_safe_level.rb +10 -24
  121. data/lib/active_support/message_encryptor.rb +197 -53
  122. data/lib/active_support/message_encryptors.rb +141 -0
  123. data/lib/active_support/message_pack/cache_serializer.rb +23 -0
  124. data/lib/active_support/message_pack/extensions.rb +292 -0
  125. data/lib/active_support/message_pack/serializer.rb +63 -0
  126. data/lib/active_support/message_pack.rb +50 -0
  127. data/lib/active_support/message_verifier.rb +212 -93
  128. data/lib/active_support/message_verifiers.rb +135 -0
  129. data/lib/active_support/messages/codec.rb +65 -0
  130. data/lib/active_support/messages/metadata.rb +111 -45
  131. data/lib/active_support/messages/rotation_coordinator.rb +93 -0
  132. data/lib/active_support/messages/rotator.rb +34 -32
  133. data/lib/active_support/messages/serializer_with_fallback.rb +158 -0
  134. data/lib/active_support/multibyte/chars.rb +2 -0
  135. data/lib/active_support/multibyte/unicode.rb +9 -37
  136. data/lib/active_support/notifications/fanout.rb +245 -81
  137. data/lib/active_support/notifications/instrumenter.rb +77 -20
  138. data/lib/active_support/notifications.rb +3 -3
  139. data/lib/active_support/number_helper/number_converter.rb +14 -5
  140. data/lib/active_support/number_helper/number_to_currency_converter.rb +6 -6
  141. data/lib/active_support/number_helper/number_to_human_size_converter.rb +3 -3
  142. data/lib/active_support/number_helper/number_to_phone_converter.rb +1 -0
  143. data/lib/active_support/number_helper.rb +379 -317
  144. data/lib/active_support/ordered_hash.rb +3 -3
  145. data/lib/active_support/ordered_options.rb +14 -0
  146. data/lib/active_support/parameter_filter.rb +103 -84
  147. data/lib/active_support/proxy_object.rb +2 -0
  148. data/lib/active_support/railtie.rb +33 -21
  149. data/lib/active_support/reloader.rb +12 -4
  150. data/lib/active_support/rescuable.rb +2 -0
  151. data/lib/active_support/secure_compare_rotator.rb +16 -9
  152. data/lib/active_support/string_inquirer.rb +3 -1
  153. data/lib/active_support/subscriber.rb +9 -27
  154. data/lib/active_support/syntax_error_proxy.rb +70 -0
  155. data/lib/active_support/tagged_logging.rb +60 -24
  156. data/lib/active_support/test_case.rb +153 -6
  157. data/lib/active_support/testing/assertions.rb +26 -10
  158. data/lib/active_support/testing/autorun.rb +0 -2
  159. data/lib/active_support/testing/constant_stubbing.rb +32 -0
  160. data/lib/active_support/testing/deprecation.rb +25 -25
  161. data/lib/active_support/testing/error_reporter_assertions.rb +107 -0
  162. data/lib/active_support/testing/isolation.rb +29 -28
  163. data/lib/active_support/testing/method_call_assertions.rb +21 -8
  164. data/lib/active_support/testing/parallelize_executor.rb +8 -3
  165. data/lib/active_support/testing/stream.rb +1 -1
  166. data/lib/active_support/testing/strict_warnings.rb +39 -0
  167. data/lib/active_support/testing/time_helpers.rb +37 -15
  168. data/lib/active_support/time_with_zone.rb +8 -37
  169. data/lib/active_support/values/time_zone.rb +9 -7
  170. data/lib/active_support/version.rb +1 -1
  171. data/lib/active_support/xml_mini/jdom.rb +3 -10
  172. data/lib/active_support/xml_mini/nokogiri.rb +1 -1
  173. data/lib/active_support/xml_mini/nokogirisax.rb +1 -1
  174. data/lib/active_support/xml_mini/rexml.rb +1 -1
  175. data/lib/active_support/xml_mini.rb +2 -2
  176. data/lib/active_support.rb +14 -3
  177. metadata +103 -16
  178. data/lib/active_support/core_ext/array/deprecated_conversions.rb +0 -25
  179. data/lib/active_support/core_ext/date/deprecated_conversions.rb +0 -26
  180. data/lib/active_support/core_ext/date_time/deprecated_conversions.rb +0 -22
  181. data/lib/active_support/core_ext/numeric/deprecated_conversions.rb +0 -60
  182. data/lib/active_support/core_ext/range/deprecated_conversions.rb +0 -26
  183. data/lib/active_support/core_ext/range/include_time_with_zone.rb +0 -7
  184. data/lib/active_support/core_ext/range/overlaps.rb +0 -10
  185. data/lib/active_support/core_ext/time/deprecated_conversions.rb +0 -22
  186. data/lib/active_support/core_ext/uri.rb +0 -5
  187. data/lib/active_support/per_thread_registry.rb +0 -65
@@ -3,9 +3,12 @@
3
3
  require "active_support/core_ext/module/attribute_accessors"
4
4
  require "active_support/core_ext/class/attribute"
5
5
  require "active_support/subscriber"
6
+ require "active_support/deprecation/proxy_wrappers"
6
7
 
7
8
  module ActiveSupport
8
- # <tt>ActiveSupport::LogSubscriber</tt> is an object set to consume
9
+ # = Active Support Log \Subscriber
10
+ #
11
+ # +ActiveSupport::LogSubscriber+ is an object set to consume
9
12
  # ActiveSupport::Notifications with the sole purpose of logging them.
10
13
  # The log subscriber dispatches notifications to a registered object based
11
14
  # on its given namespace.
@@ -15,29 +18,23 @@ module ActiveSupport
15
18
  #
16
19
  # module ActiveRecord
17
20
  # class LogSubscriber < ActiveSupport::LogSubscriber
21
+ # attach_to :active_record
22
+ #
18
23
  # def sql(event)
19
24
  # info "#{event.payload[:name]} (#{event.duration}) #{event.payload[:sql]}"
20
25
  # end
21
26
  # end
22
27
  # end
23
28
  #
24
- # And it's finally registered as:
25
- #
26
- # ActiveRecord::LogSubscriber.attach_to :active_record
29
+ # ActiveRecord::LogSubscriber.logger must be set as well, but it is assigned
30
+ # automatically in a \Rails environment.
27
31
  #
28
- # Since we need to know all instance methods before attaching the log
29
- # subscriber, the line above should be called after your
30
- # <tt>ActiveRecord::LogSubscriber</tt> definition.
31
- #
32
- # A logger also needs to be set with <tt>ActiveRecord::LogSubscriber.logger=</tt>.
33
- # This is assigned automatically in a Rails environment.
34
- #
35
- # After configured, whenever a <tt>"sql.active_record"</tt> notification is published,
36
- # it will properly dispatch the event
37
- # (<tt>ActiveSupport::Notifications::Event</tt>) to the sql method.
32
+ # After configured, whenever a <tt>"sql.active_record"</tt> notification is
33
+ # published, it will properly dispatch the event
34
+ # (ActiveSupport::Notifications::Event) to the +sql+ method.
38
35
  #
39
36
  # Being an ActiveSupport::Notifications consumer,
40
- # <tt>ActiveSupport::LogSubscriber</tt> exposes a simple interface to check if
37
+ # +ActiveSupport::LogSubscriber+ exposes a simple interface to check if
41
38
  # instrumented code raises an exception. It is common to log a different
42
39
  # message in case of an error, and this can be achieved by extending
43
40
  # the previous example:
@@ -59,15 +56,24 @@ module ActiveSupport
59
56
  # end
60
57
  # end
61
58
  #
62
- # Log subscriber also has some helpers to deal with logging and automatically
63
- # flushes all logs when the request finishes
64
- # (via <tt>action_dispatch.callback</tt> notification) in a Rails environment.
59
+ # +ActiveSupport::LogSubscriber+ also has some helpers to deal with
60
+ # logging. For example, ActiveSupport::LogSubscriber.flush_all! will ensure
61
+ # that all logs are flushed, and it is called in Rails::Rack::Logger after a
62
+ # request finishes.
65
63
  class LogSubscriber < Subscriber
66
64
  # Embed in a String to clear all previous ANSI sequences.
67
- CLEAR = "\e[0m"
68
- BOLD = "\e[1m"
69
-
70
- # Colors
65
+ CLEAR = ActiveSupport::Deprecation::DeprecatedObjectProxy.new("\e[0m", "CLEAR is deprecated! Use MODES[:clear] instead.", ActiveSupport.deprecator)
66
+ BOLD = ActiveSupport::Deprecation::DeprecatedObjectProxy.new("\e[1m", "BOLD is deprecated! Use MODES[:bold] instead.", ActiveSupport.deprecator)
67
+
68
+ # ANSI sequence modes
69
+ MODES = {
70
+ clear: 0,
71
+ bold: 1,
72
+ italic: 3,
73
+ underline: 4,
74
+ }
75
+
76
+ # ANSI sequence colors
71
77
  BLACK = "\e[30m"
72
78
  RED = "\e[31m"
73
79
  GREEN = "\e[32m"
@@ -78,6 +84,13 @@ module ActiveSupport
78
84
  WHITE = "\e[37m"
79
85
 
80
86
  mattr_accessor :colorize_logging, default: true
87
+ class_attribute :log_levels, instance_accessor: false, default: {} # :nodoc:
88
+
89
+ LEVEL_CHECKS = {
90
+ debug: -> (logger) { !logger.debug? },
91
+ info: -> (logger) { !logger.info? },
92
+ error: -> (logger) { !logger.error? },
93
+ }
81
94
 
82
95
  class << self
83
96
  def logger
@@ -86,6 +99,12 @@ module ActiveSupport
86
99
  end
87
100
  end
88
101
 
102
+ def attach_to(...) # :nodoc:
103
+ result = super
104
+ set_event_levels
105
+ result
106
+ end
107
+
89
108
  attr_writer :logger
90
109
 
91
110
  def log_subscribers
@@ -101,20 +120,36 @@ module ActiveSupport
101
120
  def fetch_public_methods(subscriber, inherit_all)
102
121
  subscriber.public_methods(inherit_all) - LogSubscriber.public_instance_methods(true)
103
122
  end
123
+
124
+ def set_event_levels
125
+ if subscriber
126
+ subscriber.event_levels = log_levels.transform_keys { |k| "#{k}.#{namespace}" }
127
+ end
128
+ end
129
+
130
+ def subscribe_log_level(method, level)
131
+ self.log_levels = log_levels.merge(method => LEVEL_CHECKS.fetch(level))
132
+ set_event_levels
133
+ end
134
+ end
135
+
136
+ def initialize
137
+ super
138
+ @event_levels = {}
104
139
  end
105
140
 
106
141
  def logger
107
142
  LogSubscriber.logger
108
143
  end
109
144
 
110
- def start(name, id, payload)
111
- super if logger
145
+ def silenced?(event)
146
+ logger.nil? || @event_levels[event]&.call(logger)
112
147
  end
113
148
 
114
- def finish(name, id, payload)
149
+ def call(event)
115
150
  super if logger
116
151
  rescue => e
117
- log_exception(name, e)
152
+ log_exception(event.name, e)
118
153
  end
119
154
 
120
155
  def publish_event(event)
@@ -123,6 +158,8 @@ module ActiveSupport
123
158
  log_exception(event.name, e)
124
159
  end
125
160
 
161
+ attr_writer :event_levels # :nodoc:
162
+
126
163
  private
127
164
  %w(info debug warn error fatal unknown).each do |level|
128
165
  class_eval <<-METHOD, __FILE__, __LINE__ + 1
@@ -132,15 +169,29 @@ module ActiveSupport
132
169
  METHOD
133
170
  end
134
171
 
135
- # Set color by using a symbol or one of the defined constants. If a third
136
- # option is set to +true+, it also adds bold to the string. This is based
137
- # on the Highline implementation and will automatically append CLEAR to the
138
- # end of the returned String.
139
- def color(text, color, bold = false) # :doc:
172
+ # Set color by using a symbol or one of the defined constants. Set modes
173
+ # by specifying bold, italic, or underline options. Inspired by Highline,
174
+ # this method will automatically clear formatting at the end of the returned String.
175
+ def color(text, color, mode_options = {}) # :doc:
140
176
  return text unless colorize_logging
141
177
  color = self.class.const_get(color.upcase) if color.is_a?(Symbol)
142
- bold = bold ? BOLD : ""
143
- "#{bold}#{color}#{text}#{CLEAR}"
178
+ mode = mode_from(mode_options)
179
+ clear = "\e[#{MODES[:clear]}m"
180
+ "#{mode}#{color}#{text}#{clear}"
181
+ end
182
+
183
+ def mode_from(options)
184
+ if options.is_a?(TrueClass) || options.is_a?(FalseClass)
185
+ ActiveSupport.deprecator.warn(<<~MSG.squish)
186
+ Bolding log text with a positional boolean is deprecated and will be removed
187
+ in Rails 7.2. Use an option hash instead (eg. `color("my text", :red, bold: true)`).
188
+ MSG
189
+ options = { bold: options }
190
+ end
191
+
192
+ modes = MODES.values_at(*options.compact_blank.keys)
193
+
194
+ "\e[#{modes.join(";")}m" if modes.any?
144
195
  end
145
196
 
146
197
  def log_exception(name, e)
@@ -14,72 +14,21 @@ module ActiveSupport
14
14
  # ActiveSupport::Logger.logger_outputs_to?(logger, STDOUT)
15
15
  # # => true
16
16
  def self.logger_outputs_to?(logger, *sources)
17
- logdev = logger.instance_variable_get(:@logdev)
18
- logger_source = logdev.dev if logdev.respond_to?(:dev)
19
- sources.any? { |source| source == logger_source }
20
- end
21
-
22
- # Broadcasts logs to multiple loggers.
23
- def self.broadcast(logger) # :nodoc:
24
- Module.new do
25
- define_method(:add) do |*args, &block|
26
- logger.add(*args, &block)
27
- super(*args, &block)
28
- end
29
-
30
- define_method(:<<) do |x|
31
- logger << x
32
- super(x)
33
- end
34
-
35
- define_method(:close) do
36
- logger.close
37
- super()
38
- end
39
-
40
- define_method(:progname=) do |name|
41
- logger.progname = name
42
- super(name)
43
- end
44
-
45
- define_method(:formatter=) do |formatter|
46
- logger.formatter = formatter
47
- super(formatter)
48
- end
49
-
50
- define_method(:level=) do |level|
51
- logger.level = level
52
- super(level)
53
- end
17
+ loggers = if logger.is_a?(BroadcastLogger)
18
+ logger.broadcasts
19
+ else
20
+ [logger]
21
+ end
54
22
 
55
- define_method(:local_level=) do |level|
56
- logger.local_level = level if logger.respond_to?(:local_level=)
57
- super(level) if respond_to?(:local_level=)
58
- end
23
+ logdevs = loggers.map { |logger| logger.instance_variable_get(:@logdev) }
24
+ logger_sources = logdevs.filter_map { |logdev| logdev.dev if logdev.respond_to?(:dev) }
59
25
 
60
- define_method(:silence) do |level = Logger::ERROR, &block|
61
- if logger.respond_to?(:silence)
62
- logger.silence(level) do
63
- if defined?(super)
64
- super(level, &block)
65
- else
66
- block.call(self)
67
- end
68
- end
69
- else
70
- if defined?(super)
71
- super(level, &block)
72
- else
73
- block.call(self)
74
- end
75
- end
76
- end
77
- end
26
+ (sources & logger_sources).any?
78
27
  end
79
28
 
80
29
  def initialize(*args, **kwargs)
81
30
  super
82
- @formatter = SimpleFormatter.new
31
+ @formatter ||= SimpleFormatter.new
83
32
  end
84
33
 
85
34
  # Simple formatter which only displays the message.
@@ -1,9 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_support/concern"
4
- require "active_support/core_ext/module/attribute_accessors"
5
- require "concurrent"
6
- require "fiber"
4
+ require "logger"
7
5
 
8
6
  module ActiveSupport
9
7
  module LoggerThreadSafeLevel # :nodoc:
@@ -18,7 +16,7 @@ module ActiveSupport
18
16
  end
19
17
 
20
18
  def local_level
21
- IsolatedExecutionState[:logger_thread_safe_level]
19
+ IsolatedExecutionState[local_level_key]
22
20
  end
23
21
 
24
22
  def local_level=(level)
@@ -30,7 +28,11 @@ module ActiveSupport
30
28
  else
31
29
  raise ArgumentError, "Invalid log level: #{level.inspect}"
32
30
  end
33
- IsolatedExecutionState[:logger_thread_safe_level] = level
31
+ if level.nil?
32
+ IsolatedExecutionState.delete(local_level_key)
33
+ else
34
+ IsolatedExecutionState[local_level_key] = level
35
+ end
34
36
  end
35
37
 
36
38
  def level
@@ -45,25 +47,9 @@ module ActiveSupport
45
47
  self.local_level = old_local_level
46
48
  end
47
49
 
48
- # Redefined to check severity against #level, and thus the thread-local level, rather than +@level+.
49
- # FIXME: Remove when the minimum Ruby version supports overriding Logger#level.
50
- def add(severity, message = nil, progname = nil, &block) # :nodoc:
51
- severity ||= UNKNOWN
52
- progname ||= @progname
53
-
54
- return true if @logdev.nil? || severity < level
55
-
56
- if message.nil?
57
- if block_given?
58
- message = yield
59
- else
60
- message = progname
61
- progname = @progname
62
- end
50
+ private
51
+ def local_level_key
52
+ @local_level_key ||= :"logger_thread_safe_level_#{object_id}"
63
53
  end
64
-
65
- @logdev.write \
66
- format_message(format_severity(severity), Time.now, progname, message)
67
- end
68
54
  end
69
55
  end
@@ -3,10 +3,13 @@
3
3
  require "openssl"
4
4
  require "base64"
5
5
  require "active_support/core_ext/module/attribute_accessors"
6
+ require "active_support/messages/codec"
7
+ require "active_support/messages/rotator"
6
8
  require "active_support/message_verifier"
7
- require "active_support/messages/metadata"
8
9
 
9
10
  module ActiveSupport
11
+ # = Active Support Message Encryptor
12
+ #
10
13
  # MessageEncryptor is a simple way to encrypt values which get stored
11
14
  # somewhere you don't trust.
12
15
  #
@@ -24,7 +27,7 @@ module ActiveSupport
24
27
  # crypt.decrypt_and_verify(encrypted_data) # => "my secret data"
25
28
  #
26
29
  # The +decrypt_and_verify+ method will raise an
27
- # <tt>ActiveSupport::MessageEncryptor::InvalidMessage</tt> exception if the data
30
+ # +ActiveSupport::MessageEncryptor::InvalidMessage+ exception if the data
28
31
  # provided cannot be decrypted or verified.
29
32
  #
30
33
  # crypt.decrypt_and_verify('not encrypted data') # => ActiveSupport::MessageEncryptor::InvalidMessage
@@ -84,8 +87,8 @@ module ActiveSupport
84
87
  # the above should be combined into:
85
88
  #
86
89
  # crypt.rotate old_secret, cipher: "aes-256-cbc"
87
- class MessageEncryptor
88
- prepend Messages::Rotator::Encryptor
90
+ class MessageEncryptor < Messages::Codec
91
+ prepend Messages::Rotator
89
92
 
90
93
  cattr_accessor :use_authenticated_message_encryption, instance_accessor: false, default: false
91
94
 
@@ -109,55 +112,140 @@ module ActiveSupport
109
112
  end
110
113
  end
111
114
 
112
- module NullVerifier # :nodoc:
113
- def self.verify(value)
114
- value
115
- end
116
-
117
- def self.generate(value)
118
- value
119
- end
120
- end
121
-
122
115
  class InvalidMessage < StandardError; end
123
116
  OpenSSLCipherError = OpenSSL::Cipher::CipherError
124
117
 
118
+ AUTH_TAG_LENGTH = 16 # :nodoc:
119
+ SEPARATOR = "--" # :nodoc:
120
+
125
121
  # Initialize a new MessageEncryptor. +secret+ must be at least as long as
126
122
  # the cipher key size. For the default 'aes-256-gcm' cipher, this is 256
127
123
  # bits. If you are using a user-entered secret, you can generate a suitable
128
124
  # key by using ActiveSupport::KeyGenerator or a similar key
129
125
  # derivation function.
130
126
  #
131
- # First additional parameter is used as the signature key for MessageVerifier.
132
- # This allows you to specify keys to encrypt and sign data.
127
+ # The first additional parameter is used as the signature key for
128
+ # MessageVerifier. This allows you to specify keys to encrypt and sign
129
+ # data. Ignored when using an AEAD cipher like 'aes-256-gcm'.
133
130
  #
134
131
  # ActiveSupport::MessageEncryptor.new('secret', 'signature_secret')
135
132
  #
136
- # Options:
137
- # * <tt>:cipher</tt> - Cipher to use. Can be any cipher returned by
138
- # <tt>OpenSSL::Cipher.ciphers</tt>. Default is 'aes-256-gcm'.
139
- # * <tt>:digest</tt> - String of digest to use for signing. Default is
140
- # +SHA1+. Ignored when using an AEAD cipher like 'aes-256-gcm'.
141
- # * <tt>:serializer</tt> - Object serializer to use. Default is +Marshal+.
142
- def initialize(secret, sign_secret = nil, cipher: nil, digest: nil, serializer: nil)
133
+ # ==== Options
134
+ #
135
+ # [+:cipher+]
136
+ # Cipher to use. Can be any cipher returned by +OpenSSL::Cipher.ciphers+.
137
+ # Default is 'aes-256-gcm'.
138
+ #
139
+ # [+:digest+]
140
+ # Digest used for signing. Ignored when using an AEAD cipher like
141
+ # 'aes-256-gcm'.
142
+ #
143
+ # [+:serializer+]
144
+ # The serializer used to serialize message data. You can specify any
145
+ # object that responds to +dump+ and +load+, or you can choose from
146
+ # several preconfigured serializers: +:marshal+, +:json_allow_marshal+,
147
+ # +:json+, +:message_pack_allow_marshal+, +:message_pack+.
148
+ #
149
+ # The preconfigured serializers include a fallback mechanism to support
150
+ # multiple deserialization formats. For example, the +:marshal+ serializer
151
+ # will serialize using +Marshal+, but can deserialize using +Marshal+,
152
+ # ActiveSupport::JSON, or ActiveSupport::MessagePack. This makes it easy
153
+ # to migrate between serializers.
154
+ #
155
+ # The +:marshal+, +:json_allow_marshal+, and +:message_pack_allow_marshal+
156
+ # serializers support deserializing using +Marshal+, but the others do
157
+ # not. Beware that +Marshal+ is a potential vector for deserialization
158
+ # attacks in cases where a message signing secret has been leaked. <em>If
159
+ # possible, choose a serializer that does not support +Marshal+.</em>
160
+ #
161
+ # The +:message_pack+ and +:message_pack_allow_marshal+ serializers use
162
+ # ActiveSupport::MessagePack, which can roundtrip some Ruby types that are
163
+ # not supported by JSON, and may provide improved performance. However,
164
+ # these require the +msgpack+ gem.
165
+ #
166
+ # When using \Rails, the default depends on +config.active_support.message_serializer+.
167
+ # Otherwise, the default is +:marshal+.
168
+ #
169
+ # [+:url_safe+]
170
+ # By default, MessageEncryptor generates RFC 4648 compliant strings
171
+ # which are not URL-safe. In other words, they can contain "+" and "/".
172
+ # If you want to generate URL-safe strings (in compliance with "Base 64
173
+ # Encoding with URL and Filename Safe Alphabet" in RFC 4648), you can
174
+ # pass +true+.
175
+ #
176
+ # [+:force_legacy_metadata_serializer+]
177
+ # Whether to use the legacy metadata serializer, which serializes the
178
+ # message first, then wraps it in an envelope which is also serialized. This
179
+ # was the default in \Rails 7.0 and below.
180
+ #
181
+ # If you don't pass a truthy value, the default is set using
182
+ # +config.active_support.use_message_serializer_for_metadata+.
183
+ def initialize(secret, sign_secret = nil, **options)
184
+ super(**options)
143
185
  @secret = secret
144
- @sign_secret = sign_secret
145
- @cipher = cipher || self.class.default_cipher
146
- @digest = digest || "SHA1" unless aead_mode?
147
- @verifier = resolve_verifier
148
- @serializer = serializer || Marshal
186
+ @cipher = options[:cipher] || self.class.default_cipher
187
+ @aead_mode = new_cipher.authenticated?
188
+ @verifier = if !@aead_mode
189
+ MessageVerifier.new(sign_secret || secret, **options, serializer: NullSerializer)
190
+ end
149
191
  end
150
192
 
151
193
  # Encrypt and sign a message. We need to sign the message in order to avoid
152
194
  # padding attacks. Reference: https://www.limited-entropy.com/padding-oracle-attacks/.
153
- def encrypt_and_sign(value, expires_at: nil, expires_in: nil, purpose: nil)
154
- verifier.generate(_encrypt(value, expires_at: expires_at, expires_in: expires_in, purpose: purpose))
195
+ #
196
+ # ==== Options
197
+ #
198
+ # [+:expires_at+]
199
+ # The datetime at which the message expires. After this datetime,
200
+ # verification of the message will fail.
201
+ #
202
+ # message = encryptor.encrypt_and_sign("hello", expires_at: Time.now.tomorrow)
203
+ # encryptor.decrypt_and_verify(message) # => "hello"
204
+ # # 24 hours later...
205
+ # encryptor.decrypt_and_verify(message) # => nil
206
+ #
207
+ # [+:expires_in+]
208
+ # The duration for which the message is valid. After this duration has
209
+ # elapsed, verification of the message will fail.
210
+ #
211
+ # message = encryptor.encrypt_and_sign("hello", expires_in: 24.hours)
212
+ # encryptor.decrypt_and_verify(message) # => "hello"
213
+ # # 24 hours later...
214
+ # encryptor.decrypt_and_verify(message) # => nil
215
+ #
216
+ # [+:purpose+]
217
+ # The purpose of the message. If specified, the same purpose must be
218
+ # specified when verifying the message; otherwise, verification will fail.
219
+ # (See #decrypt_and_verify.)
220
+ def encrypt_and_sign(value, **options)
221
+ create_message(value, **options)
155
222
  end
156
223
 
157
224
  # Decrypt and verify a message. We need to verify the message in order to
158
225
  # avoid padding attacks. Reference: https://www.limited-entropy.com/padding-oracle-attacks/.
159
- def decrypt_and_verify(data, purpose: nil, **)
160
- _decrypt(verifier.verify(data), purpose)
226
+ #
227
+ # ==== Options
228
+ #
229
+ # [+:purpose+]
230
+ # The purpose that the message was generated with. If the purpose does not
231
+ # match, +decrypt_and_verify+ will return +nil+.
232
+ #
233
+ # message = encryptor.encrypt_and_sign("hello", purpose: "greeting")
234
+ # encryptor.decrypt_and_verify(message, purpose: "greeting") # => "hello"
235
+ # encryptor.decrypt_and_verify(message) # => nil
236
+ #
237
+ # message = encryptor.encrypt_and_sign("bye")
238
+ # encryptor.decrypt_and_verify(message) # => "bye"
239
+ # encryptor.decrypt_and_verify(message, purpose: "greeting") # => nil
240
+ #
241
+ def decrypt_and_verify(message, **options)
242
+ catch_and_raise :invalid_message_format, as: InvalidMessage do
243
+ catch_and_raise :invalid_message_serialization, as: InvalidMessage do
244
+ catch_and_ignore :invalid_message_content do
245
+ read_message(message, **options)
246
+ end
247
+ end
248
+ end
161
249
  end
162
250
 
163
251
  # Given a cipher, returns the key length of the cipher to help generate the key of desired size
@@ -165,8 +253,28 @@ module ActiveSupport
165
253
  OpenSSL::Cipher.new(cipher).key_len
166
254
  end
167
255
 
256
+ def create_message(value, **options) # :nodoc:
257
+ sign(encrypt(serialize_with_metadata(value, **options)))
258
+ end
259
+
260
+ def read_message(message, **options) # :nodoc:
261
+ deserialize_with_metadata(decrypt(verify(message)), **options)
262
+ end
263
+
264
+ def inspect # :nodoc:
265
+ "#<#{self.class.name}:#{'%#016x' % (object_id << 1)}>"
266
+ end
267
+
168
268
  private
169
- def _encrypt(value, **metadata_options)
269
+ def sign(data)
270
+ @verifier ? @verifier.create_message(data) : data
271
+ end
272
+
273
+ def verify(data)
274
+ @verifier ? @verifier.read_message(data) : data
275
+ end
276
+
277
+ def encrypt(data)
170
278
  cipher = new_cipher
171
279
  cipher.encrypt
172
280
  cipher.key = @secret
@@ -175,22 +283,25 @@ module ActiveSupport
175
283
  iv = cipher.random_iv
176
284
  cipher.auth_data = "" if aead_mode?
177
285
 
178
- encrypted_data = cipher.update(Messages::Metadata.wrap(@serializer.dump(value), **metadata_options))
286
+ encrypted_data = cipher.update(data)
179
287
  encrypted_data << cipher.final
180
288
 
181
- blob = "#{::Base64.strict_encode64 encrypted_data}--#{::Base64.strict_encode64 iv}"
182
- blob = "#{blob}--#{::Base64.strict_encode64 cipher.auth_tag}" if aead_mode?
183
- blob
289
+ parts = [encrypted_data, iv]
290
+ parts << cipher.auth_tag(AUTH_TAG_LENGTH) if aead_mode?
291
+
292
+ join_parts(parts)
184
293
  end
185
294
 
186
- def _decrypt(encrypted_message, purpose)
295
+ def decrypt(encrypted_message)
187
296
  cipher = new_cipher
188
- encrypted_data, iv, auth_tag = encrypted_message.split("--").map { |v| ::Base64.strict_decode64(v) }
297
+ encrypted_data, iv, auth_tag = extract_parts(encrypted_message)
189
298
 
190
299
  # Currently the OpenSSL bindings do not raise an error if auth_tag is
191
300
  # truncated, which would allow an attacker to easily forge it. See
192
301
  # https://github.com/ruby/openssl/issues/63
193
- raise InvalidMessage if aead_mode? && (auth_tag.nil? || auth_tag.bytes.length != 16)
302
+ if aead_mode? && auth_tag.bytesize != AUTH_TAG_LENGTH
303
+ throw :invalid_message_format, "truncated auth_tag"
304
+ end
194
305
 
195
306
  cipher.decrypt
196
307
  cipher.key = @secret
@@ -202,29 +313,62 @@ module ActiveSupport
202
313
 
203
314
  decrypted_data = cipher.update(encrypted_data)
204
315
  decrypted_data << cipher.final
316
+ rescue OpenSSLCipherError => error
317
+ throw :invalid_message_format, error
318
+ end
205
319
 
206
- message = Messages::Metadata.verify(decrypted_data, purpose)
207
- @serializer.load(message) if message
208
- rescue OpenSSLCipherError, TypeError, ArgumentError
209
- raise InvalidMessage
320
+ def length_after_encode(length_before_encode)
321
+ if @url_safe
322
+ (4 * length_before_encode / 3.0).ceil # length without padding
323
+ else
324
+ 4 * (length_before_encode / 3.0).ceil # length with padding
325
+ end
210
326
  end
211
327
 
212
- def new_cipher
213
- OpenSSL::Cipher.new(@cipher)
328
+ def length_of_encoded_iv
329
+ @length_of_encoded_iv ||= length_after_encode(new_cipher.iv_len)
214
330
  end
215
331
 
216
- attr_reader :verifier
332
+ def length_of_encoded_auth_tag
333
+ @length_of_encoded_auth_tag ||= length_after_encode(AUTH_TAG_LENGTH)
334
+ end
217
335
 
218
- def aead_mode?
219
- @aead_mode ||= new_cipher.authenticated?
336
+ def join_parts(parts)
337
+ parts.map! { |part| encode(part) }.join(SEPARATOR)
220
338
  end
221
339
 
222
- def resolve_verifier
223
- if aead_mode?
224
- NullVerifier
340
+ def extract_part(encrypted_message, rindex, length)
341
+ index = rindex - length
342
+
343
+ if encrypted_message[index - SEPARATOR.length, SEPARATOR.length] == SEPARATOR
344
+ encrypted_message[index, length]
225
345
  else
226
- MessageVerifier.new(@sign_secret || @secret, digest: @digest, serializer: NullSerializer)
346
+ throw :invalid_message_format, "missing separator"
347
+ end
348
+ end
349
+
350
+ def extract_parts(encrypted_message)
351
+ parts = []
352
+ rindex = encrypted_message.length
353
+
354
+ if aead_mode?
355
+ parts << extract_part(encrypted_message, rindex, length_of_encoded_auth_tag)
356
+ rindex -= SEPARATOR.length + length_of_encoded_auth_tag
227
357
  end
358
+
359
+ parts << extract_part(encrypted_message, rindex, length_of_encoded_iv)
360
+ rindex -= SEPARATOR.length + length_of_encoded_iv
361
+
362
+ parts << encrypted_message[0, rindex]
363
+
364
+ parts.reverse!.map! { |part| decode(part) }
365
+ end
366
+
367
+ def new_cipher
368
+ OpenSSL::Cipher.new(@cipher)
228
369
  end
370
+
371
+ attr_reader :aead_mode
372
+ alias :aead_mode? :aead_mode
229
373
  end
230
374
  end