active_storage_validations 1.3.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +9 -9
  3. data/config/locales/da.yml +0 -1
  4. data/config/locales/de.yml +0 -1
  5. data/config/locales/en.yml +0 -1
  6. data/config/locales/es.yml +0 -1
  7. data/config/locales/fr.yml +0 -1
  8. data/config/locales/it.yml +0 -1
  9. data/config/locales/ja.yml +0 -1
  10. data/config/locales/nl.yml +0 -1
  11. data/config/locales/pl.yml +0 -1
  12. data/config/locales/pt-BR.yml +0 -1
  13. data/config/locales/ru.yml +0 -1
  14. data/config/locales/sv.yml +0 -1
  15. data/config/locales/tr.yml +0 -1
  16. data/config/locales/uk.yml +0 -1
  17. data/config/locales/vi.yml +0 -1
  18. data/config/locales/zh-CN.yml +0 -1
  19. data/lib/active_storage_validations/aspect_ratio_validator.rb +55 -40
  20. data/lib/active_storage_validations/base_size_validator.rb +2 -1
  21. data/lib/active_storage_validations/concerns/attachable.rb +134 -0
  22. data/lib/active_storage_validations/concerns/errorable.rb +2 -1
  23. data/lib/active_storage_validations/concerns/optionable.rb +27 -0
  24. data/lib/active_storage_validations/content_type_spoof_detector.rb +11 -47
  25. data/lib/active_storage_validations/content_type_validator.rb +85 -45
  26. data/lib/active_storage_validations/dimension_validator.rb +5 -4
  27. data/lib/active_storage_validations/limit_validator.rb +3 -2
  28. data/lib/active_storage_validations/metadata.rb +16 -17
  29. data/lib/active_storage_validations/processable_image_validator.rb +2 -2
  30. data/lib/active_storage_validations/size_validator.rb +1 -1
  31. data/lib/active_storage_validations/total_size_validator.rb +1 -1
  32. data/lib/active_storage_validations/version.rb +1 -1
  33. data/lib/active_storage_validations.rb +0 -1
  34. metadata +54 -14
  35. data/lib/active_storage_validations/concerns/metadatable.rb +0 -31
  36. data/lib/active_storage_validations/option_proc_unfolding.rb +0 -16
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '09b483aef23513aaf10b56c6df6046078bf14e93fcfdebe2e82f2db862b85514'
4
- data.tar.gz: d040c36c24fc1fb2b4d1a2e6bb92bde86521cedfda4b4e96436a10b364499f63
3
+ metadata.gz: 435de3a42881b172293cf50bc3edb1e5462f4a0f8346543f14936502abe7a041
4
+ data.tar.gz: 6dc253e3b1dcf5ac55ae13bcd4fe9d9cabe39cdf49e0de25cc0f091bfb5c3d1b
5
5
  SHA512:
6
- metadata.gz: eb6df31c0374b1bfa804281f0effbaa0443ff6a812abd4a46e1ac4293a881e691fd5c61cd4c2622b027085acb3d10c14255f04cac3bab01f13f133d2f26c60af
7
- data.tar.gz: b5553a4a7a2f08542c8e6f9dd928b0cc78a09eb846f73346dcdec1b3826bb4a5f04ac200c21e6e2f0f58f6dcc173f226a04e7a2c85d7c15a24b5b827bfb04dcb
6
+ metadata.gz: bfd15f884cda85c47ad25c1d7e2ca8423f6d6a9e3e4aadeb415492c3a9830a00b10c611ed0eb0a5866bd12e1c2b61aaa69cfccf3952447c3cd716c07e51780c5
7
+ data.tar.gz: 91ab2c226d9657f52f6cc5f289fb8697608954d59d34400042e0521b7e60657e5ac5f256b268eef16fd1ffcd83169f61345ba3e7c9e925d1384d427f10a00bad
data/README.md CHANGED
@@ -91,9 +91,10 @@ Marcel::MimeType.extend "application/ino", extensions: %w(ino), parents: "text/p
91
91
  ```
92
92
 
93
93
  **Content type spoofing protection**
94
+
94
95
  File content type spoofing happens when an ill-intentioned user uploads a file which hides its true content type by faking its extension and its declared content type value. For example, a user may try to upload a `.exe` file (application/x-msdownload content type) dissimulated as a `.jpg` file (image/jpeg content type).
95
96
 
96
- By default, the gem does not prevent content type spoofing (prevent it by default is a breaking change that will be implemented in v2). The spoofing protection relies on both the linux `file` command and `Marcel` gem.
97
+ By default, the gem does not prevent content type spoofing (prevent it by default is a breaking change that will be implemented in v2). The spoofing protection relies on both the linux `file` command and `Marcel` gem. Be careful, since it needs to load the whole file io to perform the analysis, it will use a lot of RAM for very large files. Therefore it could be a wise decision not to enable it in this case.
97
98
 
98
99
  Take note that the `file` analyzer will not find the exactly same content type as the ActiveStorage blob (its content type detection relies on a different logic using content+filename+extension). To handle this issue, we consider a close parent content type to be a match. For example, for an ActiveStorage blob which content type is `video/x-ms-wmv`, the `file` analyzer will probably detect a `video/x-ms-asf` content type, this will be considered as a valid match because these 2 content types are closely related. The correlation mapping is based on `Marcel::TYPE_PARENTS`.
99
100
 
@@ -202,7 +203,6 @@ en:
202
203
  aspect_ratio_not_portrait: "must be a portrait image"
203
204
  aspect_ratio_not_landscape: "must be a landscape image"
204
205
  aspect_ratio_is_not: "must have an aspect ratio of %{aspect_ratio}"
205
- aspect_ratio_unknown: "has an unknown aspect ratio"
206
206
  image_not_processable: "is not a valid image"
207
207
  ```
208
208
 
@@ -436,22 +436,22 @@ Then you can use the matchers with the syntax specified in the RSpec section, ju
436
436
 
437
437
  To run tests in root folder of gem:
438
438
 
439
- * `BUNDLE_GEMFILE=gemfiles/rails_6_1_4.gemfile bundle exec rake test` to run for Rails 6.1.4
440
439
  * `BUNDLE_GEMFILE=gemfiles/rails_7_0.gemfile bundle exec rake test` to run for Rails 7.0
441
- * `BUNDLE_GEMFILE=gemfiles/rails_7_1.gemfile bundle exec rake test` to run for Rails 7.0
442
- * `BUNDLE_GEMFILE=gemfiles/rails_next.gemfile bundle exec rake test` to run for Rails main branch
440
+ * `BUNDLE_GEMFILE=gemfiles/rails_7_1.gemfile bundle exec rake test` to run for Rails 7.1
441
+ * `BUNDLE_GEMFILE=gemfiles/rails_7_2.gemfile bundle exec rake test` to run for Rails 7.2
442
+ * `BUNDLE_GEMFILE=gemfiles/rails_8_0.gemfile bundle exec rake test` to run for Rails 8.0
443
443
 
