paper_trail 12.2.0 → 15.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d64732632c8d2b579619cb3e4ec4f754a3bd82310f464aa715d79709fc0ba58d
4
- data.tar.gz: 245ca1378370715e907ef2c7c9eaad6375a77aef0306729e98c2a046d4cb7c89
3
+ metadata.gz: 3a343a4be31774100a8713280696dcfad46cd119b04de6884de7b6a1b34acd13
4
+ data.tar.gz: 120dfe293f08004ee33bb09a0db23d3004ea2dadfaff56e8992283564458df28
5
5
  SHA512:
6
- metadata.gz: 85ff44a62d2ca043e38805e510ac7bea9b5b2faba00e3e75b79262404b422730a2810e0e95adc0a30e867dd8d20fa21eb63d0b329908934272a591a14df0ce03
7
- data.tar.gz: 8454773dd1a683fed0f79f6464a4d2f9944c399b81e4ce3ec205720a27db17ee4daade6793cddf5fe63e8422c3f6564893721ad02bef6abbd42d8e6e8a881b08
6
+ metadata.gz: 8401d4f39db71320590dab474b8782a4f52843394869d12aa3cb5270d25170df8435af7a1c97cdcb3b1acbd8841eacdfd0c905c681b6e1173b2e6c0a69e37af9
7
+ data.tar.gz: 0ec02b4a9ab72e45741ba79c8c4b09c524a02dc01de35b7763322eea5612112ae0be36e4a6dcf50c297b8bf8f0c5b96d78719d2f7839352b8d57c383d6fd250a
@@ -20,6 +20,12 @@ module PaperTrail
20
20
  default: false,
21
21
  desc: "Store changeset (diff) with each version"
22
22
  )
23
+ class_option(
24
+ :uuid,
25
+ type: :boolean,
26
+ default: false,
27
+ desc: "Use uuid instead of bigint for item_id type (use only if tables use UUIDs)"
28
+ )
23
29
 
24
30
  desc "Generates (but does not run) a migration to add a versions table." \
25
31
  " See section 5.c. Generators in README.md for more information."
@@ -28,7 +34,9 @@ module PaperTrail
28
34
  add_paper_trail_migration(
29
35
  "create_versions",
30
36
  item_type_options: item_type_options,
31
- versions_table_options: versions_table_options
37
+ versions_table_options: versions_table_options,
38
+ item_id_type_options: item_id_type_options,
39
+ version_table_primary_key_type: version_table_primary_key_type
32
40
  )
33
41
  if options.with_changes?
34
42
  add_paper_trail_migration("add_object_changes_to_versions")
@@ -37,13 +45,27 @@ module PaperTrail
37
45
 
38
46
  private
39
47
 
48
+ # To use uuid instead of integer for primary key
49
+ def item_id_type_options
50
+ options.uuid? ? "string" : "bigint"
51
+ end
52
+
53
+ # To use uuid for version table primary key
54
+ def version_table_primary_key_type
55
+ if options.uuid?
56
+ ", id: :uuid"
57
+ else
58
+ ""
59
+ end
60
+ end
61
+
40
62
  # MySQL 5.6 utf8mb4 limit is 191 chars for keys used in indexes.
41
63
  # See https://github.com/paper-trail-gem/paper_trail/issues/651
42
64
  def item_type_options
43
65
  if mysql?
44
- ", { null: false, limit: 191 }"
66
+ ", null: false, limit: 191"
45
67
  else
46
- ", { null: false }"
68
+ ", null: false"
47
69
  end
48
70
  end
49
71
 
@@ -9,9 +9,9 @@ class CreateVersions < ActiveRecord::Migration<%= migration_version %>
9
9
  TEXT_BYTES = 1_073_741_823
10
10
 
11
11
  def change
12
- create_table :versions<%= versions_table_options %> do |t|
12
+ create_table :versions<%= versions_table_options %><%= version_table_primary_key_type %> do |t|
13
13
  t.string :item_type<%= item_type_options %>
14
- t.bigint :item_id, null: false
14
+ t.<%= item_id_type_options %> :item_id, null: false
15
15
  t.string :event, null: false
16
16
  t.string :whodunnit
17
17
  t.text :object, limit: TEXT_BYTES
@@ -28,11 +28,11 @@ class CreateVersions < ActiveRecord::Migration<%= migration_version %>
28
28
  # MySQL users should also upgrade to at least rails 4.2, which is the first
