active_storage_validations 1.3.0 → 1.3.1

Sign up to get free protection for your applications and to get access to all the features.
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