444
444
  Snippet to run in console:
445
445
 
446
446
  ```bash
447
- BUNDLE_GEMFILE=gemfiles/rails_6_1_4.gemfile bundle
448
447
  BUNDLE_GEMFILE=gemfiles/rails_7_0.gemfile bundle
449
448
  BUNDLE_GEMFILE=gemfiles/rails_7_1.gemfile bundle
450
- BUNDLE_GEMFILE=gemfiles/rails_next.gemfile bundle
451
- BUNDLE_GEMFILE=gemfiles/rails_6_1_4.gemfile bundle exec rake test
449
+ BUNDLE_GEMFILE=gemfiles/rails_7_2.gemfile bundle
450
+ BUNDLE_GEMFILE=gemfiles/rails_8_0.gemfile bundle
452
451
  BUNDLE_GEMFILE=gemfiles/rails_7_0.gemfile bundle exec rake test
453
452
  BUNDLE_GEMFILE=gemfiles/rails_7_1.gemfile bundle exec rake test
454
- BUNDLE_GEMFILE=gemfiles/rails_next.gemfile bundle exec rake test
453
+ BUNDLE_GEMFILE=gemfiles/rails_7_2.gemfile bundle exec rake test
454
+ BUNDLE_GEMFILE=gemfiles/rails_8_0.gemfile bundle exec rake test
455
455
  ```
456
456
 
457
457
  Tips:
@@ -29,5 +29,4 @@ da:
29
29
  aspect_ratio_not_portrait: "skal være et portrætbillede"
30
30
  aspect_ratio_not_landscape: "skal være et landskabsbillede"
31
31
  aspect_ratio_is_not: "skal have et størrelsesforhold på %{aspect_ratio}"
32
- aspect_ratio_unknown: "har et ukendt størrelsesforhold"
33
32
  image_not_processable: "er ikke et gyldigt billede"
@@ -29,5 +29,4 @@ de:
29
29
  aspect_ratio_not_portrait: "muss Hochformat sein"
30
30
  aspect_ratio_not_landscape: "muss Querformat sein"
31
31
  aspect_ratio_is_not: "muss ein Bildseitenverhältnis von %{aspect_ratio} haben"
32
- aspect_ratio_unknown: "hat ein unbekanntes Bildseitenverhältnis"
33
32
  image_not_processable: "ist kein gültiges Bild"
@@ -29,5 +29,4 @@ en:
29
29
  aspect_ratio_not_portrait: "must be a portrait image"
30
30
  aspect_ratio_not_landscape: "must be a landscape image"
31
31
  aspect_ratio_is_not: "must have an aspect ratio of %{aspect_ratio}"
32
- aspect_ratio_unknown: "has an unknown aspect ratio"
33
32
  image_not_processable: "is not a valid image"
@@ -29,5 +29,4 @@ es:
29
29
  aspect_ratio_not_portrait: "debe ser una imagen vertical"
30
30
  aspect_ratio_not_landscape: "debe ser una imagen apaisada"
31
31
  aspect_ratio_is_not: "debe tener una relación de aspecto de %{aspect_ratio}"
32
- aspect_ratio_unknown: "tiene una relación de aspecto desconocida"
33
32
  image_not_processable: "no es una imagen válida"
@@ -29,5 +29,4 @@ fr:
29
29
  aspect_ratio_not_portrait: "doit être une image en format portrait"
30
30
  aspect_ratio_not_landscape: "doit être une image en format paysage"
31
31
  aspect_ratio_is_not: "doit avoir un rapport hauteur / largeur de %{aspect_ratio}"
32
- aspect_ratio_unknown: "a un rapport d'aspect inconnu"
33
32
  image_not_processable: "n'est pas une image valide"
@@ -29,5 +29,4 @@ it:
29
29
  aspect_ratio_not_portrait: "l’orientamento dell’immagine deve essere verticale"
30
30
  aspect_ratio_not_landscape: "l’orientamento dell’immagine deve essere orizzontale"
31
31
  aspect_ratio_is_not: "deve avere un rapporto altezza / larghezza di %{aspect_ratio}"
32
- aspect_ratio_unknown: "ha un rapporto altezza / larghezza sconosciuto"
33
32
  image_not_processable: "non è un'immagine valida"
@@ -29,5 +29,4 @@ ja:
29
29
  aspect_ratio_not_portrait: "は縦長にしてください"
30
30
  aspect_ratio_not_landscape: "は横長にしてください"
31
31
  aspect_ratio_is_not: "のアスペクト比は %{aspect_ratio} にしてください"
32
- aspect_ratio_unknown: "のアスペクト比を取得できませんでした"
33
32
  image_not_processable: "は不正な画像です"
@@ -29,5 +29,4 @@ nl:
29
29
  aspect_ratio_not_portrait: "moet een staande afbeelding zijn"
30
30
  aspect_ratio_not_landscape: "moet een liggende afbeelding zijn"
31
31
  aspect_ratio_is_not: "moet een beeldverhouding hebben van %{aspect_ratio}"
32
- aspect_ratio_unknown: "heeft een onbekende beeldverhouding"
33
32
  image_not_processable: "is geen geldige afbeelding"
@@ -29,5 +29,4 @@ pl:
29
29
  aspect_ratio_not_portrait: "musi mieć proporcje portretu"
30
30
  aspect_ratio_not_landscape: "musi mieć proporcje pejzażu"
31
31
  aspect_ratio_is_not: "musi mieć proporcje %{aspect_ratio}"
32
- aspect_ratio_unknown: "ma nieokreślone proporcje"
33
32
  image_not_processable: "nie jest prawidłowym obrazem"
@@ -29,5 +29,4 @@ pt-BR:
29
29
  aspect_ratio_not_portrait: "não está no formato retrato"
30
30
  aspect_ratio_not_landscape: "não está no formato paisagem"
31
31
  aspect_ratio_is_not: "não contém uma proporção de %{aspect_ratio}"
32
- aspect_ratio_unknown: "não tem uma proporção definida"
33
32
  image_not_processable: "não é uma imagem válida"
@@ -29,5 +29,4 @@ ru:
29
29
  aspect_ratio_not_portrait: "должно быть портретное изображение"
30
30
  aspect_ratio_not_landscape: "должно быть пейзажное изображение"
31
31
  aspect_ratio_is_not: "должен иметь соотношение сторон %{aspect_ratio}"
32
- aspect_ratio_unknown: "имеет неизвестное соотношение сторон"
33
32
  image_not_processable: "не является допустимым изображением"
@@ -29,5 +29,4 @@ sv:
29
29
  aspect_ratio_not_portrait: "måste vara en porträttorienterad bild"
30
30
  aspect_ratio_not_landscape: "måste vara en landskapsorienterad bild"
31
31
  aspect_ratio_is_not: "måste ha en följande aspect ratio %{aspect_ratio}"
32
- aspect_ratio_unknown: "har en okänd aspect ratio"
33
32
  image_not_processable: "är inte en giltig bild"
