active_storage_validations 1.1.4 → 1.3.0
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 +84 -33
- data/config/locales/da.yml +33 -0
- data/config/locales/de.yml +6 -0
- data/config/locales/en.yml +6 -0
- data/config/locales/es.yml +6 -0
- data/config/locales/fr.yml +6 -0
- data/config/locales/it.yml +6 -0
- data/config/locales/ja.yml +6 -0
- data/config/locales/nl.yml +6 -0
- data/config/locales/pl.yml +6 -0
- data/config/locales/pt-BR.yml +6 -0
- data/config/locales/ru.yml +6 -0
- data/config/locales/sv.yml +11 -1
- data/config/locales/tr.yml +6 -0
- data/config/locales/uk.yml +6 -0
- data/config/locales/vi.yml +6 -0
- data/config/locales/zh-CN.yml +6 -0
- data/lib/active_storage_validations/aspect_ratio_validator.rb +10 -34
- data/lib/active_storage_validations/attached_validator.rb +6 -4
- data/lib/active_storage_validations/base_size_validator.rb +68 -0
- data/lib/active_storage_validations/concerns/active_storageable.rb +28 -0
- data/lib/active_storage_validations/concerns/errorable.rb +4 -5
- data/lib/active_storage_validations/concerns/loggable.rb +9 -0
- data/lib/active_storage_validations/concerns/metadatable.rb +31 -0
- data/lib/active_storage_validations/content_type_spoof_detector.rb +130 -0
- data/lib/active_storage_validations/content_type_validator.rb +56 -22
- data/lib/active_storage_validations/dimension_validator.rb +31 -52
- data/lib/active_storage_validations/limit_validator.rb +5 -3
- data/lib/active_storage_validations/marcel_extensor.rb +5 -0
- data/lib/active_storage_validations/matchers/aspect_ratio_validator_matcher.rb +6 -15
- data/lib/active_storage_validations/matchers/attached_validator_matcher.rb +5 -13
- data/lib/active_storage_validations/matchers/base_size_validator_matcher.rb +134 -0
- data/lib/active_storage_validations/matchers/concerns/attachable.rb +66 -0
- data/lib/active_storage_validations/matchers/concerns/contextable.rb +20 -8
- data/lib/active_storage_validations/matchers/concerns/messageable.rb +1 -1
- data/lib/active_storage_validations/matchers/concerns/validatable.rb +9 -3
- data/lib/active_storage_validations/matchers/content_type_validator_matcher.rb +12 -2
- data/lib/active_storage_validations/matchers/dimension_validator_matcher.rb +6 -15
- data/lib/active_storage_validations/matchers/limit_validator_matcher.rb +127 -0
- data/lib/active_storage_validations/matchers/processable_image_validator_matcher.rb +78 -0
- data/lib/active_storage_validations/matchers/size_validator_matcher.rb +4 -139
- data/lib/active_storage_validations/matchers/total_size_validator_matcher.rb +31 -0
- data/lib/active_storage_validations/matchers.rb +6 -15
- data/lib/active_storage_validations/metadata.rb +27 -12
- data/lib/active_storage_validations/processable_image_validator.rb +17 -32
- data/lib/active_storage_validations/size_validator.rb +6 -55
- data/lib/active_storage_validations/total_size_validator.rb +45 -0
- data/lib/active_storage_validations/version.rb +1 -1
- data/lib/active_storage_validations.rb +4 -1
- metadata +45 -46
@@ -1,13 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'concerns/active_storageable.rb'
|
3
4
|
require_relative 'concerns/errorable.rb'
|
5
|
+
require_relative 'concerns/metadatable.rb'
|
4
6
|
require_relative 'concerns/symbolizable.rb'
|
5
|
-
require_relative 'metadata.rb'
|
6
7
|
|
7
8
|
module ActiveStorageValidations
|
8
9
|
class AspectRatioValidator < ActiveModel::EachValidator # :nodoc
|
9
|
-
include
|
10
|
+
include ActiveStorageable
|
10
11
|
include Errorable
|
12
|
+
include Metadatable
|
13
|
+
include OptionProcUnfolding
|
11
14
|
include Symbolizable
|
12
15
|
|
13
16
|
AVAILABLE_CHECKS = %i[with].freeze
|
@@ -28,44 +31,17 @@ module ActiveStorageValidations
|
|
28
31
|
ensure_aspect_ratio_validity
|
29
32
|
end
|
30
33
|
|
31
|
-
|
32
|
-
|
33
|
-
return true unless record.send(attribute).attached?
|
34
|
-
|
35
|
-
changes = record.attachment_changes[attribute.to_s]
|
36
|
-
return true if changes.blank?
|
34
|
+
def validate_each(record, attribute, _value)
|
35
|
+
return if no_attachments?(record, attribute)
|
37
36
|
|
38
|
-
|
39
|
-
|
40
|
-
files.each do |file|
|
41
|
-
metadata = Metadata.new(file).metadata
|
42
|
-
next if is_valid?(record, attribute, file, metadata)
|
43
|
-
break
|
44
|
-
end
|
45
|
-
end
|
46
|
-
else
|
47
|
-
# Rails 5
|
48
|
-
def validate_each(record, attribute, _value)
|
49
|
-
return true unless record.send(attribute).attached?
|
50
|
-
|
51
|
-
files = Array.wrap(record.send(attribute))
|
52
|
-
|
53
|
-
files.each do |file|
|
54
|
-
# Analyze file first if not analyzed to get all required metadata.
|
55
|
-
file.analyze; file.reload unless file.analyzed?
|
56
|
-
metadata = file.metadata
|
57
|
-
|
58
|
-
next if is_valid?(record, attribute, file, metadata)
|
59
|
-
break
|
60
|
-
end
|
61
|
-
end
|
37
|
+
validate_changed_files_from_metadata(record, attribute)
|
62
38
|
end
|
63
39
|
|
64
40
|
private
|
65
41
|
|
66
|
-
def is_valid?(record, attribute,
|
42
|
+
def is_valid?(record, attribute, attachable, metadata)
|
67
43
|
flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS)
|
68
|
-
errors_options = initialize_error_options(options,
|
44
|
+
errors_options = initialize_error_options(options, attachable)
|
69
45
|
|
70
46
|
if metadata[:width].to_i <= 0 || metadata[:height].to_i <= 0
|
71
47
|
errors_options[:aspect_ratio] = flat_options[:with]
|
@@ -1,26 +1,28 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'concerns/active_storageable.rb'
|
3
4
|
require_relative 'concerns/errorable.rb'
|
4
5
|
require_relative 'concerns/symbolizable.rb'
|
5
6
|
|
6
7
|
module ActiveStorageValidations
|
7
8
|
class AttachedValidator < ActiveModel::EachValidator # :nodoc:
|
9
|
+
include ActiveStorageable
|
8
10
|
include Errorable
|
9
11
|
include Symbolizable
|
10
12
|
|
11
13
|
ERROR_TYPES = %i[blank].freeze
|
12
14
|
|
13
15
|
def check_validity!
|
14
|
-
%i
|
16
|
+
%i[allow_nil allow_blank].each do |not_authorized_option|
|
15
17
|
if options.include?(not_authorized_option)
|
16
|
-
raise ArgumentError, "You cannot pass the :#{not_authorized_option} option to
|
18
|
+
raise ArgumentError, "You cannot pass the :#{not_authorized_option} option to the #{self.class.to_sym} validator"
|
17
19
|
end
|
18
20
|
end
|
19
21
|
end
|
20
22
|
|
21
23
|
def validate_each(record, attribute, _value)
|
22
|
-
return if record
|
23
|
-
|
24
|
+
return if attachments_present?(record, attribute) &&
|
25
|
+
will_have_attachments_after_save?(record, attribute)
|
24
26
|
|
25
27
|
errors_options = initialize_error_options(options)
|
26
28
|
add_error(record, attribute, ERROR_TYPES.first, **errors_options)
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'concerns/active_storageable.rb'
|
4
|
+
require_relative 'concerns/errorable.rb'
|
5
|
+
require_relative 'concerns/symbolizable.rb'
|
6
|
+
|
7
|
+
module ActiveStorageValidations
|
8
|
+
class BaseSizeValidator < ActiveModel::EachValidator # :nodoc:
|
9
|
+
include ActiveStorageable
|
10
|
+
include Errorable
|
11
|
+
include OptionProcUnfolding
|
12
|
+
include Symbolizable
|
13
|
+
|
14
|
+
delegate :number_to_human_size, to: ActiveSupport::NumberHelper
|
15
|
+
|
16
|
+
AVAILABLE_CHECKS = %i[
|
17
|
+
less_than
|
18
|
+
less_than_or_equal_to
|
19
|
+
greater_than
|
20
|
+
greater_than_or_equal_to
|
21
|
+
between
|
22
|
+
].freeze
|
23
|
+
|
24
|
+
def initialize(*args)
|
25
|
+
if self.class == BaseSizeValidator
|
26
|
+
raise NotImplementedError, 'BaseSizeValidator is an abstract class and cannot be instantiated directly.'
|
27
|
+
end
|
28
|
+
super
|
29
|
+
end
|
30
|
+
|
31
|
+
def check_validity!
|
32
|
+
unless AVAILABLE_CHECKS.one? { |argument| options.key?(argument) }
|
33
|
+
raise ArgumentError, 'You must pass either :less_than(_or_equal_to), :greater_than(_or_equal_to), or :between to the validator'
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def is_valid?(size, flat_options)
|
40
|
+
return false if size < 0
|
41
|
+
|
42
|
+
if flat_options[:between].present?
|
43
|
+
flat_options[:between].include?(size)
|
44
|
+
elsif flat_options[:less_than].present?
|
45
|
+
size < flat_options[:less_than]
|
46
|
+
elsif flat_options[:less_than_or_equal_to].present?
|
47
|
+
size <= flat_options[:less_than_or_equal_to]
|
48
|
+
elsif flat_options[:greater_than].present?
|
49
|
+
size > flat_options[:greater_than]
|
50
|
+
elsif flat_options[:greater_than_or_equal_to].present?
|
51
|
+
size >= flat_options[:greater_than_or_equal_to]
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def populate_error_options(errors_options, flat_options)
|
56
|
+
errors_options[:min_size] = number_to_human_size(min_size(flat_options))
|
57
|
+
errors_options[:max_size] = number_to_human_size(max_size(flat_options))
|
58
|
+
end
|
59
|
+
|
60
|
+
def min_size(flat_options)
|
61
|
+
flat_options[:between]&.min || flat_options[:greater_than] || flat_options[:greater_than_or_equal_to]
|
62
|
+
end
|
63
|
+
|
64
|
+
def max_size(flat_options)
|
65
|
+
flat_options[:between]&.max || flat_options[:less_than] || flat_options[:less_than_or_equal_to]
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module ActiveStorageValidations
|
2
|
+
# ActiveStorageValidations::ActiveStorageable
|
3
|
+
#
|
4
|
+
# Validator helper methods to make our code more explicit.
|
5
|
+
module ActiveStorageable
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
private
|
9
|
+
|
10
|
+
# Retrieve either an ActiveStorage::Attached::One or an
|
11
|
+
# ActiveStorage::Attached::Many instance depending on attribute definition
|
12
|
+
def attached_files(record, attribute)
|
13
|
+
Array.wrap(record.send(attribute))
|
14
|
+
end
|
15
|
+
|
16
|
+
def attachments_present?(record, attribute)
|
17
|
+
record.send(attribute).attached?
|
18
|
+
end
|
19
|
+
|
20
|
+
def no_attachments?(record, attribute)
|
21
|
+
!attachments_present?(record, attribute)
|
22
|
+
end
|
23
|
+
|
24
|
+
def will_have_attachments_after_save?(record, attribute)
|
25
|
+
!Array.wrap(record.send(attribute)).all?(&:marked_for_destruction?)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -9,19 +9,18 @@ module ActiveStorageValidations
|
|
9
9
|
active_storage_validations_options = {
|
10
10
|
validator_type: self.class.to_sym,
|
11
11
|
custom_message: (options[:message] if options[:message].present?),
|
12
|
-
filename: get_filename(file)
|
12
|
+
filename: (get_filename(file) unless self.class.to_sym == :total_size)
|
13
13
|
}.compact
|
14
14
|
|
15
15
|
curated_options.merge(active_storage_validations_options)
|
16
16
|
end
|
17
17
|
|
18
18
|
def add_error(record, attribute, error_type, **errors_options)
|
19
|
-
|
20
|
-
return if record.errors.added?(attribute, type)
|
19
|
+
return if record.errors.added?(attribute, error_type)
|
21
20
|
|
22
21
|
# You can read https://api.rubyonrails.org/classes/ActiveModel/Errors.html#method-i-add
|
23
22
|
# to better understand how Rails model errors work
|
24
|
-
record.errors.add(attribute,
|
23
|
+
record.errors.add(attribute, error_type, **errors_options)
|
25
24
|
end
|
26
25
|
|
27
26
|
private
|
@@ -30,7 +29,7 @@ module ActiveStorageValidations
|
|
30
29
|
return nil unless file
|
31
30
|
|
32
31
|
case file
|
33
|
-
when ActiveStorage::Attached then file.blob
|
32
|
+
when ActiveStorage::Attached, ActiveStorage::Attachment then file.blob&.filename&.to_s
|
34
33
|
when Hash then file[:filename]
|
35
34
|
end
|
36
35
|
end
|
@@ -0,0 +1,31 @@
|
|
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
|
@@ -0,0 +1,130 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'concerns/loggable'
|
4
|
+
require 'open3'
|
5
|
+
|
6
|
+
module ActiveStorageValidations
|
7
|
+
class ContentTypeSpoofDetector
|
8
|
+
class FileCommandLineToolNotInstalledError < StandardError; end
|
9
|
+
|
10
|
+
include Loggable
|
11
|
+
|
12
|
+
def initialize(record, attribute, file)
|
13
|
+
@record = record
|
14
|
+
@attribute = attribute
|
15
|
+
@file = file
|
16
|
+
end
|
17
|
+
|
18
|
+
def spoofed?
|
19
|
+
if supplied_content_type_vs_open3_analizer_mismatch?
|
20
|
+
logger.info "Content Type Spoofing detected for file '#{filename}'. The supplied content type is '#{supplied_content_type}' but the content type discovered using open3 is '#{content_type_from_analyzer}'."
|
21
|
+
true
|
22
|
+
else
|
23
|
+
false
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def filename
|
30
|
+
@filename ||= @file.blob.present? && @file.blob.filename.to_s
|
31
|
+
end
|
32
|
+
|
33
|
+
def supplied_content_type
|
34
|
+
# We remove potential mime type parameters
|
35
|
+
@supplied_content_type ||= @file.blob.present? && @file.blob.content_type.downcase.split(/[;,\s]/, 2).first
|
36
|
+
end
|
37
|
+
|
38
|
+
def io
|
39
|
+
@io ||= case @record.public_send(@attribute)
|
40
|
+
when ActiveStorage::Attached::One then get_io_from_one
|
41
|
+
when ActiveStorage::Attached::Many then get_io_from_many
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def get_io_from_one
|
46
|
+
attachable = @record.attachment_changes[@attribute.to_s].attachable
|
47
|
+
|
48
|
+
case attachable
|
49
|
+
when ActionDispatch::Http::UploadedFile
|
50
|
+
attachable.read
|
51
|
+
when String
|
52
|
+
blob = ActiveStorage::Blob.find_signed!(attachable)
|
53
|
+
blob.download
|
54
|
+
when ActiveStorage::Blob
|
55
|
+
attachable.download
|
56
|
+
when Hash
|
57
|
+
attachable[:io].read
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def get_io_from_many
|
62
|
+
attachables = @record.attachment_changes[@attribute.to_s].attachables
|
63
|
+
|
64
|
+
if attachables.all? { |attachable| attachable.is_a?(ActionDispatch::Http::UploadedFile) }
|
65
|
+
attachables.find do |uploaded_file|
|
66
|
+
checksum = ActiveStorage::Blob.new.send(:compute_checksum_in_chunks, uploaded_file)
|
67
|
+
checksum == @file.checksum
|
68
|
+
end.read
|
69
|
+
elsif attachables.all? { |attachable| attachable.is_a?(String) }
|
70
|
+
# It's only possible to pass one String as attachable (not an array of String)
|
71
|
+
blob = ActiveStorage::Blob.find_signed!(attachables.first)
|
72
|
+
blob.download
|
73
|
+
elsif attachables.all? { |attachable| attachable.is_a?(ActiveStorage::Blob) }
|
74
|
+
attachables.find { |blob| blob == @file.blob }.download
|
75
|
+
elsif attachables.all? { |attachable| attachable.is_a?(Hash) }
|
76
|
+
# It's only possible to pass one Hash as attachable (not an array of Hash)
|
77
|
+
attachables.first[:io].read
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
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
|
+
@content_type_from_analyzer ||= open3_mime_type_for_io
|
85
|
+
end
|
86
|
+
|
87
|
+
def open3_mime_type_for_io
|
88
|
+
return nil if io.blank?
|
89
|
+
|
90
|
+
Tempfile.create do |tempfile|
|
91
|
+
tempfile.binmode
|
92
|
+
tempfile.write(io)
|
93
|
+
tempfile.rewind
|
94
|
+
|
95
|
+
command = "file -b --mime-type #{tempfile.path}"
|
96
|
+
output, status = Open3.capture2(command)
|
97
|
+
|
98
|
+
if status.success?
|
99
|
+
mime_type = output.strip
|
100
|
+
return mime_type
|
101
|
+
else
|
102
|
+
raise "Error determining MIME type: #{output}"
|
103
|
+
end
|
104
|
+
|
105
|
+
rescue Errno::ENOENT
|
106
|
+
raise FileCommandLineToolNotInstalledError, 'file command-line tool is not installed'
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def supplied_content_type_vs_open3_analizer_mismatch?
|
111
|
+
supplied_content_type.present? &&
|
112
|
+
!supplied_content_type_intersects_content_type_from_analyzer?
|
113
|
+
end
|
114
|
+
|
115
|
+
def supplied_content_type_intersects_content_type_from_analyzer?
|
116
|
+
# Ruby intersects? method is only available from 3.1
|
117
|
+
enlarged_content_type(supplied_content_type).any? do |item|
|
118
|
+
enlarged_content_type(content_type_from_analyzer).include?(item)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def enlarged_content_type(content_type)
|
123
|
+
[content_type, *parent_content_types(content_type)].compact.uniq
|
124
|
+
end
|
125
|
+
|
126
|
+
def parent_content_types(content_type)
|
127
|
+
Marcel::TYPE_PARENTS[content_type] || []
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -1,16 +1,22 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'concerns/active_storageable.rb'
|
3
4
|
require_relative 'concerns/errorable.rb'
|
4
5
|
require_relative 'concerns/symbolizable.rb'
|
6
|
+
require_relative 'content_type_spoof_detector.rb'
|
5
7
|
|
6
8
|
module ActiveStorageValidations
|
7
9
|
class ContentTypeValidator < ActiveModel::EachValidator # :nodoc:
|
10
|
+
include ActiveStorageable
|
8
11
|
include OptionProcUnfolding
|
9
12
|
include Errorable
|
10
13
|
include Symbolizable
|
11
14
|
|
12
15
|
AVAILABLE_CHECKS = %i[with in].freeze
|
13
|
-
ERROR_TYPES = %i[
|
16
|
+
ERROR_TYPES = %i[
|
17
|
+
content_type_invalid
|
18
|
+
spoofed_content_type
|
19
|
+
].freeze
|
14
20
|
|
15
21
|
def check_validity!
|
16
22
|
ensure_exactly_one_validator_option
|
@@ -18,31 +24,24 @@ module ActiveStorageValidations
|
|
18
24
|
end
|
19
25
|
|
20
26
|
def validate_each(record, attribute, _value)
|
21
|
-
return
|
27
|
+
return if no_attachments?(record, attribute)
|
22
28
|
|
23
29
|
types = authorized_types(record)
|
24
|
-
return
|
30
|
+
return if types.empty?
|
25
31
|
|
26
|
-
|
27
|
-
|
28
|
-
files.each do |file|
|
29
|
-
next if is_valid?(file, types)
|
30
|
-
|
31
|
-
errors_options = initialize_error_options(options, file)
|
32
|
-
errors_options[:authorized_types] = types_to_human_format(types)
|
33
|
-
errors_options[:content_type] = content_type(file)
|
34
|
-
add_error(record, attribute, ERROR_TYPES.first, **errors_options)
|
35
|
-
break
|
32
|
+
attached_files(record, attribute).each do |file|
|
33
|
+
is_valid?(record, attribute, file, types)
|
36
34
|
end
|
37
35
|
end
|
38
36
|
|
37
|
+
private
|
38
|
+
|
39
39
|
def authorized_types(record)
|
40
40
|
flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS)
|
41
41
|
(Array.wrap(flat_options[:with]) + Array.wrap(flat_options[:in])).compact.map do |type|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
Marcel::MimeType.for(declared_type: type.to_s, extension: type.to_s)
|
42
|
+
case type
|
43
|
+
when String, Symbol then Marcel::MimeType.for(declared_type: type.to_s, extension: type.to_s)
|
44
|
+
when Regexp then type
|
46
45
|
end
|
47
46
|
end
|
48
47
|
end
|
@@ -54,13 +53,44 @@ module ActiveStorageValidations
|
|
54
53
|
end
|
55
54
|
|
56
55
|
def content_type(file)
|
57
|
-
|
56
|
+
# We remove potential mime type parameters
|
57
|
+
file.blob.present? && file.blob.content_type.downcase.split(/[;,\s]/, 2).first
|
58
|
+
end
|
59
|
+
|
60
|
+
def is_valid?(record, attribute, file, types)
|
61
|
+
file_type_in_authorized_types?(record, attribute, file, types) &&
|
62
|
+
not_spoofing_content_type?(record, attribute, file)
|
58
63
|
end
|
59
64
|
|
60
|
-
def
|
65
|
+
def file_type_in_authorized_types?(record, attribute, file, types)
|
61
66
|
file_type = content_type(file)
|
62
|
-
types.any? do |type|
|
63
|
-
|
67
|
+
file_type_is_authorized = types.any? do |type|
|
68
|
+
case type
|
69
|
+
when String then type == file_type
|
70
|
+
when Regexp then type.match?(file_type.to_s)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
if file_type_is_authorized
|
75
|
+
true
|
76
|
+
else
|
77
|
+
errors_options = initialize_error_options(options, file)
|
78
|
+
errors_options[:authorized_types] = types_to_human_format(types)
|
79
|
+
errors_options[:content_type] = content_type(file)
|
80
|
+
add_error(record, attribute, ERROR_TYPES.first, **errors_options)
|
81
|
+
false
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def not_spoofing_content_type?(record, attribute, file)
|
86
|
+
return true unless enable_spoofing_protection?
|
87
|
+
|
88
|
+
if ContentTypeSpoofDetector.new(record, attribute, file).spoofed?
|
89
|
+
errors_options = initialize_error_options(options, file)
|
90
|
+
add_error(record, attribute, ERROR_TYPES.second, **errors_options)
|
91
|
+
false
|
92
|
+
else
|
93
|
+
true
|
64
94
|
end
|
65
95
|
end
|
66
96
|
|
@@ -81,7 +111,7 @@ module ActiveStorageValidations
|
|
81
111
|
def invalid_content_type_message(content_type)
|
82
112
|
<<~ERROR_MESSAGE
|
83
113
|
You must pass valid content types to the validator
|
84
|
-
'#{content_type}' is not
|
114
|
+
'#{content_type}' is not found in Marcel::EXTENSIONS mimes
|
85
115
|
ERROR_MESSAGE
|
86
116
|
end
|
87
117
|
|
@@ -93,5 +123,9 @@ module ActiveStorageValidations
|
|
93
123
|
false # We always validate regexes
|
94
124
|
end
|
95
125
|
end
|
126
|
+
|
127
|
+
def enable_spoofing_protection?
|
128
|
+
options[:spoofing_protection] == true
|
129
|
+
end
|
96
130
|
end
|
97
131
|
end
|
@@ -1,13 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'concerns/active_storageable.rb'
|
3
4
|
require_relative 'concerns/errorable.rb'
|
5
|
+
require_relative 'concerns/metadatable.rb'
|
4
6
|
require_relative 'concerns/symbolizable.rb'
|
5
|
-
require_relative 'metadata.rb'
|
6
7
|
|
7
8
|
module ActiveStorageValidations
|
8
9
|
class DimensionValidator < ActiveModel::EachValidator # :nodoc
|
9
|
-
include
|
10
|
+
include ActiveStorageable
|
10
11
|
include Errorable
|
12
|
+
include OptionProcUnfolding
|
13
|
+
include Metadatable
|
11
14
|
include Symbolizable
|
12
15
|
|
13
16
|
AVAILABLE_CHECKS = %i[width height min max].freeze
|
@@ -25,65 +28,19 @@ module ActiveStorageValidations
|
|
25
28
|
dimension_height_equal_to
|
26
29
|
].freeze
|
27
30
|
|
28
|
-
def process_options(record)
|
29
|
-
flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS)
|
30
|
-
|
31
|
-
[:width, :height].each do |length|
|
32
|
-
if flat_options[length] and flat_options[length].is_a?(Hash)
|
33
|
-
if (range = flat_options[length][:in])
|
34
|
-
raise ArgumentError, ":in must be a Range" unless range.is_a?(Range)
|
35
|
-
flat_options[length][:min], flat_options[length][:max] = range.min, range.max
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|
39
|
-
[:min, :max].each do |dim|
|
40
|
-
if (range = flat_options[dim])
|
41
|
-
raise ArgumentError, ":#{dim} must be a Range (width..height)" unless range.is_a?(Range)
|
42
|
-
flat_options[:width] = { dim => range.first }
|
43
|
-
flat_options[:height] = { dim => range.last }
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
|
-
flat_options
|
48
|
-
end
|
49
|
-
|
50
31
|
def check_validity!
|
51
32
|
unless AVAILABLE_CHECKS.any? { |argument| options.key?(argument) }
|
52
33
|
raise ArgumentError, 'You must pass either :width, :height, :min or :max to the validator'
|
53
34
|
end
|
54
35
|
end
|
55
36
|
|
37
|
+
def validate_each(record, attribute, _value)
|
38
|
+
return if no_attachments?(record, attribute)
|
56
39
|
|
57
|
-
|
58
|
-
def validate_each(record, attribute, _value)
|
59
|
-
return true unless record.send(attribute).attached?
|
60
|
-
|
61
|
-
changes = record.attachment_changes[attribute.to_s]
|
62
|
-
return true if changes.blank?
|
63
|
-
|
64
|
-
files = Array.wrap(changes.is_a?(ActiveStorage::Attached::Changes::CreateMany) ? changes.attachables : changes.attachable)
|
65
|
-
files.each do |file|
|
66
|
-
metadata = Metadata.new(file).metadata
|
67
|
-
next if is_valid?(record, attribute, file, metadata)
|
68
|
-
break
|
69
|
-
end
|
70
|
-
end
|
71
|
-
else
|
72
|
-
# Rails 5
|
73
|
-
def validate_each(record, attribute, _value)
|
74
|
-
return true unless record.send(attribute).attached?
|
75
|
-
|
76
|
-
files = Array.wrap(record.send(attribute))
|
77
|
-
files.each do |file|
|
78
|
-
# Analyze file first if not analyzed to get all required metadata.
|
79
|
-
file.analyze; file.reload unless file.analyzed?
|
80
|
-
metadata = file.metadata rescue {}
|
81
|
-
next if is_valid?(record, attribute, file, metadata)
|
82
|
-
break
|
83
|
-
end
|
84
|
-
end
|
40
|
+
validate_changed_files_from_metadata(record, attribute)
|
85
41
|
end
|
86
42
|
|
43
|
+
private
|
87
44
|
|
88
45
|
def is_valid?(record, attribute, file, metadata)
|
89
46
|
flat_options = process_options(record)
|
@@ -163,5 +120,27 @@ module ActiveStorageValidations
|
|
163
120
|
|
164
121
|
true # valid file
|
165
122
|
end
|
123
|
+
|
124
|
+
def process_options(record)
|
125
|
+
flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS)
|
126
|
+
|
127
|
+
[:width, :height].each do |length|
|
128
|
+
if flat_options[length] and flat_options[length].is_a?(Hash)
|
129
|
+
if (range = flat_options[length][:in])
|
130
|
+
raise ArgumentError, ":in must be a Range" unless range.is_a?(Range)
|
131
|
+
flat_options[length][:min], flat_options[length][:max] = range.min, range.max
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
[:min, :max].each do |dim|
|
136
|
+
if (range = flat_options[dim])
|
137
|
+
raise ArgumentError, ":#{dim} must be a Range (width..height)" unless range.is_a?(Range)
|
138
|
+
flat_options[:width] = { dim => range.first }
|
139
|
+
flat_options[:height] = { dim => range.last }
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
flat_options
|
144
|
+
end
|
166
145
|
end
|
167
146
|
end
|
@@ -1,10 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'concerns/active_storageable.rb'
|
3
4
|
require_relative 'concerns/errorable.rb'
|
4
5
|
require_relative 'concerns/symbolizable.rb'
|
5
6
|
|
6
7
|
module ActiveStorageValidations
|
7
8
|
class LimitValidator < ActiveModel::EachValidator # :nodoc:
|
9
|
+
include ActiveStorageable
|
8
10
|
include OptionProcUnfolding
|
9
11
|
include Errorable
|
10
12
|
include Symbolizable
|
@@ -19,11 +21,11 @@ module ActiveStorageValidations
|
|
19
21
|
ensure_arguments_validity
|
20
22
|
end
|
21
23
|
|
22
|
-
def validate_each(record, attribute,
|
23
|
-
files =
|
24
|
+
def validate_each(record, attribute, _value)
|
25
|
+
files = attached_files(record, attribute).reject(&:blank?)
|
24
26
|
flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS)
|
25
27
|
|
26
|
-
return
|
28
|
+
return if files_count_valid?(files.count, flat_options)
|
27
29
|
|
28
30
|
errors_options = initialize_error_options(options)
|
29
31
|
errors_options[:min] = flat_options[:min]
|
@@ -0,0 +1,5 @@
|
|
1
|
+
Marcel::MimeType.extend "application/x-rar-compressed", parents: %(application/x-rar)
|
2
|
+
Marcel::MimeType.extend "audio/x-hx-aac-adts", parents: %(audio/x-aac)
|
3
|
+
Marcel::MimeType.extend "audio/x-m4a", parents: %(audio/mp4)
|
4
|
+
Marcel::MimeType.extend "text/xml", parents: %(application/xml) # alias
|
5
|
+
Marcel::MimeType.extend "video/theora", parents: %(video/ogg)
|