29
29
  # version of ActiveRecord with support for fractional seconds in MySQL.
30
30
  # (https://github.com/rails/rails/pull/14359)
31
- #
31
+ #
32
32
  # MySQL users should use the following line for `created_at`
33
- # t.datetime :created_at, limit: 6
33
+ # t.datetime :created_at, limit: 6
34
34
  t.datetime :created_at
35
35
  end
36
- add_index :versions, %i(item_type item_id)
36
+ add_index :versions, %i[item_type item_id]
37
37
  end
38
38
  end
@@ -32,7 +32,7 @@ module PaperTrail
32
32
  if defined_enums[attr] && val.is_a?(::String)
33
33
  # Because PT 4 used to save the string version of enums to `object_changes`
34
34
  val
35
- elsif PaperTrail::RAILS_GTE_7_0 && val.is_a?(ActiveRecord::Type::Time::Value)
35
+ elsif PaperTrail.active_record_gte_7_0? && val.is_a?(ActiveRecord::Type::Time::Value)
36
36
  # Because Rails 7 time attribute throws a delegation error when you deserialize
37
37
  # it with the factory.
38
38
  # See ActiveRecord::Type::Time::Value crashes when loaded from YAML on rails 7.0
@@ -8,6 +8,12 @@ module PaperTrail
8
8
  class ObjectAttribute
9
9
  def initialize(model_class)
10
10
  @model_class = model_class
11
+
12
+ # ActiveRecord since 7.0 has a built-in encryption mechanism
13
+ @encrypted_attributes =
14
+ if PaperTrail.active_record_gte_7_0?
15
+ @model_class.encrypted_attributes&.map(&:to_s)
16
+ end
11
17
  end
12
18
 
13
19
  def serialize(attributes)
@@ -23,14 +29,18 @@ module PaperTrail
23
29
  # Modifies `attributes` in place.
24
30
  # TODO: Return a new hash instead.
25
31
  def alter(attributes, serialization_method)
26
- # Don't serialize before values before inserting into columns of type
32
+ # Don't serialize non-encrypted before values before inserting into columns of type
27
33
  # `JSON` on `PostgreSQL` databases.
28
- return attributes if object_col_is_json?
34
+ attributes_to_serialize =
35
+ object_col_is_json? ? attributes.slice(*@encrypted_attributes) : attributes
36
+ return attributes if attributes_to_serialize.blank?
29
37
 
30
38
  serializer = CastAttributeSerializer.new(@model_class)
31
- attributes.each do |key, value|
39
+ attributes_to_serialize.each do |key, value|
32
40
  attributes[key] = serializer.send(serialization_method, key, value)
33
41
  end
42
+
43
+ attributes
34
44
  end
35
45
 
36
46
  def object_col_is_json?
@@ -8,6 +8,12 @@ module PaperTrail
8
8
  class ObjectChangesAttribute
9
9
  def initialize(item_class)
10
10
  @item_class = item_class
11
+
12
+ # ActiveRecord since 7.0 has a built-in encryption mechanism
13
+ @encrypted_attributes =
14
+ if PaperTrail.active_record_gte_7_0?
15
+ @item_class.encrypted_attributes&.map(&:to_s)
16
+ end
11
17
  end
12
18
 
13
19
  def serialize(changes)
@@ -23,17 +29,21 @@ module PaperTrail
23
29
  # Modifies `changes` in place.
24
30
  # TODO: Return a new hash instead.
25
31
  def alter(changes, serialization_method)
26
- # Don't serialize before values before inserting into columns of type
32
+ # Don't serialize non-encrypted before values before inserting into columns of type
27
33
  # `JSON` on `PostgreSQL` databases.
28
- return changes if object_changes_col_is_json?
34
+ changes_to_serialize =
35
+ object_changes_col_is_json? ? changes.slice(*@encrypted_attributes) : changes.clone
36
+ return changes if changes_to_serialize.blank?
29
37
 
30
38
  serializer = CastAttributeSerializer.new(@item_class)
31
- changes.clone.each do |key, change|
39
+ changes_to_serialize.each do |key, change|
32
40
  # `change` is an Array with two elements, representing before and after.
33
41
  changes[key] = Array(change).map do |value|
34
42
  serializer.send(serialization_method, key, value)
35
43
  end
36
44
  end
45
+
46
+ changes
37
47
  end
38
48
 
39
49
  def object_changes_col_is_json?