@@ -29,5 +29,4 @@ tr:
29
29
  aspect_ratio_not_portrait: "dikey bir imaj olmalı"
30
30
  aspect_ratio_not_landscape: "yatay bir imaj olmalı"
31
31
  aspect_ratio_is_not: "%{aspect_ratio} en boy oranına sahip olmalı"
32
- aspect_ratio_unknown: "bilinmeyen en boy oranı"
33
32
  image_not_processable: "geçerli bir imaj değil"
@@ -29,5 +29,4 @@ uk:
29
29
  aspect_ratio_not_portrait: "мусить бути портретне зображення"
30
30
  aspect_ratio_not_landscape: "мусить бути пейзажне зображення"
31
31
  aspect_ratio_is_not: "мусить мати співвідношення сторін %{aspect_ratio}"
32
- aspect_ratio_unknown: "має невідоме співвідношення сторін"
33
32
  image_not_processable: "не є допустимим зображенням"
@@ -29,5 +29,4 @@ vi:
29
29
  aspect_ratio_not_portrait: "phải là ảnh đứng"
30
30
  aspect_ratio_not_landscape: "phải là ảnh ngang"
31
31
  aspect_ratio_is_not: "phải có tỉ lệ ảnh %{aspect_ratio}"
32
- aspect_ratio_unknown: "tỉ lệ ảnh không xác định"
33
32
  image_not_processable: "không phải là ảnh"
@@ -29,5 +29,4 @@ zh-CN:
29
29
  aspect_ratio_not_portrait: "必须是竖屏图片"
30
30
  aspect_ratio_not_landscape: "必须是横屏图片"
31
31
  aspect_ratio_is_not: "纵横比必须是 %{aspect_ratio}"
32
- aspect_ratio_unknown: "未知的纵横比"
33
32
  image_not_processable: "不是有效的图像"
@@ -1,16 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'concerns/active_storageable.rb'
4
+ require_relative 'concerns/attachable.rb'
4
5
  require_relative 'concerns/errorable.rb'
5
- require_relative 'concerns/metadatable.rb'
6
+ require_relative 'concerns/optionable.rb'
6
7
  require_relative 'concerns/symbolizable.rb'
7
8
 
8
9
  module ActiveStorageValidations
9
10
  class AspectRatioValidator < ActiveModel::EachValidator # :nodoc
10
11
  include ActiveStorageable
12
+ include Attachable
11
13
  include Errorable
12
- include Metadatable
13
- include OptionProcUnfolding
14
+ include Optionable
14
15
  include Symbolizable
15
16
 
16
17
  AVAILABLE_CHECKS = %i[with].freeze
@@ -22,7 +23,6 @@ module ActiveStorageValidations
22
23
  aspect_ratio_not_portrait
23
24
  aspect_ratio_not_landscape
24
25
  aspect_ratio_is_not
25
- aspect_ratio_unknown
26
26
  ].freeze
27
27
  PRECISION = 3.freeze
28
28
 
@@ -40,48 +40,63 @@ module ActiveStorageValidations
40
40
  private
41
41
 
42
42
  def is_valid?(record, attribute, attachable, metadata)
43
- flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS)
44
- errors_options = initialize_error_options(options, attachable)
45
-
46
- if metadata[:width].to_i <= 0 || metadata[:height].to_i <= 0
47
- errors_options[:aspect_ratio] = flat_options[:with]
43
+ flat_options = set_flat_options(record)
48
44
 
49
- add_error(record, attribute, :image_metadata_missing, **errors_options)
50
- return false
51
- end
45
+ return if image_metadata_missing?(record, attribute, attachable, flat_options, metadata)
52
46
 
53
47
  case flat_options[:with]
54
- when :square
55
- return true if metadata[:width] == metadata[:height]
56
- errors_options[:aspect_ratio] = flat_options[:with]
57
- add_error(record, attribute, :aspect_ratio_not_square, **errors_options)
58
-
59
- when :portrait
60
- return true if metadata[:height] > metadata[:width]
61
- errors_options[:aspect_ratio] = flat_options[:with]
62
- add_error(record, attribute, :aspect_ratio_not_portrait, **errors_options)
63
-
64
- when :landscape
65
- return true if metadata[:width] > metadata[:height]
66
- errors_options[:aspect_ratio] = flat_options[:with]
67
- add_error(record, attribute, :aspect_ratio_not_landscape, **errors_options)
68
-
69
- when ASPECT_RATIO_REGEX
70
- flat_options[:with] =~ ASPECT_RATIO_REGEX
71
- x = $1.to_i
72
- y = $2.to_i
73
-
74
- return true if x > 0 && y > 0 && (x.to_f / y).round(PRECISION) == (metadata[:width].to_f / metadata[:height]).round(PRECISION)
75
-
76
- errors_options[:aspect_ratio] = "#{x}:#{y}"
77
- add_error(record, attribute, :aspect_ratio_is_not, **errors_options)
78
- else
79
- errors_options[:aspect_ratio] = flat_options[:with]
80
- add_error(record, attribute, :aspect_ratio_unknown, **errors_options)
81
- return false
48
+ when :square then validate_square_aspect_ratio(record, attribute, attachable, flat_options, metadata)
49
+ when :portrait then validate_portrait_aspect_ratio(record, attribute, attachable, flat_options, metadata)
50
+ when :landscape then validate_landscape_aspect_ratio(record, attribute, attachable, flat_options, metadata)
51
+ when ASPECT_RATIO_REGEX then validate_regex_aspect_ratio(record, attribute, attachable, flat_options, metadata)
82
52
  end
83
53
  end
84
54
 
55
+ def image_metadata_missing?(record, attribute, attachable, flat_options, metadata)
56
+ return false if metadata[:width].to_i > 0 && metadata[:height].to_i > 0
57
+
58
+ errors_options = initialize_error_options(options, attachable)
59
+ errors_options[:aspect_ratio] = flat_options[:with]
60
+ add_error(record, attribute, :image_metadata_missing, **errors_options)
61
+ true
62
+ end
63
+
64
+ def validate_square_aspect_ratio(record, attribute, attachable, flat_options, metadata)
65
+ return if metadata[:width] == metadata[:height]
66
+
67
+ errors_options = initialize_error_options(options, attachable)
68
+ errors_options[:aspect_ratio] = flat_options[:with]
69
+ add_error(record, attribute, :aspect_ratio_not_square, **errors_options)
70
+ end
71
+
72
+ def validate_portrait_aspect_ratio(record, attribute, attachable, flat_options, metadata)
73
+ return if metadata[:width] < metadata[:height]
74
+
75
+ errors_options = initialize_error_options(options, attachable)
76
+ errors_options[:aspect_ratio] = flat_options[:with]
77
+ add_error(record, attribute, :aspect_ratio_not_portrait, **errors_options)
78
+ end
79
+
80
+ def validate_landscape_aspect_ratio(record, attribute, attachable, flat_options, metadata)
81
+ return if metadata[:width] > metadata[:height]
82
+
83
+ errors_options = initialize_error_options(options, attachable)
84
+ errors_options[:aspect_ratio] = flat_options[:with]
85
+ add_error(record, attribute, :aspect_ratio_not_landscape, **errors_options)
86
+ end
87
+
88
+ def validate_regex_aspect_ratio(record, attribute, attachable, flat_options, metadata)
89
+ flat_options[:with] =~ ASPECT_RATIO_REGEX
90
+ x = $1.to_i
91
+ y = $2.to_i
92
+
93
+ return if x > 0 && y > 0 && (x.to_f / y).round(PRECISION) == (metadata[:width].to_f / metadata[:height]).round(PRECISION)
94
+
95
+ errors_options = initialize_error_options(options, attachable)
96
+ errors_options[:aspect_ratio] = "#{x}:#{y}"
97
+ add_error(record, attribute, :aspect_ratio_is_not, **errors_options)
98
+ end
99
+
85
100
  def ensure_at_least_one_validator_option
