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.
- checksums.yaml +4 -4
- data/README.md +9 -9
- data/config/locales/da.yml +0 -1
- data/config/locales/de.yml +0 -1
- data/config/locales/en.yml +0 -1
- data/config/locales/es.yml +0 -1
- data/config/locales/fr.yml +0 -1
- data/config/locales/it.yml +0 -1
- data/config/locales/ja.yml +0 -1
- data/config/locales/nl.yml +0 -1
- data/config/locales/pl.yml +0 -1
- data/config/locales/pt-BR.yml +0 -1
- data/config/locales/ru.yml +0 -1
- data/config/locales/sv.yml +0 -1
- data/config/locales/tr.yml +0 -1
- data/config/locales/uk.yml +0 -1
- data/config/locales/vi.yml +0 -1
- data/config/locales/zh-CN.yml +0 -1
- data/lib/active_storage_validations/aspect_ratio_validator.rb +55 -40
- data/lib/active_storage_validations/base_size_validator.rb +2 -1
- data/lib/active_storage_validations/concerns/attachable.rb +134 -0
- data/lib/active_storage_validations/concerns/errorable.rb +2 -1
- data/lib/active_storage_validations/concerns/optionable.rb +27 -0
- data/lib/active_storage_validations/content_type_spoof_detector.rb +11 -47
- data/lib/active_storage_validations/content_type_validator.rb +85 -45
- data/lib/active_storage_validations/dimension_validator.rb +5 -4
- data/lib/active_storage_validations/limit_validator.rb +3 -2
- data/lib/active_storage_validations/metadata.rb +16 -17
- data/lib/active_storage_validations/processable_image_validator.rb +2 -2
- data/lib/active_storage_validations/size_validator.rb +1 -1
- data/lib/active_storage_validations/total_size_validator.rb +1 -1
- data/lib/active_storage_validations/version.rb +1 -1
- data/lib/active_storage_validations.rb +0 -1
- metadata +54 -14
- data/lib/active_storage_validations/concerns/metadatable.rb +0 -31
- 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:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 435de3a42881b172293cf50bc3edb1e5462f4a0f8346543f14936502abe7a041
|
4
|
+
data.tar.gz: 6dc253e3b1dcf5ac55ae13bcd4fe9d9cabe39cdf49e0de25cc0f091bfb5c3d1b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
442
|
-
* `BUNDLE_GEMFILE=gemfiles/
|
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/
|
451
|
-
BUNDLE_GEMFILE=gemfiles/
|
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/
|
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:
|
data/config/locales/da.yml
CHANGED
@@ -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"
|
data/config/locales/de.yml
CHANGED
@@ -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"
|
data/config/locales/en.yml
CHANGED
@@ -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"
|
data/config/locales/es.yml
CHANGED
@@ -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"
|
data/config/locales/fr.yml
CHANGED
@@ -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"
|
data/config/locales/it.yml
CHANGED
@@ -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"
|
data/config/locales/ja.yml
CHANGED
data/config/locales/nl.yml
CHANGED
@@ -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"
|
data/config/locales/pl.yml
CHANGED
@@ -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"
|
data/config/locales/pt-BR.yml
CHANGED
@@ -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"
|
data/config/locales/ru.yml
CHANGED
@@ -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: "не является допустимым изображением"
|
data/config/locales/sv.yml
CHANGED
@@ -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"
|
data/config/locales/tr.yml
CHANGED
@@ -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"
|
data/config/locales/uk.yml
CHANGED
@@ -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: "не є допустимим зображенням"
|
data/config/locales/vi.yml
CHANGED
data/config/locales/zh-CN.yml
CHANGED
@@ -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/
|
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
|
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 =
|
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
|
-
|
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
|
-
|
56
|
-
|
57
|
-
|
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
|
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,
|
14
|
+
def initialize(record, attribute, attachable)
|
13
15
|
@record = record
|
14
16
|
@attribute = attribute
|
15
|
-
@
|
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 ||= @
|
32
|
+
@filename ||= attachable_filename(@attachable).to_s
|
31
33
|
end
|
32
34
|
|
33
35
|
def supplied_content_type
|
34
|
-
|
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 ||=
|
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
|
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
|
-
|
30
|
-
return if
|
32
|
+
@authorized_content_types = authorized_content_types_from_options(record)
|
33
|
+
return if @authorized_content_types.empty?
|
31
34
|
|
32
|
-
|
33
|
-
|
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
|
40
|
-
flat_options =
|
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
|
50
|
-
|
51
|
-
|
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
|
56
|
-
|
57
|
-
|
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
|
61
|
-
|
62
|
-
|
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
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
when
|
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
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
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,
|
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,
|
89
|
-
errors_options = initialize_error_options(options,
|
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,
|
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
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
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
|
154
|
+
def invalid_option?(content_type)
|
119
155
|
case content_type
|
120
156
|
when String, Symbol
|
121
|
-
|
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
|
128
|
-
|
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/
|
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
|
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 =
|
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 =
|
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 :
|
11
|
+
attr_reader :attachable
|
12
12
|
|
13
13
|
DEFAULT_IMAGE_PROCESSOR = :mini_magick.freeze
|
14
14
|
|
15
|
-
def initialize(
|
15
|
+
def initialize(attachable)
|
16
16
|
require_image_processor
|
17
|
-
@
|
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 =
|
68
|
-
if is_string ||
|
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
|
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
|
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
|
154
|
+
case attachable
|
160
155
|
when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile
|
161
|
-
|
156
|
+
attachable.path
|
162
157
|
when Hash
|
163
|
-
io =
|
158
|
+
io = attachable.fetch(:io)
|
164
159
|
if io.is_a?(StringIO)
|
165
|
-
tempfile = Tempfile.new([File.basename(
|
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 =
|
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 =
|
21
|
+
flat_options = set_flat_options(record)
|
22
22
|
|
23
23
|
return if is_valid?(total_file_size, flat_options)
|
24
24
|
|
@@ -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.
|
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-
|
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:
|
98
|
+
name: mini_magick
|
85
99
|
requirement: !ruby/object:Gem::Requirement
|
86
100
|
requirements:
|
87
101
|
- - ">="
|
88
102
|
- !ruby/object:Gem::Version
|
89
|
-
version:
|
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:
|
110
|
+
version: 4.9.5
|
97
111
|
- !ruby/object:Gem::Dependency
|
98
|
-
name:
|
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:
|
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:
|
144
|
+
version: 1.2.0
|
111
145
|
- !ruby/object:Gem::Dependency
|
112
|
-
name: minitest-
|
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.
|
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.
|
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/
|
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.
|
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
|