@@ -17,7 +17,7 @@ module PaperTrail
17
17
  # newer rails versions. Most PT users should avoid incompatible rails
18
18
  # versions.
19
19
  module Compatibility
20
- ACTIVERECORD_GTE = ">= 5.2" # enforced in gemspec
20
+ ACTIVERECORD_GTE = ">= 6.1" # enforced in gemspec
21
21
  ACTIVERECORD_LT = "< 7.1" # not enforced in gemspec
22
22
 
23
23
  E_INCOMPATIBLE_AR = <<-EOS
@@ -22,6 +22,19 @@ module PaperTrail
22
22
  #
23
23
  # @api private
24
24
  class Base
25
+ E_FORBIDDEN_METADATA_KEY = <<-EOS.squish
26
+ Forbidden metadata key: %s. As of PT 14, the following metadata keys are
27
+ forbidden: %s
28
+ EOS
29
+ FORBIDDEN_METADATA_KEYS = %i[
30
+ created_at
31
+ id
32
+ item_id
33
+ item_subtype
34
+ item_type
35
+ updated_at
36
+ ].freeze
37
+
25
38
  # @api private
26
39
  def initialize(record, in_after_callback)
27
40
  @record = record
@@ -44,6 +57,13 @@ module PaperTrail
44
57
 
45
58
  private
46
59
 
60
+ # @api private
61
+ def assert_metadatum_key_is_permitted(key)
62
+ return unless FORBIDDEN_METADATA_KEYS.include?(key.to_sym)
63
+ raise PaperTrail::InvalidOption,
64
+ format(E_FORBIDDEN_METADATA_KEY, key, FORBIDDEN_METADATA_KEYS)
65
+ end
66
+
47
67
  # Rails 5.1 changed the API of `ActiveRecord::Dirty`. See
48
68
  # https://github.com/paper-trail-gem/paper_trail/pull/899
49
69
  #
@@ -175,7 +195,9 @@ module PaperTrail
175
195
  #
176
196
  # @api private
177
197
  def merge_metadata_from_controller_into(data)
178
- data.merge(PaperTrail.request.controller_info || {})
198
+ metadata = PaperTrail.request.controller_info || {}
199
+ metadata.keys.each { |k| assert_metadatum_key_is_permitted(k) }
200
+ data.merge(metadata)
179
201
  end
180
202
 
181
203
  # Updates `data` from the model's `meta` option.
@@ -183,6 +205,7 @@ module PaperTrail
183
205
  # @api private
184
206
  def merge_metadata_from_model_into(data)
185
207
  @record.paper_trail_options[:meta].each do |k, v|
208
+ assert_metadatum_key_is_permitted(k)
186
209
  data[k] = model_metadatum(v, data[:event])
187
210
  end
188
211
  end
@@ -221,7 +244,7 @@ module PaperTrail
221
244
  # @api private
222
245
  def notable_changes
223
246
  changes_in_latest_version.delete_if { |k, _v|
224
- !notably_changed.include?(k)
247
+ notably_changed.exclude?(k)
225
248
  }
226
249
  end
227
250
 
@@ -29,9 +29,6 @@ module PaperTrail
29
29
  event: @record.paper_trail_event || "update",
30
30
  whodunnit: PaperTrail.request.whodunnit
31
31
  }
32
- if @record.respond_to?(:updated_at)
33
- data[:created_at] = @record.updated_at
34
- end
35
32
  if record_object?
36
33
  data[:object] = recordable_object(@is_touch)
37
34
  end
@@ -40,6 +37,20 @@ module PaperTrail
40
37
  merge_metadata_into(data)
41
38
  end
42
39
 
40
+ # If it is a touch event, and changed are empty, it is assumed to be
41
+ # implicit `touch` mutation, and will a version is created.
42
+ #
43
+ # See https://github.com/rails/rails/commit/dcb825902d79d0f6baba956f7c6ec5767611353e
44
+ #
45
+ # @api private
46
+ def changed_notably?
47
+ if @is_touch && changes_in_latest_version.empty?
48
+ true
49
+ else
50
+ super
51
+ end
52
+ end
53
+
43
54
  private
44
55
 
45
56
  # @api private
@@ -53,6 +53,10 @@ module PaperTrail
53
53
  # - A Hash - options passed to `has_many`, plus `name:` and `scope:`.
54
54
  # - :version - The name to use for the method which returns the version
55
55
  # the instance was reified from. Default is `:version`.