86
101
  unless AVAILABLE_CHECKS.any? { |argument| options.key?(argument) }
87
102
  raise ArgumentError, 'You must pass :with to the validator'
@@ -2,13 +2,14 @@
2
2
 
3
3
  require_relative 'concerns/active_storageable.rb'
4
4
  require_relative 'concerns/errorable.rb'
5
+ require_relative 'concerns/optionable.rb'
5
6
  require_relative 'concerns/symbolizable.rb'
6
7
 
7
8
  module ActiveStorageValidations
8
9
  class BaseSizeValidator < ActiveModel::EachValidator # :nodoc:
9
10
  include ActiveStorageable
10
11
  include Errorable
11
- include OptionProcUnfolding
12
+ include Optionable
12
13
  include Symbolizable
13
14
 
14
15
  delegate :number_to_human_size, to: ActiveSupport::NumberHelper
@@ -0,0 +1,134 @@
1
+ require_relative "../metadata"
2
+
3
+ module ActiveStorageValidations
4
+ # ActiveStorageValidations::Attachable
5
+ #
6
+ # Validator methods for analyzing attachable.
7
+ #
8
+ # An attachable is a file representation such as ActiveStorage::Blob,
9
+ # ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile, Hash, String,
10
+ # File or Pathname
11
+ module Attachable
12
+ extend ActiveSupport::Concern
13
+
14
+ private
15
+
16
+ # Loop through the newly submitted attachables to validate them. Using
17
+ # attachables is the only way to get the attached file io that is necessary
18
+ # to perform file analyses.
19
+ def validate_changed_files_from_metadata(record, attribute)
20
+ attachables_from_changes(record, attribute).each do |attachable|
21
+ is_valid?(record, attribute, attachable, Metadata.new(attachable).metadata)
22
+ end
23
+ end
24
+
25
+ # Retrieve an array of newly submitted attachables. Some file could be passed
26
+ # several times, we just need to perform the analysis once on the file,
27
+ # therefore the use of #uniq.
28
+ def attachables_from_changes(record, attribute)
29
+ changes = record.attachment_changes[attribute.to_s]
30
+ return [] if changes.blank?
31
+
32
+ Array.wrap(
33
+ changes.is_a?(ActiveStorage::Attached::Changes::CreateMany) ? changes.attachables : changes.attachable
34
+ ).uniq
35
+ end
36
+
37
+ # Retrieve the full declared content_type from attachable.
38
+ def full_attachable_content_type(attachable)
39
+ case attachable
40
+ when ActiveStorage::Blob
41
+ attachable.content_type
42
+ when ActionDispatch::Http::UploadedFile
43
+ attachable.content_type
44
+ when Rack::Test::UploadedFile
45
+ attachable.content_type
46
+ when String
47
+ blob = ActiveStorage::Blob.find_signed!(attachable)
48
+ blob.content_type
49
+ when Hash
50
+ attachable[:content_type]
51
+ when File
52
+ supports_file_attachment? ? marcel_content_type_from_filename(attachable) : raise_rails_like_error(attachable)
53
+ when Pathname
54
+ supports_pathname_attachment? ? marcel_content_type_from_filename(attachable) : raise_rails_like_error(attachable)
55
+ else
56
+ raise_rails_like_error(attachable)
57
+ end
58
+ end
59
+
60
+ # Retrieve the declared content_type from attachable without potential mime
61
+ # type parameters (e.g. 'application/x-rar-compressed;version=5')
62
+ def attachable_content_type(attachable)
63
+ full_attachable_content_type(attachable) && full_attachable_content_type(attachable).downcase.split(/[;,\s]/, 2).first
64
+ end
65
+
66
+ # Retrieve the io from attachable.
67
+ def attachable_io(attachable)
68
+ case attachable
69
+ when ActiveStorage::Blob
70
+ attachable.download
71
+ when ActionDispatch::Http::UploadedFile
72
+ attachable.read
73
+ when Rack::Test::UploadedFile
74
+ attachable.read
75
+ when String
76
+ blob = ActiveStorage::Blob.find_signed!(attachable)
77
+ blob.download
78
+ when Hash
79
+ attachable[:io].read
80
+ when File
81
+ supports_file_attachment? ? attachable : raise_rails_like_error(attachable)
82
+ when Pathname
83
+ supports_pathname_attachment? ? attachable.read : raise_rails_like_error(attachable)
84
+ else
85
+ raise_rails_like_error(attachable)
86
+ end
87
+ end
88
+
89
+ # Retrieve the declared filename from attachable.
90
+ def attachable_filename(attachable)
91
+ case attachable
92
+ when ActiveStorage::Blob
93
+ attachable.filename
94
+ when ActionDispatch::Http::UploadedFile
95
+ attachable.original_filename
96
+ when Rack::Test::UploadedFile
97
+ attachable.original_filename
98
+ when String
99
+ blob = ActiveStorage::Blob.find_signed!(attachable)
100
+ blob.filename
101
+ when Hash
102
+ attachable[:filename]
103
+ when File
104
+ supports_file_attachment? ? File.basename(attachable) : raise_rails_like_error(attachable)
105
+ when Pathname
106
+ supports_pathname_attachment? ? File.basename(attachable) : raise_rails_like_error(attachable)
107
+ else
108
+ raise_rails_like_error(attachable)
109
+ end
110
+ end
111
+
112
+ # Raise the same Rails error for not-implemented file representations.
113
+ def raise_rails_like_error(attachable)
114
+ raise(
115
+ ArgumentError,
116
+ "Could not find or build blob: expected attachable, " \
117
+ "got #{attachable.inspect}"
118
+ )
119
+ end
120
+
121
+ # Check if the current Rails version supports File or Pathname attachment
122
+ #
123
+ # https://github.com/rails/rails/blob/7-1-stable/activestorage/CHANGELOG.md#rails-710rc1-september-27-2023
124
+ def supports_file_attachment?
125
+ Rails.gem_version >= Gem::Version.new('7.1.0.rc1')
126
+ end
127
+ alias :supports_pathname_attachment? :supports_file_attachment?
128
+
129
+ # Retrieve the content_type from the file name only
130
+ def marcel_content_type_from_filename(attachable)
131
+ Marcel::MimeType.for(name: attachable_filename(attachable).to_s)
132
+ end
133
+ end
134
+ end
@@ -30,8 +30,9 @@ module ActiveStorageValidations
30
30
 
