active_storage_validations 1.2.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 +35 -22
- data/config/locales/da.yml +1 -2
- data/config/locales/de.yml +1 -1
- data/config/locales/en.yml +1 -1
- data/config/locales/es.yml +1 -1
- data/config/locales/fr.yml +1 -1
- data/config/locales/it.yml +1 -1
- data/config/locales/ja.yml +1 -1
- data/config/locales/nl.yml +1 -1
- data/config/locales/pl.yml +1 -1
- data/config/locales/pt-BR.yml +1 -1
- data/config/locales/ru.yml +1 -1
- data/config/locales/sv.yml +1 -1
- data/config/locales/tr.yml +1 -1
- data/config/locales/uk.yml +1 -1
- data/config/locales/vi.yml +1 -1
- data/config/locales/zh-CN.yml +1 -1
- data/lib/active_storage_validations/aspect_ratio_validator.rb +57 -66
- data/lib/active_storage_validations/attached_validator.rb +5 -3
- data/lib/active_storage_validations/base_size_validator.rb +4 -1
- data/lib/active_storage_validations/concerns/active_storageable.rb +28 -0
- data/lib/active_storage_validations/concerns/attachable.rb +134 -0
- data/lib/active_storage_validations/concerns/errorable.rb +4 -4
- data/lib/active_storage_validations/concerns/loggable.rb +9 -0
- data/lib/active_storage_validations/concerns/optionable.rb +27 -0
- data/lib/active_storage_validations/content_type_spoof_detector.rb +94 -0
- data/lib/active_storage_validations/content_type_validator.rb +113 -39
- data/lib/active_storage_validations/dimension_validator.rb +32 -52
- data/lib/active_storage_validations/limit_validator.rb +8 -5
- data/lib/active_storage_validations/marcel_extensor.rb +5 -0
- data/lib/active_storage_validations/matchers/concerns/attachable.rb +27 -9
- data/lib/active_storage_validations/matchers/concerns/messageable.rb +1 -1
- data/lib/active_storage_validations/matchers/content_type_validator_matcher.rb +7 -0
- data/lib/active_storage_validations/matchers/limit_validator_matcher.rb +127 -0
- data/lib/active_storage_validations/matchers/total_size_validator_matcher.rb +1 -10
- data/lib/active_storage_validations/matchers.rb +4 -15
- data/lib/active_storage_validations/metadata.rb +22 -26
- data/lib/active_storage_validations/processable_image_validator.rb +17 -32
- data/lib/active_storage_validations/size_validator.rb +3 -7
- data/lib/active_storage_validations/total_size_validator.rb +4 -8
- data/lib/active_storage_validations/version.rb +1 -1
- data/lib/active_storage_validations.rb +2 -1
- metadata +67 -21
- data/lib/active_storage_validations/option_proc_unfolding.rb +0 -16
@@ -8,6 +8,27 @@ module ActiveStorageValidations
|
|
8
8
|
@subject.public_send(@attribute_name)
|
9
9
|
end
|
10
10
|
|
11
|
+
def attach_files(count)
|
12
|
+
return unless count.positive?
|
13
|
+
|
14
|
+
file_array = Array.new(count, dummy_file)
|
15
|
+
|
16
|
+
@subject.public_send(@attribute_name).attach(file_array)
|
17
|
+
end
|
18
|
+
|
19
|
+
def detach_file
|
20
|
+
@subject.attachment_changes.delete(@attribute_name.to_s)
|
21
|
+
end
|
22
|
+
alias :detach_files :detach_file
|
23
|
+
|
24
|
+
def file_attached?
|
25
|
+
@subject.public_send(@attribute_name).attached?
|
26
|
+
end
|
27
|
+
|
28
|
+
def dummy_blob
|
29
|
+
ActiveStorage::Blob.create_and_upload!(**dummy_file)
|
30
|
+
end
|
31
|
+
|
11
32
|
def dummy_file
|
12
33
|
{
|
13
34
|
io: io,
|
@@ -18,8 +39,8 @@ module ActiveStorageValidations
|
|
18
39
|
|
19
40
|
def processable_image
|
20
41
|
{
|
21
|
-
io:
|
22
|
-
filename: '
|
42
|
+
io: StringIO.new(image_data),
|
43
|
+
filename: 'processable_image.png',
|
23
44
|
content_type: 'image/png'
|
24
45
|
}
|
25
46
|
end
|
@@ -27,7 +48,7 @@ module ActiveStorageValidations
|
|
27
48
|
def not_processable_image
|
28
49
|
{
|
29
50
|
io: Tempfile.new('.'),
|
30
|
-
filename: '
|
51
|
+
filename: 'not_processable_image.txt',
|
31
52
|
content_type: 'text/plain'
|
32
53
|
}
|
33
54
|
end
|
@@ -36,12 +57,9 @@ module ActiveStorageValidations
|
|
36
57
|
@io ||= Tempfile.new('Hello world!')
|
37
58
|
end
|
38
59
|
|
39
|
-
def
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
def file_attached?
|
44
|
-
@subject.public_send(@attribute_name).attached?
|
60
|
+
def image_data
|
61
|
+
# Binary data for a 1x1 transparent PNG image
|
62
|
+
"\x89PNG\r\n\x1A\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1F\x15\xC4\x89\x00\x00\x00\nIDATx\x9Cc\x00\x01\x00\x00\x05\x00\x01\r\n\x2D\xB4\x00\x00\x00\x00IEND\xAE\x42\x60\x82"
|
45
63
|
end
|
46
64
|
end
|
47
65
|
end
|
@@ -130,6 +130,13 @@ module ActiveStorageValidations
|
|
130
130
|
content_type: type
|
131
131
|
}
|
132
132
|
end
|
133
|
+
|
134
|
+
# Due to the way we build test attachments in #attachment_for
|
135
|
+
# (ie spoofed file basically), we need to ignore the error related to
|
136
|
+
# content type spoofing in our matcher to pass the tests
|
137
|
+
def validator_errors_for_attribute
|
138
|
+
super.reject { |hash| hash[:error] == :spoofed_content_type }
|
139
|
+
end
|
133
140
|
end
|
134
141
|
end
|
135
142
|
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'concerns/active_storageable'
|
4
|
+
require_relative 'concerns/allow_blankable'
|
5
|
+
require_relative 'concerns/attachable'
|
6
|
+
require_relative 'concerns/contextable'
|
7
|
+
require_relative 'concerns/messageable'
|
8
|
+
require_relative 'concerns/rspecable'
|
9
|
+
require_relative 'concerns/validatable'
|
10
|
+
|
11
|
+
module ActiveStorageValidations
|
12
|
+
module Matchers
|
13
|
+
def validate_limits_of(name)
|
14
|
+
LimitValidatorMatcher.new(name)
|
15
|
+
end
|
16
|
+
|
17
|
+
class LimitValidatorMatcher
|
18
|
+
include ActiveStorageable
|
19
|
+
include AllowBlankable
|
20
|
+
include Attachable
|
21
|
+
include Contextable
|
22
|
+
include Messageable
|
23
|
+
include Rspecable
|
24
|
+
include Validatable
|
25
|
+
|
26
|
+
def initialize(attribute_name)
|
27
|
+
initialize_allow_blankable
|
28
|
+
initialize_contextable
|
29
|
+
initialize_messageable
|
30
|
+
initialize_rspecable
|
31
|
+
@attribute_name = attribute_name
|
32
|
+
@min = @max = nil
|
33
|
+
end
|
34
|
+
|
35
|
+
def description
|
36
|
+
"validate the limit files of :#{@attribute_name}"
|
37
|
+
end
|
38
|
+
|
39
|
+
def failure_message
|
40
|
+
message = ["is expected to validate limit file of :#{@attribute_name}"]
|
41
|
+
build_failure_message(message)
|
42
|
+
message.join("\n")
|
43
|
+
end
|
44
|
+
|
45
|
+
def min(count)
|
46
|
+
@min = count
|
47
|
+
self
|
48
|
+
end
|
49
|
+
|
50
|
+
def max(count)
|
51
|
+
@max = count
|
52
|
+
self
|
53
|
+
end
|
54
|
+
|
55
|
+
def matches?(subject)
|
56
|
+
@subject = subject.is_a?(Class) ? subject.new : subject
|
57
|
+
|
58
|
+
is_a_valid_active_storage_attribute? &&
|
59
|
+
is_context_valid? &&
|
60
|
+
is_custom_message_valid? &&
|
61
|
+
file_count_not_smaller_than_min? &&
|
62
|
+
file_count_equal_min? &&
|
63
|
+
file_count_larger_than_min? &&
|
64
|
+
file_count_smaller_than_max? &&
|
65
|
+
file_count_equal_max? &&
|
66
|
+
file_count_not_larger_than_max?
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def build_failure_message(message)
|
72
|
+
return unless @failure_message_artefacts.present?
|
73
|
+
|
74
|
+
message << " but there seem to have issues with the matcher methods you used, since:"
|
75
|
+
@failure_message_artefacts.each do |error_case|
|
76
|
+
message << " validation failed when provided with #{error_case[:count]} file(s)"
|
77
|
+
end
|
78
|
+
message << " whereas it should have passed"
|
79
|
+
end
|
80
|
+
|
81
|
+
def file_count_not_smaller_than_min?
|
82
|
+
@min.nil? || @min.zero? || !passes_validation_with_limits(@min - 1)
|
83
|
+
end
|
84
|
+
|
85
|
+
def file_count_equal_min?
|
86
|
+
@min.nil? || @min.zero? || passes_validation_with_limits(@min)
|
87
|
+
end
|
88
|
+
|
89
|
+
def file_count_larger_than_min?
|
90
|
+
@min.nil? || @min.zero? || @min == @max || passes_validation_with_limits(@min + 1)
|
91
|
+
end
|
92
|
+
|
93
|
+
def file_count_smaller_than_max?
|
94
|
+
@max.nil? || @min == @max || passes_validation_with_limits(@max - 1)
|
95
|
+
end
|
96
|
+
|
97
|
+
def file_count_equal_max?
|
98
|
+
@max.nil? || passes_validation_with_limits(@max)
|
99
|
+
end
|
100
|
+
|
101
|
+
def file_count_not_larger_than_max?
|
102
|
+
@max.nil? || !passes_validation_with_limits(@max + 1)
|
103
|
+
end
|
104
|
+
|
105
|
+
def passes_validation_with_limits(count)
|
106
|
+
attach_files(count)
|
107
|
+
validate
|
108
|
+
detach_files
|
109
|
+
is_valid? || add_failure_message_artefact(count)
|
110
|
+
end
|
111
|
+
|
112
|
+
def is_custom_message_valid?
|
113
|
+
return true if !@custom_message || (@min&.zero? && @max.nil?)
|
114
|
+
|
115
|
+
@min.nil? ? attach_files(@max + 1) : attach_files(@min - 1)
|
116
|
+
validate
|
117
|
+
detach_files
|
118
|
+
has_an_error_message_which_is_custom_message?
|
119
|
+
end
|
120
|
+
|
121
|
+
def add_failure_message_artefact(count)
|
122
|
+
@failure_message_artefacts << { count: count }
|
123
|
+
false
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -22,19 +22,10 @@ module ActiveStorageValidations
|
|
22
22
|
protected
|
23
23
|
|
24
24
|
def attach_file
|
25
|
-
#
|
25
|
+
# has_many_attached relation
|
26
26
|
@subject.public_send(@attribute_name).attach([dummy_blob])
|
27
27
|
@subject.public_send(@attribute_name)
|
28
28
|
end
|
29
|
-
|
30
|
-
def dummy_blob
|
31
|
-
ActiveStorage::Blob.create_and_upload!(
|
32
|
-
io: io,
|
33
|
-
filename: 'test.png',
|
34
|
-
content_type: 'image/png',
|
35
|
-
service_name: 'test'
|
36
|
-
)
|
37
|
-
end
|
38
29
|
end
|
39
30
|
end
|
40
31
|
end
|
@@ -3,6 +3,7 @@
|
|
3
3
|
require 'active_storage_validations/matchers/aspect_ratio_validator_matcher'
|
4
4
|
require 'active_storage_validations/matchers/attached_validator_matcher'
|
5
5
|
require 'active_storage_validations/matchers/processable_image_validator_matcher'
|
6
|
+
require 'active_storage_validations/matchers/limit_validator_matcher'
|
6
7
|
require 'active_storage_validations/matchers/content_type_validator_matcher'
|
7
8
|
require 'active_storage_validations/matchers/dimension_validator_matcher'
|
8
9
|
require 'active_storage_validations/matchers/size_validator_matcher'
|
@@ -25,21 +26,9 @@ module ActiveStorageValidations
|
|
25
26
|
end
|
26
27
|
|
27
28
|
def self.mock_metadata(attachment, width, height)
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
stub_method(ActiveStorageValidations::Metadata, :new, mock) do
|
32
|
-
yield
|
33
|
-
end
|
34
|
-
else
|
35
|
-
# Stub the metadata analysis for rails 5
|
36
|
-
stub_method(attachment, :analyze, true) do
|
37
|
-
stub_method(attachment, :analyzed?, true) do
|
38
|
-
stub_method(attachment, :metadata, { width: width, height: height }) do
|
39
|
-
yield
|
40
|
-
end
|
41
|
-
end
|
42
|
-
end
|
29
|
+
mock = Struct.new(:metadata).new({ width: width, height: height })
|
30
|
+
stub_method(ActiveStorageValidations::Metadata, :new, mock) do
|
31
|
+
yield
|
43
32
|
end
|
44
33
|
end
|
45
34
|
end
|
@@ -1,14 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'concerns/loggable'
|
4
|
+
|
1
5
|
module ActiveStorageValidations
|
2
6
|
class Metadata
|
7
|
+
include Loggable
|
8
|
+
|
3
9
|
class InvalidImageError < StandardError; end
|
4
10
|
|
5
|
-
attr_reader :
|
11
|
+
attr_reader :attachable
|
6
12
|
|
7
13
|
DEFAULT_IMAGE_PROCESSOR = :mini_magick.freeze
|
8
14
|
|
9
|
-
def initialize(
|
15
|
+
def initialize(attachable)
|
10
16
|
require_image_processor
|
11
|
-
@
|
17
|
+
@attachable = attachable
|
12
18
|
end
|
13
19
|
|
14
20
|
def valid?
|
@@ -58,18 +64,9 @@ module ActiveStorageValidations
|
|
58
64
|
end
|
59
65
|
|
60
66
|
def read_image
|
61
|
-
is_string =
|
62
|
-
if is_string ||
|
63
|
-
blob =
|
64
|
-
if is_string
|
65
|
-
if Rails.gem_version < Gem::Version.new('6.1.0')
|
66
|
-
ActiveStorage::Blob.find_signed(file)
|
67
|
-
else
|
68
|
-
ActiveStorage::Blob.find_signed!(file)
|
69
|
-
end
|
70
|
-
else
|
71
|
-
file
|
72
|
-
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
|
73
70
|
|
74
71
|
tempfile = Tempfile.new(["ActiveStorage-#{blob.id}-", blob.filename.extension_with_delimiter])
|
75
72
|
tempfile.binmode
|
@@ -105,9 +102,9 @@ module ActiveStorageValidations
|
|
105
102
|
begin
|
106
103
|
Vips::Image.new_from_file(path)
|
107
104
|
rescue exception_class
|
108
|
-
# We handle cases where an error is raised when reading the
|
105
|
+
# We handle cases where an error is raised when reading the attachable
|
109
106
|
# because Vips can throw errors rather than returning false
|
110
|
-
# We stumble upon this issue while reading 0 byte size
|
107
|
+
# We stumble upon this issue while reading 0 byte size attachable
|
111
108
|
# https://github.com/janko/image_processing/issues/97
|
112
109
|
false
|
113
110
|
end
|
@@ -154,13 +151,13 @@ module ActiveStorageValidations
|
|
154
151
|
end
|
155
152
|
|
156
153
|
def read_file_path
|
157
|
-
case
|
154
|
+
case attachable
|
158
155
|
when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile
|
159
|
-
|
156
|
+
attachable.path
|
160
157
|
when Hash
|
161
|
-
io =
|
158
|
+
io = attachable.fetch(:io)
|
162
159
|
if io.is_a?(StringIO)
|
163
|
-
tempfile = Tempfile.new([File.basename(
|
160
|
+
tempfile = Tempfile.new([File.basename(attachable[:filename], '.*'), File.extname(attachable[:filename])])
|
164
161
|
tempfile.binmode
|
165
162
|
IO.copy_stream(io, tempfile)
|
166
163
|
io.rewind
|
@@ -170,14 +167,13 @@ module ActiveStorageValidations
|
|
170
167
|
else
|
171
168
|
File.open(io).path
|
172
169
|
end
|
170
|
+
when File
|
171
|
+
attachable.path
|
172
|
+
when Pathname
|
173
|
+
attachable.to_s
|
173
174
|
else
|
174
175
|
raise "Something wrong with params."
|
175
176
|
end
|
176
177
|
end
|
177
|
-
|
178
|
-
def logger
|
179
|
-
Rails.logger
|
180
|
-
end
|
181
|
-
|
182
178
|
end
|
183
179
|
end
|
@@ -1,12 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'concerns/active_storageable.rb'
|
4
|
+
require_relative 'concerns/attachable.rb'
|
3
5
|
require_relative 'concerns/errorable.rb'
|
4
6
|
require_relative 'concerns/symbolizable.rb'
|
5
|
-
require_relative 'metadata.rb'
|
6
7
|
|
7
8
|
module ActiveStorageValidations
|
8
9
|
class ProcessableImageValidator < ActiveModel::EachValidator # :nodoc
|
9
|
-
include
|
10
|
+
include ActiveStorageable
|
11
|
+
include Attachable
|
10
12
|
include Errorable
|
11
13
|
include Symbolizable
|
12
14
|
|
@@ -14,36 +16,19 @@ module ActiveStorageValidations
|
|
14
16
|
image_not_processable
|
15
17
|
].freeze
|
16
18
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
33
|
-
else
|
34
|
-
# Rails 5
|
35
|
-
def validate_each(record, attribute, _value)
|
36
|
-
return true unless record.send(attribute).attached?
|
37
|
-
|
38
|
-
files = Array.wrap(record.send(attribute))
|
39
|
-
|
40
|
-
files.each do |file|
|
41
|
-
if !Metadata.new(file).valid?
|
42
|
-
errors_options = initialize_error_options(options, file)
|
43
|
-
add_error(record, attribute, ERROR_TYPES.first , **errors_options) unless Metadata.new(file).valid?
|
44
|
-
end
|
45
|
-
end
|
46
|
-
end
|
19
|
+
def validate_each(record, attribute, _value)
|
20
|
+
return if no_attachments?(record, attribute)
|
21
|
+
|
22
|
+
validate_changed_files_from_metadata(record, attribute)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def is_valid?(record, attribute, attachable, metadata)
|
28
|
+
return if !metadata.empty?
|
29
|
+
|
30
|
+
errors_options = initialize_error_options(options, attachable)
|
31
|
+
add_error(record, attribute, ERROR_TYPES.first , **errors_options)
|
47
32
|
end
|
48
33
|
end
|
49
34
|
end
|
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative 'concerns/errorable.rb'
|
4
|
-
require_relative 'concerns/symbolizable.rb'
|
5
3
|
require_relative 'base_size_validator.rb'
|
6
4
|
|
7
5
|
module ActiveStorageValidations
|
@@ -15,12 +13,11 @@ module ActiveStorageValidations
|
|
15
13
|
].freeze
|
16
14
|
|
17
15
|
def validate_each(record, attribute, _value)
|
18
|
-
return
|
16
|
+
return if no_attachments?(record, attribute)
|
19
17
|
|
20
|
-
|
21
|
-
flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS)
|
18
|
+
flat_options = set_flat_options(record)
|
22
19
|
|
23
|
-
|
20
|
+
attached_files(record, attribute).each do |file|
|
24
21
|
next if is_valid?(file.blob.byte_size, flat_options)
|
25
22
|
|
26
23
|
errors_options = initialize_error_options(options, file)
|
@@ -31,7 +28,6 @@ module ActiveStorageValidations
|
|
31
28
|
error_type = "file_size_not_#{keys.first}".to_sym
|
32
29
|
|
33
30
|
add_error(record, attribute, error_type, **errors_options)
|
34
|
-
break
|
35
31
|
end
|
36
32
|
end
|
37
33
|
end
|
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative 'concerns/errorable.rb'
|
4
|
-
require_relative 'concerns/symbolizable.rb'
|
5
3
|
require_relative 'base_size_validator.rb'
|
6
4
|
|
7
5
|
module ActiveStorageValidations
|
@@ -17,14 +15,12 @@ module ActiveStorageValidations
|
|
17
15
|
def validate_each(record, attribute, _value)
|
18
16
|
custom_check_validity!(record, attribute)
|
19
17
|
|
20
|
-
return
|
18
|
+
return if no_attachments?(record, attribute)
|
21
19
|
|
22
|
-
|
23
|
-
flat_options =
|
20
|
+
total_file_size = attached_files(record, attribute).sum { |file| file.blob.byte_size }
|
21
|
+
flat_options = set_flat_options(record)
|
24
22
|
|
25
|
-
|
26
|
-
|
27
|
-
return true if is_valid?(total_file_size, flat_options)
|
23
|
+
return if is_valid?(total_file_size, flat_options)
|
28
24
|
|
29
25
|
errors_options = initialize_error_options(options, nil)
|
30
26
|
populate_error_options(errors_options, flat_options)
|
@@ -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'
|
@@ -15,6 +14,8 @@ require 'active_storage_validations/processable_image_validator'
|
|
15
14
|
require 'active_storage_validations/size_validator'
|
16
15
|
require 'active_storage_validations/total_size_validator'
|
17
16
|
|
17
|
+
require 'active_storage_validations/marcel_extensor'
|
18
|
+
|
18
19
|
ActiveSupport.on_load(:active_record) do
|
19
20
|
send :include, ActiveStorageValidations
|
20
21
|
end
|