56
+ # - :synchronize_version_creation_timestamp - By default, paper trail
57
+ # sets the `created_at` field for a new Version equal to the `updated_at`
58
+ # column of the model being updated. If you instead want `created_at` to
59
+ # populate with the current timestamp, set this option to `false`.
56
60
  #
57
61
  # Plugins like the experimental `paper_trail-association_tracking` gem
58
62
  # may accept additional options.
@@ -82,8 +82,9 @@ module PaperTrail
82
82
 
83
83
  # Adds a callback that records a version after a "touch" event.
84
84
  #
85
- # Rails < 6.0 has a bug where dirty-tracking does not occur during
86
- # a `touch`. (https://github.com/rails/rails/issues/33429) See also:
85
+ # Rails < 6.0 (no longer supported by PT) had a bug where dirty-tracking
86
+ # did not occur during a `touch`.
87
+ # (https://github.com/rails/rails/issues/33429) See also:
87
88
  # https://github.com/paper-trail-gem/paper_trail/issues/1121
88
89
  # https://github.com/paper-trail-gem/paper_trail/issues/1161
89
90
  # https://github.com/paper-trail-gem/paper_trail/pull/1285
@@ -93,7 +94,7 @@ module PaperTrail
93
94
  @model_class.after_touch { |r|
94
95
  if r.paper_trail.save_version?
95
96
  r.paper_trail.record_update(
96
- force: RAILS_LT_6_0,
97
+ force: false,
97
98
  in_after_callback: true,
98
99
  is_touch: true
99
100
  )
@@ -121,9 +122,6 @@ module PaperTrail
121
122
 
122
123
  private
123
124
 
124
- RAILS_LT_6_0 = ::ActiveRecord.gem_version < ::Gem::Version.new("6.0.0")
125
- private_constant :RAILS_LT_6_0
126
-
127
125
  # @api private
128
126
  def append_option_uniquely(option, value)
129
127
  collection = @model_class.paper_trail_options.fetch(option)
@@ -15,7 +15,7 @@ module PaperTrail
15
15
  @version_model_class = version_model_class
16
16
 
17
17
  # Currently, this `deep_dup` is necessary because the `jsonb` branch
18
- # modifies `@attributes`, and that would be a nasty suprise for
18
+ # modifies `@attributes`, and that would be a nasty surprise for
19
19
  # consumers of this class.
20
20
  # TODO: Stop modifying `@attributes`, then remove `deep_dup`.
21
21
  @attributes = attributes.deep_dup
@@ -25,14 +25,6 @@ module PaperTrail
25
25
  @record.send("#{@record.class.version_association_name}=", nil)
26
26
  end
27
27
 
28
- # Is PT enabled for this particular record?
29
- # @api private
30
- def enabled?
31
- PaperTrail.enabled? &&
32
- PaperTrail.request.enabled? &&
33
- PaperTrail.request.enabled_for_model?(@record.class)
34
- end
35
-
36
28
  # Returns true if this instance is the current, live one;
37
29
  # returns false if this instance came from a previous version.
38
30
  def live?
@@ -75,13 +67,6 @@ module PaperTrail
75
67
  end
76
68
  end
77
69
 
78
- # PT-AT extends this method to add its transaction id.
79
- #
80
- # @api private
81
- def data_for_create
82
- {}
83
- end
84
-
85
70
  # `recording_order` is "after" or "before". See ModelConfig#on_destroy.
86
71
  #
87
72
  # @api private
@@ -105,14 +90,12 @@ module PaperTrail
105
90
  end
106
91
  end
107
92
 
108
- # PT-AT extends this method to add its transaction id.
109
- #
110
- # @api private
111
- def data_for_destroy
112
- {}
113
- end
114
-
115
93
  # @api private
94
+ # @param force [boolean] Insert a `Version` even if `@record` has not
95
+ # `changed_notably?`.
96
+ # @param in_after_callback [boolean] True when called from an `after_update`
97
+ # or `after_touch` callback.
98
+ # @param is_touch [boolean] True when called from an `after_touch` callback.
116
99
  # @return - The created version object, so that plugins can use it, e.g.
117
100
  # paper_trail-association_tracking
118
101
  def record_update(force:, in_after_callback:, is_touch:)
@@ -136,40 +119,6 @@ module PaperTrail
136
119
  end
137
120
  end
138
121
 
139
- # PT-AT extends this method to add its transaction id.
140
- #
141
- # @api private
142
- def data_for_update
143
- {}
144
- end
145
-
146
- # @api private
147
- # @return - The created version object, so that plugins can use it, e.g.
148
- # paper_trail-association_tracking
149
- def record_update_columns(changes)
150
- return unless enabled?
151
- event = Events::Update.new(@record, false, false, changes)
152
-
153
- # Merge data from `Event` with data from PT-AT. We no longer use
154
- # `data_for_update_columns` but PT-AT still does.
155
- data = event.data.merge(data_for_update_columns)
156
-
157
- versions_assoc = @record.send(@record.class.versions_association_name)
158
- version = versions_assoc.create(data)
159
- if version.errors.any?
160
- log_version_errors(version, :update)
161
- else
162
- version
163
- end
164
- end
165
-
166
- # PT-AT extends this method to add its transaction id.
167
- #
168
- # @api private
169
- def data_for_update_columns
170
- {}
171
- end
172
-
173
122
  # Invoked via callback when a user attempts to persist a reified
174
123
  # `Version`.
175
124
  def reset_timestamp_attrs_for_update_if_needed
@@ -194,15 +143,17 @@ module PaperTrail
194
143
  # Save, and create a version record regardless of options such as `:on`,
195
144
  # `:if`, or `:unless`.
196
145
  #
197
- # Arguments are passed to `save`.
146
+ # `in_after_callback`: Indicates if this method is being called within an
147
+ # `after` callback. Defaults to `false`.
148
+ # `options`: Optional arguments passed to `save`.
198
149
  #
199
150
  # This is an "update" event. That is, we record the same data we would in
200
151
  # the case of a normal AR `update`.
201
- def save_with_version(**options)
152
+ def save_with_version(in_after_callback: false, **options)
202
153
  ::PaperTrail.request(enabled: false) do
203
154
  @record.save(**options)
204
155
  end
205
- record_update(force: true, in_after_callback: false, is_touch: false)
156
+ record_update(force: true, in_after_callback: in_after_callback, is_touch: false)
206
157
  end
207
158
 
208
159
  # Like the `update_column` method from `ActiveRecord::Persistence`, but also
@@ -267,11 +218,23 @@ module PaperTrail
267
218
  def build_version_on_update(force:, in_after_callback:, is_touch:)
268
219
  event = Events::Update.new(@record, in_after_callback, is_touch, nil)
269
220
  return unless force || event.changed_notably?
221
+ data = event.data
222
+
223
+ # Copy the (recently set) `updated_at` from the record to the `created_at`
224
+ # of the `Version`. Without this feature, these two timestamps would
225
+ # differ by a few milliseconds. To some people, it seems a little
226
+ # unnatural to tamper with creation timestamps in this way. But, this
227
+ # feature has existed for a long time, almost a decade now, and some users
228
+ # may rely on it now.
229
+ if @record.respond_to?(:updated_at) &&
230
+ @record.paper_trail_options[:synchronize_version_creation_timestamp] != false
231
+ data[:created_at] = @record.updated_at
232
+ end
270
233
 
271
234
  # Merge data from `Event` with data from PT-AT. We no longer use
272
235
  # `data_for_update` but PT-AT still does. To save memory, we use `merge!`
273
236
  # instead of `merge`.
274
- data = event.data.merge!(data_for_update)
237
+ data.merge!(data_for_update)
275
238
 
276
239
  # Using `version_class.new` reduces memory usage compared to
277
240
  # `versions_assoc.build`. It's a trade-off though. We have to clear
@@ -280,6 +243,42 @@ module PaperTrail
280
243
  @record.class.paper_trail.version_class.new(data)
281
244
  end
282
245
 
246
+ # PT-AT extends this method to add its transaction id.
247
+ #
248
+ # @api public
249
+ def data_for_create
250
+ {}
251
+ end
252
+
253
+ # PT-AT extends this method to add its transaction id.
254
+ #
255
+ # @api public
256
+ def data_for_destroy
257
+ {}
258
+ end
259
+
260
+ # PT-AT extends this method to add its transaction id.
261
+ #
262
+ # @api public
263
+ def data_for_update
264
+ {}
265
+ end
266
+
267
+ # PT-AT extends this method to add its transaction id.
268
+ #
269
+ # @api public
270
+ def data_for_update_columns
271
+ {}
272
+ end
273
+
274
+ # Is PT enabled for this particular record?
275
+ # @api private
276
+ def enabled?
277
+ PaperTrail.enabled? &&
278
+ PaperTrail.request.enabled? &&
279
+ PaperTrail.request.enabled_for_model?(@record.class)
280
+ end
281
+
283
282
  def log_version_errors(version, action)
284
283
  version.logger&.warn(
285
284
  "Unable to create version for #{action} of #{@record.class.name}" \
@@ -287,6 +286,26 @@ module PaperTrail
287
286
  )
288
287
  end
289
288
 
289
+ # @api private
290
+ # @return - The created version object, so that plugins can use it, e.g.
291
+ # paper_trail-association_tracking
292
+ def record_update_columns(changes)
293
+ return unless enabled?
294
+ data = Events::Update.new(@record, false, false, changes).data
295
+
296
+ # Merge data from `Event` with data from PT-AT. We no longer use
297
+ # `data_for_update_columns` but PT-AT still does.
298
+ data.merge!(data_for_update_columns)
299
+
300
+ versions_assoc = @record.send(@record.class.versions_association_name)
301
+ version = versions_assoc.create(data)
302
+ if version.errors.any?
303
+ log_version_errors(version, :update)
304
+ else
305
+ version
306
+ end
307
+ end
308
+
290
309
  def version
291
310
  @record.public_send(@record.class.version_association_name)
292
311
  end
@@ -75,28 +75,6 @@ module PaperTrail
75
75
  !!store.fetch(:"enabled_for_#{model}", true)
76
76
  end
77
77
 
78
- # @api private
79
- def merge(options)
80
- options.to_h.each do |k, v|
81
- store[k] = v
82
- end
83
- end
84
-
85
- # @api private
86
- def set(options)
87
- store.clear
88
- merge(options)
89
- end
90
-
91
- # Returns a deep copy of the internal hash from our RequestStore. Keys are
92
- # all symbols. Values are mostly primitives, but whodunnit can be a Proc.
93
- # We cannot use Marshal.dump here because it doesn't support Proc. It is
94
- # unclear exactly how `deep_dup` handles a Proc, but it doesn't complain.
95
- # @api private
96
- def to_h
97
- store.deep_dup
98
- end
99
-
100
78
  # Temporarily set `options` and execute a block.
101
79
  # @api private
102
80
  def with(options)
@@ -133,6 +111,19 @@ module PaperTrail
133
111
 
134
112
  private
135
113
 
114
+ # @api private
115
+ def merge(options)
116
+ options.to_h.each do |k, v|
117
+ store[k] = v
118
+ end
119
+ end
120
+
121
+ # @api private
122
+ def set(options)
123
+ store.clear
124
+ merge(options)
125
+ end
126
+
136
127
  # Returns a Hash, initializing with default values if necessary.
137
128
  # @api private
138
129
  def store
@@ -141,6 +132,15 @@ module PaperTrail
141
132
  }