31
31
  case file
32
32
  when ActiveStorage::Attached, ActiveStorage::Attachment then file.blob&.filename&.to_s
33
+ when ActiveStorage::Blob then file.filename
33
34
  when Hash then file[:filename]
34
- end
35
+ end.to_s
35
36
  end
36
37
  end
37
38
  end
@@ -0,0 +1,27 @@
1
+ module ActiveStorageValidations
2
+ # ActiveStorageValidations::Optionable
3
+ #
4
+ # Helper method to flatten the validator options.
5
+ module Optionable
6
+ extend ActiveSupport::Concern
7
+
8
+ private
9
+
10
+ def set_flat_options(record)
11
+ flatten_options(record, self.options)
12
+ end
13
+
14
+ def flatten_options(record, options, available_checks = self.class::AVAILABLE_CHECKS)
15
+ case options
16
+ when Hash
17
+ options.merge(options) do |key, value|
18
+ available_checks&.exclude?(key) ? {} : flatten_options(record, value, nil)
19
+ end
20
+ when Array
21
+ options.map { |option| flatten_options(record, option, available_checks) }
22
+ else
23
+ options.is_a?(Proc) ? options.call(record) : options
24
+ end
25
+ end
26
+ end
27
+ end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'concerns/attachable'
3
4
  require_relative 'concerns/loggable'
4
5
  require 'open3'
5
6
 
@@ -7,12 +8,13 @@ module ActiveStorageValidations
7
8
  class ContentTypeSpoofDetector
8
9
  class FileCommandLineToolNotInstalledError < StandardError; end
9
10
 
11
+ include Attachable
10
12
  include Loggable
11
13
 
12
- def initialize(record, attribute, file)
14
+ def initialize(record, attribute, attachable)
13
15
  @record = record
14
16
  @attribute = attribute
15
- @file = file
17
+ @attachable = attachable
16
18
  end
17
19
 
18
20
  def spoofed?
@@ -27,60 +29,22 @@ module ActiveStorageValidations
27
29
  private
28
30
 
29
31
  def filename
30
- @filename ||= @file.blob.present? && @file.blob.filename.to_s
32
+ @filename ||= attachable_filename(@attachable).to_s
31
33
  end
32
34
 
33
35
  def supplied_content_type
34
- # We remove potential mime type parameters
35
- @supplied_content_type ||= @file.blob.present? && @file.blob.content_type.downcase.split(/[;,\s]/, 2).first
36
+ @supplied_content_type ||= attachable_content_type(@attachable)
36
37
  end
37
38
 
38
39
  def io
39
- @io ||= case @record.public_send(@attribute)
40
- when ActiveStorage::Attached::One then get_io_from_one
41
- when ActiveStorage::Attached::Many then get_io_from_many
42
- end
43
- end
44
-
45
- def get_io_from_one
46
- attachable = @record.attachment_changes[@attribute.to_s].attachable
47
-
48
- case attachable
49
- when ActionDispatch::Http::UploadedFile
50
- attachable.read
51
- when String
52
- blob = ActiveStorage::Blob.find_signed!(attachable)
53
- blob.download
54
- when ActiveStorage::Blob
55
- attachable.download
56
- when Hash
57
- attachable[:io].read
58
- end
59
- end
60
-
61
- def get_io_from_many
62
- attachables = @record.attachment_changes[@attribute.to_s].attachables
63
-
64
- if attachables.all? { |attachable| attachable.is_a?(ActionDispatch::Http::UploadedFile) }
65
- attachables.find do |uploaded_file|
66
- checksum = ActiveStorage::Blob.new.send(:compute_checksum_in_chunks, uploaded_file)
67
- checksum == @file.checksum
68
- end.read
69
- elsif attachables.all? { |attachable| attachable.is_a?(String) }
70
- # It's only possible to pass one String as attachable (not an array of String)
71
- blob = ActiveStorage::Blob.find_signed!(attachables.first)
72
- blob.download
73
- elsif attachables.all? { |attachable| attachable.is_a?(ActiveStorage::Blob) }
74
- attachables.find { |blob| blob == @file.blob }.download
75
- elsif attachables.all? { |attachable| attachable.is_a?(Hash) }
76
- # It's only possible to pass one Hash as attachable (not an array of Hash)
77
- attachables.first[:io].read
78
- end
40
+ @io ||= attachable_io(@attachable)
79
41
  end
80
42
 
43
+ # Return the content_type found by Open3 analysis.
44
+ #
45
+ # Using Open3 is a better alternative than Marcel (Marcel::MimeType.for(io))
46
+ # for analyzing content type solely based on the file io.
81
47
  def content_type_from_analyzer
82
- # Using Open3 is a better alternative than Marcel (Marcel::MimeType.for(io))
83
- # for analyzing content type solely based on the file io
84
48
  @content_type_from_analyzer ||= open3_mime_type_for_io
85
49
  end
86
50
 
@@ -1,15 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'concerns/active_storageable.rb'
4
+ require_relative 'concerns/attachable.rb'
4
5
  require_relative 'concerns/errorable.rb'
6
+ require_relative 'concerns/optionable.rb'
5
7
  require_relative 'concerns/symbolizable.rb'
6
8
  require_relative 'content_type_spoof_detector.rb'
7
9
 
8
10
  module ActiveStorageValidations
9
11
  class ContentTypeValidator < ActiveModel::EachValidator # :nodoc:
10
12
  include ActiveStorageable
11
- include OptionProcUnfolding
13
+ include Attachable
12
14
  include Errorable
15
+ include Optionable
13
16
  include Symbolizable
14
17
 
15
18
  AVAILABLE_CHECKS = %i[with in].freeze
@@ -26,18 +29,20 @@ module ActiveStorageValidations
26
29
  def validate_each(record, attribute, _value)
27
30
  return if no_attachments?(record, attribute)
28
31
 
29
- types = authorized_types(record)
30
- return if types.empty?
32
+ @authorized_content_types = authorized_content_types_from_options(record)
33
+ return if @authorized_content_types.empty?
31
34
 
32
- attached_files(record, attribute).each do |file|
33
- is_valid?(record, attribute, file, types)
35
+ attachables_from_changes(record, attribute).each do |attachable|
36
+ set_attachable_cached_values(attachable)
37
+ is_valid?(record, attribute, attachable)
34
38
  end
35
39
  end
36
40
 
37
41
  private
38
42
 
39
- def authorized_types(record)
40
- flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS)
43
+ def authorized_content_types_from_options(record)
44
+ flat_options = set_flat_options(record)
45
+
41
46
  (Array.wrap(flat_options[:with]) + Array.wrap(flat_options[:in])).compact.map do |type|
42
47
  case type
