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.
- 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
|