142
133
  end
143
134
 
135
+ # Returns a deep copy of the internal hash from our RequestStore. Keys are
136
+ # all symbols. Values are mostly primitives, but whodunnit can be a Proc.
137
+ # We cannot use Marshal.dump here because it doesn't support Proc. It is
138
+ # unclear exactly how `deep_dup` handles a Proc, but it doesn't complain.
139
+ # @api private
140
+ def to_h
141
+ store.deep_dup
142
+ end
143
+
144
144
  # Provide a helpful error message if someone has a typo in one of their
145
145
  # option keys. We don't validate option values here. That's traditionally
146
146
  # been handled with casting (`to_s`, `!!`) in the accessor method.
@@ -9,13 +9,23 @@ module PaperTrail
9
9
  extend self # makes all instance methods become module methods as well
10
10
 
11
11
  def load(string)
12
- ::YAML.respond_to?(:unsafe_load) ? ::YAML.unsafe_load(string) : ::YAML.load(string)
12
+ if use_safe_load?
13
+ ::YAML.safe_load(
14
+ string,
15
+ permitted_classes: yaml_column_permitted_classes,
16
+ aliases: true
17
+ )
18
+ elsif ::YAML.respond_to?(:unsafe_load)
19
+ ::YAML.unsafe_load(string)
20
+ else
21
+ ::YAML.load(string)
22
+ end
13
23
  end