43
48
  when String, Symbol then Marcel::MimeType.for(declared_type: type.to_s, extension: type.to_s)
@@ -46,47 +51,48 @@ module ActiveStorageValidations
46
51
  end
47
52
  end
48
53
 
49
- def types_to_human_format(types)
50
- types
51
- .map { |type| type.is_a?(Regexp) ? type.source : type.to_s.split('/').last.upcase }
52
- .join(', ')
54
+ def set_attachable_cached_values(attachable)
55
+ @attachable_content_type = attachable_content_type(attachable)
56
+ @attachable_filename = attachable_filename(attachable).to_s
53
57
  end
54
58
 
55
- def content_type(file)
56
- # We remove potential mime type parameters
57
- file.blob.present? && file.blob.content_type.downcase.split(/[;,\s]/, 2).first
59
+ def is_valid?(record, attribute, attachable)
60
+ extension_matches_content_type?(record, attribute, attachable) &&
61
+ authorized_content_type?(record, attribute, attachable) &&
62
+ not_spoofing_content_type?(record, attribute, attachable)
58
63
  end
59
64
 
60
- def is_valid?(record, attribute, file, types)
61
- file_type_in_authorized_types?(record, attribute, file, types) &&
62
- not_spoofing_content_type?(record, attribute, file)
65
+ def extension_matches_content_type?(record, attribute, attachable)
66
+ extension = @attachable_filename.split('.').second
67
+
68
+ possible_extensions = Marcel::TYPE_EXTS[@attachable_content_type]
69
+ return true if possible_extensions && extension.in?(possible_extensions)
70
+
71
+ errors_options = initialize_and_populate_error_options(options, attachable)
72
+ add_error(record, attribute, ERROR_TYPES.first, **errors_options)
73
+ false
63
74
  end
64
75
 
65
- def file_type_in_authorized_types?(record, attribute, file, types)
66
- file_type = content_type(file)
67
- file_type_is_authorized = types.any? do |type|
68
- case type
69
- when String then type == file_type
70
- when Regexp then type.match?(file_type.to_s)
76
+ def authorized_content_type?(record, attribute, attachable)
77
+ attachable_content_type_is_authorized = @authorized_content_types.any? do |authorized_content_type|
78
+ case authorized_content_type
79
+ when String then authorized_content_type == marcel_attachable_content_type(attachable)
80
+ when Regexp then authorized_content_type.match?(marcel_attachable_content_type(attachable).to_s)
71
81
  end
72
82
  end
73
83
 
74
- if file_type_is_authorized
75
- true
76
- else
77
- errors_options = initialize_error_options(options, file)
78
- errors_options[:authorized_types] = types_to_human_format(types)
79
- errors_options[:content_type] = content_type(file)
80
- add_error(record, attribute, ERROR_TYPES.first, **errors_options)
81
- false
82
- end
84
+ return true if attachable_content_type_is_authorized
85
+
86
+ errors_options = initialize_and_populate_error_options(options, attachable)
87
+ add_error(record, attribute, ERROR_TYPES.first, **errors_options)
88
+ false
83
89
  end
84
90
 
85
- def not_spoofing_content_type?(record, attribute, file)
91
+ def not_spoofing_content_type?(record, attribute, attachable)
86
92
  return true unless enable_spoofing_protection?
87
93
 
88
- if ContentTypeSpoofDetector.new(record, attribute, file).spoofed?
89
- errors_options = initialize_error_options(options, file)
94
+ if ContentTypeSpoofDetector.new(record, attribute, attachable).spoofed?
95
+ errors_options = initialize_error_options(options, attachable)
90
96
  add_error(record, attribute, ERROR_TYPES.second, **errors_options)
91
97
  false
92
98
  else
@@ -94,6 +100,29 @@ module ActiveStorageValidations
94
100
  end
95
101
  end
96
102
 
103
+ def marcel_attachable_content_type(attachable)
104
+ Marcel::MimeType.for(declared_type: @attachable_content_type, name: @attachable_filename)
105
+ end
106
+
107
+ def enable_spoofing_protection?
108
+ options[:spoofing_protection] == true
109
+ end
110
+
111
+ def initialize_and_populate_error_options(options, attachable)
112
+ errors_options = initialize_error_options(options, attachable)
113
+ errors_options[:content_type] = @attachable_content_type
114
+ errors_options[:authorized_types] = authorized_content_types_to_human_format
115
+ errors_options
116
+ end
117
+
118
+ def authorized_content_types_to_human_format
119
+ @authorized_content_types
120
+ .map do |authorized_content_type|
121
+ authorized_content_type.is_a?(Regexp) ? authorized_content_type.source : authorized_content_type.to_s.split('/').last.upcase
122
+ end
123
+ .join(', ')
124
+ end
125
+
97
126
  def ensure_exactly_one_validator_option
98
127
  unless AVAILABLE_CHECKS.one? { |argument| options.key?(argument) }
99
128
  raise ArgumentError, 'You must pass either :with or :in to the validator'
@@ -104,28 +133,39 @@ module ActiveStorageValidations
104
133
  return true if options[:with]&.is_a?(Proc) || options[:in]&.is_a?(Proc)
105
134
 
106
135
  ([options[:with]] || options[:in]).each do |content_type|
107
- raise ArgumentError, invalid_content_type_message(content_type) if invalid_content_type?(content_type)
136
+ raise ArgumentError, invalid_content_type_option_message(content_type) if invalid_option?(content_type)
108
137
  end
109
138
  end
110
139
 
111
- def invalid_content_type_message(content_type)
112
- <<~ERROR_MESSAGE
113
- You must pass valid content types to the validator
114
- '#{content_type}' is not found in Marcel::EXTENSIONS mimes
115
- ERROR_MESSAGE
140
+ def invalid_content_type_option_message(content_type)
141
+ if content_type.to_s.match?(/\//)
142
+ <<~ERROR_MESSAGE
143
+ You must pass valid content types to the validator
144
+ '#{content_type}' is not found in Marcel::TYPE_EXTS
145
+ ERROR_MESSAGE
146
+ else
147
+ <<~ERROR_MESSAGE
148
+ You must pass valid content types extensions to the validator
149
+ '#{content_type}' is not found in Marcel::EXTENSIONS
150
+ ERROR_MESSAGE
151
+ end
116
152
  end
117
153
 
118
- def invalid_content_type?(content_type)
154
+ def invalid_option?(content_type)
119
155
  case content_type
120
156
  when String, Symbol
121
- Marcel::MimeType.for(declared_type: content_type.to_s, extension: content_type.to_s) == 'application/octet-stream'
157
+ content_type.to_s.match?(/\//) ? invalid_content_type?(content_type) : invalid_extension?(content_type)
122
158
  when Regexp
123
159
  false # We always validate regexes
124
160
  end
125
161
  end
126
162
 
127
- def enable_spoofing_protection?
128
- options[:spoofing_protection] == true
163
+ def invalid_content_type?(content_type)
164
+ Marcel::TYPE_EXTS[content_type.to_s] == nil
165
+ end
166
+
167
+ def invalid_extension?(content_type)
168
+ Marcel::MimeType.for(extension: content_type.to_s) == 'application/octet-stream'
129
169
  end
130
170
  end
131
171
  end
@@ -1,16 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'concerns/active_storageable.rb'
4
+ require_relative 'concerns/attachable.rb'
4
5
  require_relative 'concerns/errorable.rb'
5
- require_relative 'concerns/metadatable.rb'
6
+ require_relative 'concerns/optionable.rb'
6
7
  require_relative 'concerns/symbolizable.rb'
7
8
 
8
9
  module ActiveStorageValidations
9
10
  class DimensionValidator < ActiveModel::EachValidator # :nodoc
10
11
  include ActiveStorageable
12
+ include Attachable
11
13
  include Errorable
12
- include OptionProcUnfolding
13
- include Metadatable
14
+ include Optionable
14
15
  include Symbolizable
15
16
 
16
17
  AVAILABLE_CHECKS = %i[width height min max].freeze
@@ -122,7 +123,7 @@ module ActiveStorageValidations
122
123
  end
123
124
 
124
125
  def process_options(record)
125
- flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS)
126
+ flat_options = set_flat_options(record)
126
127
 
127
128
  [:width, :height].each do |length|
128
129
  if flat_options[length] and flat_options[length].is_a?(Hash)
@@ -2,13 +2,14 @@
2
2
 
3
3
  require_relative 'concerns/active_storageable.rb'
4
4
  require_relative 'concerns/errorable.rb'
5
+ require_relative 'concerns/optionable.rb'
5
6
  require_relative 'concerns/symbolizable.rb'
6
7
 
7
8
  module ActiveStorageValidations
8
9
  class LimitValidator < ActiveModel::EachValidator # :nodoc:
9
10
  include ActiveStorageable
10
- include OptionProcUnfolding
11
11
  include Errorable
12
+ include Optionable
12
13
  include Symbolizable
13
14
 
14
15
  AVAILABLE_CHECKS = %i[max min].freeze
@@ -23,7 +24,7 @@ module ActiveStorageValidations
23
24
 
24
25
  def validate_each(record, attribute, _value)
25
26
  files = attached_files(record, attribute).reject(&:blank?)
26
- flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS)
27
+ flat_options = set_flat_options(record)
27
28
 
28
29
  return if files_count_valid?(files.count, flat_options)
29
30
 
@@ -8,13 +8,13 @@ module ActiveStorageValidations
8
8
 
9
9
  class InvalidImageError < StandardError; end
10
10
 
11
- attr_reader :file
11
+ attr_reader :attachable
12
12
 
13
13
  DEFAULT_IMAGE_PROCESSOR = :mini_magick.freeze
14
14
 
15
- def initialize(file)
15
+ def initialize(attachable)
16
16
  require_image_processor
17
- @file = file
17
+ @attachable = attachable
18
18
  end
19
19
 
20
20
  def valid?
@@ -64,14 +64,9 @@ module ActiveStorageValidations
64
64
  end
65
65
 
66
66
  def read_image
67
- is_string = file.is_a?(String)
68
- if is_string || file.is_a?(ActiveStorage::Blob)
69
- blob =
70
- if is_string
71
- ActiveStorage::Blob.find_signed!(file)
72
- else
73
- file
74
- end
67
+ is_string = attachable.is_a?(String)
68
+ if is_string || attachable.is_a?(ActiveStorage::Blob)
69
+ blob = is_string ? ActiveStorage::Blob.find_signed!(attachable) : attachable
75
70
 
76
71
  tempfile = Tempfile.new(["ActiveStorage-#{blob.id}-", blob.filename.extension_with_delimiter])
77
72
  tempfile.binmode
@@ -107,9 +102,9 @@ module ActiveStorageValidations
107
102
  begin
108
103
  Vips::Image.new_from_file(path)
109
104
  rescue exception_class
110
- # We handle cases where an error is raised when reading the file
105
+ # We handle cases where an error is raised when reading the attachable
111
106
  # because Vips can throw errors rather than returning false
112
- # We stumble upon this issue while reading 0 byte size file
107
+ # We stumble upon this issue while reading 0 byte size attachable
113
108
  # https://github.com/janko/image_processing/issues/97
114
109
  false
115
110
  end
@@ -156,13 +151,13 @@ module ActiveStorageValidations
156
151
  end
157
152
 
158
153
  def read_file_path
159
- case file
154
+ case attachable
160
155
  when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile
161
- file.path
156
+ attachable.path
162
157
  when Hash
163
- io = file.fetch(:io)
158
+ io = attachable.fetch(:io)
164
159
  if io.is_a?(StringIO)
165
- tempfile = Tempfile.new([File.basename(file[:filename], '.*'), File.extname(file[:filename])])
160
+ tempfile = Tempfile.new([File.basename(attachable[:filename], '.*'), File.extname(attachable[:filename])])
166
161
  tempfile.binmode
167
162
  IO.copy_stream(io, tempfile)
168
163
  io.rewind
@@ -172,6 +167,10 @@ module ActiveStorageValidations
172
167
  else
173
168
  File.open(io).path
174
169
  end
170
+ when File
171
+ attachable.path
172
+ when Pathname
173
+ attachable.to_s
175
174
  else
176
175
  raise "Something wrong with params."
177
176
  end
@@ -1,15 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'concerns/active_storageable.rb'
4
+ require_relative 'concerns/attachable.rb'
4
5
  require_relative 'concerns/errorable.rb'
5
- require_relative 'concerns/metadatable.rb'
6
6
  require_relative 'concerns/symbolizable.rb'
7
7
 
8
8
  module ActiveStorageValidations
9
9
  class ProcessableImageValidator < ActiveModel::EachValidator # :nodoc
10
10
  include ActiveStorageable
11
+ include Attachable
11
12
  include Errorable
12
- include Metadatable
13
13
  include Symbolizable
14
14
 
15
15
  ERROR_TYPES = %i[
@@ -15,7 +15,7 @@ module ActiveStorageValidations
15
15
  def validate_each(record, attribute, _value)
16
16
  return if no_attachments?(record, attribute)
17
17
 
18
- flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS)
18
+ flat_options = set_flat_options(record)
19
19
 
20
20
  attached_files(record, attribute).each do |file|
21
21
  next if is_valid?(file.blob.byte_size, flat_options)
@@ -18,7 +18,7 @@ module ActiveStorageValidations
18
18
  return if no_attachments?(record, attribute)
19
19
 
20
20
  total_file_size = attached_files(record, attribute).sum { |file| file.blob.byte_size }
21
- flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS)
21
+ flat_options = set_flat_options(record)
22
22
 
23
23
  return if is_valid?(total_file_size, flat_options)
24
24
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveStorageValidations
4
- VERSION = '1.3.0'
4
+ VERSION = '1.3.1'
5
5
  end
@@ -5,7 +5,6 @@ require 'active_support/concern'
5
5
 
6
6
  require 'active_storage_validations/railtie'