14
24
 
15
25
  # @param object (Hash | HashWithIndifferentAccess) - Coming from
16
26
  # `recordable_object` `object` will be a plain `Hash`. However, due to
17
- # recent [memory optimizations](https://git.io/fjeYv), when coming from
18
- # `recordable_object_changes`, it will be a `HashWithIndifferentAccess`.
27
+ # recent [memory optimizations](https://github.com/paper-trail-gem/paper_trail/pull/1189),
28
+ # when coming from `recordable_object_changes`, it will be a `HashWithIndifferentAccess`.
19
29
  def dump(object)
20
30
  object = object.to_hash if object.is_a?(HashWithIndifferentAccess)
21
31
  ::YAML.dump object
@@ -26,6 +36,33 @@ module PaperTrail
26
36
  def where_object_condition(arel_field, field, value)
27
37
  arel_field.matches("%\n#{field}: #{value}\n%")
28
38
  end
39
+
40
+ private
41
+
42
+ def use_safe_load?
43
+ if ::ActiveRecord.gem_version >= Gem::Version.new("7.0.3.1")
44
+ # `use_yaml_unsafe_load` may be removed in the future, at which point
45
+ # safe loading will be the default.
46
+ !defined?(ActiveRecord.use_yaml_unsafe_load) || !ActiveRecord.use_yaml_unsafe_load
47
+ elsif defined?(ActiveRecord::Base.use_yaml_unsafe_load)
48
+ # Rails 5.2.8.1, 6.0.5.1, 6.1.6.1
49
+ !ActiveRecord::Base.use_yaml_unsafe_load
50
+ else
51
+ false
52
+ end
53
+ end
54
+
55
+ def yaml_column_permitted_classes
56
+ if defined?(ActiveRecord.yaml_column_permitted_classes)
57
+ # Rails >= 7.0.3.1
58
+ ActiveRecord.yaml_column_permitted_classes
59
+ elsif defined?(ActiveRecord::Base.yaml_column_permitted_classes)
60
+ # Rails 5.2.8.1, 6.0.5.1, 6.1.6.1
61
+ ActiveRecord::Base.yaml_column_permitted_classes
62
+ else
63
+ []
64
+ end
65
+ end
29
66
  end