7
7
  require 'active_storage_validations/engine'
8
- require 'active_storage_validations/option_proc_unfolding'
9
8
  require 'active_storage_validations/attached_validator'
10
9
  require 'active_storage_validations/content_type_validator'
11
10
  require 'active_storage_validations/limit_validator'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_storage_validations
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ version: 1.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Igor Kasyanchuk
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-10-20 00:00:00.000000000 Z
11
+ date: 2024-11-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activejob
@@ -66,6 +66,20 @@ dependencies:
66
66
  - - ">="
67
67
  - !ruby/object:Gem::Version
68
68
  version: 6.1.4
69
+ - !ruby/object:Gem::Dependency
70
+ name: marcel
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: 1.0.3
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: 1.0.3
69
83
  - !ruby/object:Gem::Dependency
70
84
  name: combustion
71
85
  requirement: !ruby/object:Gem::Requirement
@@ -81,47 +95,73 @@ dependencies:
81
95
  - !ruby/object:Gem::Version
82
96
  version: '1.3'
83
97
  - !ruby/object:Gem::Dependency
84
- name: marcel
98
+ name: mini_magick
85
99
  requirement: !ruby/object:Gem::Requirement
86
100
  requirements:
87
101
  - - ">="
88
102
  - !ruby/object:Gem::Version
89
- version: 1.0.3
103
+ version: 4.9.5
90
104
  type: :development
91
105
  prerelease: false
92
106
  version_requirements: !ruby/object:Gem::Requirement
93
107
  requirements:
94
108
  - - ">="
95
109
  - !ruby/object:Gem::Version
96
- version: 1.0.3
110
+ version: 4.9.5
97
111
  - !ruby/object:Gem::Dependency
98
- name: mini_magick
112
+ name: minitest-focus
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '1.4'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '1.4'
125
+ - !ruby/object:Gem::Dependency
126
+ name: minitest-mock_expectations
99
127
  requirement: !ruby/object:Gem::Requirement
100
128
  requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '1.2'
101
132
  - - ">="
102
133
  - !ruby/object:Gem::Version
103
- version: 4.9.5
134
+ version: 1.2.0
104
135
  type: :development
105
136
  prerelease: false
106
137
  version_requirements: !ruby/object:Gem::Requirement
107
138
  requirements:
139
+ - - "~>"
140
+ - !ruby/object:Gem::Version
141
+ version: '1.2'
108
142
  - - ">="
109
143
  - !ruby/object:Gem::Version
110
- version: 4.9.5
144
+ version: 1.2.0
111
145
  - !ruby/object:Gem::Dependency
112
- name: minitest-focus
146
+ name: minitest-stub_any_instance
113
147
  requirement: !ruby/object:Gem::Requirement
114
148
  requirements:
115
149
  - - "~>"
116
150
  - !ruby/object:Gem::Version
117
- version: '1.4'
151
+ version: '1.0'
152
+ - - ">="
153
+ - !ruby/object:Gem::Version
154
+ version: 1.0.3
118
155
  type: :development
119
156
  prerelease: false
120
157
  version_requirements: !ruby/object:Gem::Requirement
121
158
  requirements:
122
159
  - - "~>"
123
160
  - !ruby/object:Gem::Version
124
- version: '1.4'
161
+ version: '1.0'
162
+ - - ">="
163
+ - !ruby/object:Gem::Version
164
+ version: 1.0.3
125
165
  - !ruby/object:Gem::Dependency
126
166
  name: pry
127
167
  requirement: !ruby/object:Gem::Requirement
@@ -223,9 +263,10 @@ files:
223
263
  - lib/active_storage_validations/attached_validator.rb
224
264
  - lib/active_storage_validations/base_size_validator.rb
225
265
  - lib/active_storage_validations/concerns/active_storageable.rb
266
+ - lib/active_storage_validations/concerns/attachable.rb
226
267
  - lib/active_storage_validations/concerns/errorable.rb
227
268
  - lib/active_storage_validations/concerns/loggable.rb
228
- - lib/active_storage_validations/concerns/metadatable.rb
269
+ - lib/active_storage_validations/concerns/optionable.rb
229
270
  - lib/active_storage_validations/concerns/symbolizable.rb
230
271
  - lib/active_storage_validations/content_type_spoof_detector.rb
231
272
  - lib/active_storage_validations/content_type_validator.rb
@@ -251,7 +292,6 @@ files:
251
292
  - lib/active_storage_validations/matchers/size_validator_matcher.rb
252
293
  - lib/active_storage_validations/matchers/total_size_validator_matcher.rb
253
294
  - lib/active_storage_validations/metadata.rb
254
- - lib/active_storage_validations/option_proc_unfolding.rb
255
295
  - lib/active_storage_validations/processable_image_validator.rb
256
296
  - lib/active_storage_validations/railtie.rb
257
297
  - lib/active_storage_validations/size_validator.rb
@@ -278,7 +318,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
278
318
  - !ruby/object:Gem::Version
279
319
  version: '0'
280
320
  requirements: []
281
- rubygems_version: 3.5.11
321
+ rubygems_version: 3.5.16
282
322
  signing_key:
283
323
  specification_version: 4
284
324
  summary: Validations for Active Storage
@@ -1,31 +0,0 @@
1
- require_relative '../metadata'
2
-
3
- module ActiveStorageValidations
4
- # ActiveStorageValidations::Metadatable
5
- #
6
- # Validator methods for analyzing the attachment metadata.
7
- module Metadatable
8
- extend ActiveSupport::Concern
9
-
10
- private
11
-
12
- # Loop through the newly submitted attachables to validate them
13
- def validate_changed_files_from_metadata(record, attribute)
14
- attachables_from_changes(record, attribute).each do |attachable|
15
- is_valid?(record, attribute, attachable, Metadata.new(attachable).metadata)
16
- end
17
- end
18
-
19
- # Retrieve an array of newly submitted attachables which are file
20
- # representations such as ActiveStorage::Blob, ActionDispatch::Http::UploadedFile,
21
- # Rack::Test::UploadedFile, Hash, String, File or Pathname
22
- def attachables_from_changes(record, attribute)
23
- changes = record.attachment_changes[attribute.to_s]
24
- return [] if changes.blank?
25
-
26
- Array.wrap(
27
- changes.is_a?(ActiveStorage::Attached::Changes::CreateMany) ? changes.attachables : changes.attachable
28
- )
29
- end
30
- end
31
- end
@@ -1,16 +0,0 @@
1
- module ActiveStorageValidations
2
- module OptionProcUnfolding
3
-
4
- def unfold_procs(record, object, only_keys)
5
- case object
6
- when Hash
7
- object.merge(object) { |key, value| only_keys&.exclude?(key) ? {} : unfold_procs(record, value, nil) }
8
- when Array
9
- object.map { |o| unfold_procs(record, o, only_keys) }
10
- else
11
- object.is_a?(Proc) ? object.call(record) : object
12
- end
13
- end
14
-
15
- end
16
- end