30
67
  end
31
68
  end
@@ -15,6 +15,12 @@ module PaperTrail
15
15
  module VersionConcern
16
16
  extend ::ActiveSupport::Concern
17
17
 
18
+ E_YAML_PERMITTED_CLASSES = <<-EOS.squish.freeze
19
+ PaperTrail encountered a Psych::DisallowedClass error during
20
+ deserialization of YAML column, indicating that
21
+ yaml_column_permitted_classes has not been configured correctly. %s
22
+ EOS
23
+
18
24
  included do
19
25
  belongs_to :item, polymorphic: true, optional: true, inverse_of: false
20
26
  validates_presence_of :event
@@ -348,7 +354,10 @@ module PaperTrail
348
354
  else
349
355
  begin
350
356
  PaperTrail.serializer.load(object_changes)
351
- rescue StandardError # TODO: Rescue something more specific
357
+ rescue StandardError => e
358
+ if defined?(::Psych::Exception) && e.instance_of?(::Psych::Exception)
359
+ ::Kernel.warn format(E_YAML_PERMITTED_CLASSES, e)
360
+ end
352
361
  {}
353
362
  end
354
363
  end
@@ -7,8 +7,8 @@ module PaperTrail
7
7
  # because of this confusion, but it's not worth the breaking change.
8
8
  # People are encouraged to use `PaperTrail.gem_version` instead.
9
9
  module VERSION
10
- MAJOR = 12
11
- MINOR = 2
10
+ MAJOR = 15
11
+ MINOR = 0
12
12
  TINY = 0
13
13
 
14
14
  # Set PRE to nil unless it's a pre-release (beta, rc, etc.)
data/lib/paper_trail.rb CHANGED
@@ -8,6 +8,12 @@
8
8
  # can revisit this decision.
9
9
  require "active_support/all"
10
10
 
11
+ # We used to `require "active_record"` here, but that was [replaced with a
12
+ # Railtie](https://github.com/paper-trail-gem/paper_trail/pull/1281) in PT 12.
13
+ # As a result, we cannot reference `ActiveRecord` in this file (ie. until our
14
+ # Railtie has loaded). If we did, it would cause [problems with non-Rails
15
+ # projects](https://github.com/paper-trail-gem/paper_trail/pull/1401).
16
+
11
17
  require "paper_trail/errors"
12
18
  require "paper_trail/cleaner"
13
19
  require "paper_trail/compatibility"
@@ -26,8 +32,6 @@ module PaperTrail
26
32
  named created_at.
27
33
  EOS
28
34
 
29
- RAILS_GTE_7_0 = ::ActiveRecord.gem_version >= ::Gem::Version.new("7.0.0")
30
-
31
35
  extend PaperTrail::Cleaner
32
36
 
33
37
  class << self
@@ -98,7 +102,7 @@ module PaperTrail
98
102
 
99
103
  # Returns PaperTrail's global configuration object, a singleton. These
100
104
  # settings affect all threads.
101
- # @api private
105
+ # @api public
102
106
  def config
103
107
  @config ||= PaperTrail::Config.instance
104
108
  yield @config if block_given?
@@ -106,9 +110,14 @@ module PaperTrail
106
110
  end
107
111
  alias configure config
108
112
 
113
+ # @api public
109
114
  def version
110
115
  VERSION::STRING
111
116
  end
117
+
118
+ def active_record_gte_7_0?
119
+ @active_record_gte_7_0 ||= ::ActiveRecord.gem_version >= ::Gem::Version.new("7.0.0")
120
+ end
112
121
  end
113
122
  end
114
123
 
metadata CHANGED
@@ -1,16 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: paper_trail
3
3
  version: !ruby/object:Gem::Version
4
- version: 12.2.0
4
+ version: 15.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Stewart
8
8
  - Ben Atkins
9
9
  - Jared Beck
10
- autorequire:
10
+ autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2022-01-21 00:00:00.000000000 Z
13
+ date: 2023-08-07 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activerecord
@@ -18,28 +18,28 @@ dependencies:
18
18
  requirements:
19
19
  - - ">="
20
20
  - !ruby/object:Gem::Version
21
- version: '5.2'
21
+ version: '6.1'
22
22
  type: :runtime
23
23
  prerelease: false
24
24
  version_requirements: !ruby/object:Gem::Requirement
25
25
  requirements:
26
26
  - - ">="
27
27
  - !ruby/object:Gem::Version
28
- version: '5.2'
28
+ version: '6.1'
29
29
  - !ruby/object:Gem::Dependency
30
30
  name: request_store
31
31
  requirement: !ruby/object:Gem::Requirement
32
32
  requirements:
33
33
  - - "~>"
34
34
  - !ruby/object:Gem::Version
35
- version: '1.1'
35
+ version: '1.4'
36
36
  type: :runtime
37
37
  prerelease: false
38
38
  version_requirements: !ruby/object:Gem::Requirement
39
39
  requirements:
40
40
  - - "~>"
41
41
  - !ruby/object:Gem::Version
42
- version: '1.1'
42
+ version: '1.4'
43
43
  - !ruby/object:Gem::Dependency
44
44
  name: appraisal
45
45
  requirement: !ruby/object:Gem::Requirement
@@ -116,14 +116,14 @@ dependencies:
116
116
  requirements:
117
117
  - - ">="
118
118
  - !ruby/object:Gem::Version
119
- version: '5.2'
119
+ version: '6.1'
120
120
  type: :development
121
121
  prerelease: false
122
122
  version_requirements: !ruby/object:Gem::Requirement
123
123
  requirements:
124
124
  - - ">="
125
125
  - !ruby/object:Gem::Version
126
- version: '5.2'
126
+ version: '6.1'
127
127
  - !ruby/object:Gem::Dependency
128
128
  name: rake
129
129
  requirement: !ruby/object:Gem::Requirement
@@ -352,7 +352,7 @@ homepage: https://github.com/paper-trail-gem/paper_trail
352
352
  licenses:
353
353
  - MIT
354
354
  metadata: {}
355
- post_install_message:
355
+ post_install_message:
356
356
  rdoc_options: []
357
357
  require_paths:
358
358
  - lib
@@ -360,15 +360,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
360
360
  requirements:
361
361
  - - ">="
362
362
  - !ruby/object:Gem::Version
363
- version: 2.6.0
363
+ version: 3.0.0
364
364
  required_rubygems_version: !ruby/object:Gem::Requirement
365
365
  requirements:
366
366
  - - ">="
367
367
  - !ruby/object:Gem::Version
368
368
  version: 1.3.6
369
369
  requirements: []
370
- rubygems_version: 3.2.22
371
- signing_key:
370
+ rubygems_version: 3.3.7
371
+ signing_key:
372
372
  specification_version: 4
373
373
  summary: Track changes to your models.
374
374
  test_files: []