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
@@ -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
|
@@ -16,12 +16,11 @@ module ActiveStorageValidations
|
|
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
|
@@ -31,8 +30,9 @@ module ActiveStorageValidations
|
|
31
30
|
|
32
31
|
case file
|
33
32
|
when ActiveStorage::Attached, ActiveStorage::Attachment then file.blob&.filename&.to_s
|
33
|
+
when ActiveStorage::Blob then file.filename
|
34
34
|
when Hash then file[:filename]
|
35
|
-
end
|
35
|
+
end.to_s
|
36
36
|
end
|
37
37
|
end
|
38
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
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'concerns/attachable'
|
4
|
+
require_relative 'concerns/loggable'
|
5
|
+
require 'open3'
|
6
|
+
|
7
|
+
module ActiveStorageValidations
|
8
|
+
class ContentTypeSpoofDetector
|
9
|
+
class FileCommandLineToolNotInstalledError < StandardError; end
|
10
|
+
|
11
|
+
include Attachable
|
12
|
+
include Loggable
|
13
|
+
|
14
|
+
def initialize(record, attribute, attachable)
|
15
|
+
@record = record
|
16
|
+
@attribute = attribute
|
17
|
+
@attachable = attachable
|
18
|
+
end
|
19
|
+
|
20
|
+
def spoofed?
|
21
|
+
if supplied_content_type_vs_open3_analizer_mismatch?
|
22
|
+
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}'."
|
23
|
+
true
|
24
|
+
else
|
25
|
+
false
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def filename
|
32
|
+
@filename ||= attachable_filename(@attachable).to_s
|
33
|
+
end
|
34
|
+
|
35
|
+
def supplied_content_type
|
36
|
+
@supplied_content_type ||= attachable_content_type(@attachable)
|
37
|
+
end
|
38
|
+
|
39
|
+
def io
|
40
|
+
@io ||= attachable_io(@attachable)
|
41
|
+
end
|
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.
|
47
|
+
def content_type_from_analyzer
|
48
|
+
@content_type_from_analyzer ||= open3_mime_type_for_io
|
49
|
+
end
|
50
|
+
|
51
|
+
def open3_mime_type_for_io
|
52
|
+
return nil if io.blank?
|
53
|
+
|
54
|
+
Tempfile.create do |tempfile|
|
55
|
+
tempfile.binmode
|
56
|
+
tempfile.write(io)
|
57
|
+
tempfile.rewind
|
58
|
+
|
59
|
+
command = "file -b --mime-type #{tempfile.path}"
|
60
|
+
output, status = Open3.capture2(command)
|
61
|
+
|
62
|
+
if status.success?
|
63
|
+
mime_type = output.strip
|
64
|
+
return mime_type
|
65
|
+
else
|
66
|
+
raise "Error determining MIME type: #{output}"
|
67
|
+
end
|
68
|
+
|
69
|
+
rescue Errno::ENOENT
|
70
|
+
raise FileCommandLineToolNotInstalledError, 'file command-line tool is not installed'
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def supplied_content_type_vs_open3_analizer_mismatch?
|
75
|
+
supplied_content_type.present? &&
|
76
|
+
!supplied_content_type_intersects_content_type_from_analyzer?
|
77
|
+
end
|
78
|
+
|
79
|
+
def supplied_content_type_intersects_content_type_from_analyzer?
|
80
|
+
# Ruby intersects? method is only available from 3.1
|
81
|
+
enlarged_content_type(supplied_content_type).any? do |item|
|
82
|
+
enlarged_content_type(content_type_from_analyzer).include?(item)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def enlarged_content_type(content_type)
|
87
|
+
[content_type, *parent_content_types(content_type)].compact.uniq
|
88
|
+
end
|
89
|
+
|
90
|
+
def parent_content_types(content_type)
|
91
|
+
Marcel::TYPE_PARENTS[content_type] || []
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -1,16 +1,25 @@
|
|
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'
|
6
|
+
require_relative 'concerns/optionable.rb'
|
4
7
|
require_relative 'concerns/symbolizable.rb'
|
8
|
+
require_relative 'content_type_spoof_detector.rb'
|
5
9
|
|
6
10
|
module ActiveStorageValidations
|
7
11
|
class ContentTypeValidator < ActiveModel::EachValidator # :nodoc:
|
8
|
-
include
|
12
|
+
include ActiveStorageable
|
13
|
+
include Attachable
|
9
14
|
include Errorable
|
15
|
+
include Optionable
|
10
16
|
include Symbolizable
|
11
17
|
|
12
18
|
AVAILABLE_CHECKS = %i[with in].freeze
|
13
|
-
ERROR_TYPES = %i[
|
19
|
+
ERROR_TYPES = %i[
|
20
|
+
content_type_invalid
|
21
|
+
spoofed_content_type
|
22
|
+
].freeze
|
14
23
|
|
15
24
|
def check_validity!
|
16
25
|
ensure_exactly_one_validator_option
|
@@ -18,52 +27,102 @@ module ActiveStorageValidations
|
|
18
27
|
end
|
19
28
|
|
20
29
|
def validate_each(record, attribute, _value)
|
21
|
-
return
|
30
|
+
return if no_attachments?(record, attribute)
|
22
31
|
|
23
|
-
|
24
|
-
return
|
32
|
+
@authorized_content_types = authorized_content_types_from_options(record)
|
33
|
+
return if @authorized_content_types.empty?
|
25
34
|
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|
35
|
+
attachables_from_changes(record, attribute).each do |attachable|
|
36
|
+
set_attachable_cached_values(attachable)
|
37
|
+
is_valid?(record, attribute, attachable)
|
36
38
|
end
|
37
39
|
end
|
38
40
|
|
39
|
-
|
40
|
-
|
41
|
+
private
|
42
|
+
|
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
|
-
|
43
|
-
|
44
|
-
|
45
|
-
Marcel::MimeType.for(declared_type: type.to_s, extension: type.to_s)
|
47
|
+
case type
|
48
|
+
when String, Symbol then Marcel::MimeType.for(declared_type: type.to_s, extension: type.to_s)
|
49
|
+
when Regexp then type
|
46
50
|
end
|
47
51
|
end
|
48
52
|
end
|
49
53
|
|
50
|
-
def
|
51
|
-
|
52
|
-
|
53
|
-
.join(', ')
|
54
|
+
def set_attachable_cached_values(attachable)
|
55
|
+
@attachable_content_type = attachable_content_type(attachable)
|
56
|
+
@attachable_filename = attachable_filename(attachable).to_s
|
54
57
|
end
|
55
58
|
|
56
|
-
def
|
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)
|
63
|
+
end
|
64
|
+
|
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
|
58
74
|
end
|
59
75
|
|
60
|
-
def
|
61
|
-
|
62
|
-
|
63
|
-
|
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)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
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
|
89
|
+
end
|
90
|
+
|
91
|
+
def not_spoofing_content_type?(record, attribute, attachable)
|
92
|
+
return true unless enable_spoofing_protection?
|
93
|
+
|
94
|
+
if ContentTypeSpoofDetector.new(record, attribute, attachable).spoofed?
|
95
|
+
errors_options = initialize_error_options(options, attachable)
|
96
|
+
add_error(record, attribute, ERROR_TYPES.second, **errors_options)
|
97
|
+
false
|
98
|
+
else
|
99
|
+
true
|
64
100
|
end
|
65
101
|
end
|
66
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
|
+
|
67
126
|
def ensure_exactly_one_validator_option
|
68
127
|
unless AVAILABLE_CHECKS.one? { |argument| options.key?(argument) }
|
69
128
|
raise ArgumentError, 'You must pass either :with or :in to the validator'
|
@@ -74,24 +133,39 @@ module ActiveStorageValidations
|
|
74
133
|
return true if options[:with]&.is_a?(Proc) || options[:in]&.is_a?(Proc)
|
75
134
|
|
76
135
|
([options[:with]] || options[:in]).each do |content_type|
|
77
|
-
raise ArgumentError,
|
136
|
+
raise ArgumentError, invalid_content_type_option_message(content_type) if invalid_option?(content_type)
|
78
137
|
end
|
79
138
|
end
|
80
139
|
|
81
|
-
def
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
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
|
86
152
|
end
|
87
153
|
|
88
|
-
def
|
154
|
+
def invalid_option?(content_type)
|
89
155
|
case content_type
|
90
156
|
when String, Symbol
|
91
|
-
|
157
|
+
content_type.to_s.match?(/\//) ? invalid_content_type?(content_type) : invalid_extension?(content_type)
|
92
158
|
when Regexp
|
93
159
|
false # We always validate regexes
|
94
160
|
end
|
95
161
|
end
|
162
|
+
|
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'
|
169
|
+
end
|
96
170
|
end
|
97
171
|
end
|
@@ -1,13 +1,17 @@
|
|
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'
|
6
|
+
require_relative 'concerns/optionable.rb'
|
4
7
|
require_relative 'concerns/symbolizable.rb'
|
5
|
-
require_relative 'metadata.rb'
|
6
8
|
|
7
9
|
module ActiveStorageValidations
|
8
10
|
class DimensionValidator < ActiveModel::EachValidator # :nodoc
|
9
|
-
include
|
11
|
+
include ActiveStorageable
|
12
|
+
include Attachable
|
10
13
|
include Errorable
|
14
|
+
include Optionable
|
11
15
|
include Symbolizable
|
12
16
|
|
13
17
|
AVAILABLE_CHECKS = %i[width height min max].freeze
|
@@ -25,65 +29,19 @@ module ActiveStorageValidations
|
|
25
29
|
dimension_height_equal_to
|
26
30
|
].freeze
|
27
31
|
|
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
32
|
def check_validity!
|
51
33
|
unless AVAILABLE_CHECKS.any? { |argument| options.key?(argument) }
|
52
34
|
raise ArgumentError, 'You must pass either :width, :height, :min or :max to the validator'
|
53
35
|
end
|
54
36
|
end
|
55
37
|
|
38
|
+
def validate_each(record, attribute, _value)
|
39
|
+
return if no_attachments?(record, attribute)
|
56
40
|
|
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
|
41
|
+
validate_changed_files_from_metadata(record, attribute)
|
85
42
|
end
|
86
43
|
|
44
|
+
private
|
87
45
|
|
88
46
|
def is_valid?(record, attribute, file, metadata)
|
89
47
|
flat_options = process_options(record)
|
@@ -163,5 +121,27 @@ module ActiveStorageValidations
|
|
163
121
|
|
164
122
|
true # valid file
|
165
123
|
end
|
124
|
+
|
125
|
+
def process_options(record)
|
126
|
+
flat_options = set_flat_options(record)
|
127
|
+
|
128
|
+
[:width, :height].each do |length|
|
129
|
+
if flat_options[length] and flat_options[length].is_a?(Hash)
|
130
|
+
if (range = flat_options[length][:in])
|
131
|
+
raise ArgumentError, ":in must be a Range" unless range.is_a?(Range)
|
132
|
+
flat_options[length][:min], flat_options[length][:max] = range.min, range.max
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
[:min, :max].each do |dim|
|
137
|
+
if (range = flat_options[dim])
|
138
|
+
raise ArgumentError, ":#{dim} must be a Range (width..height)" unless range.is_a?(Range)
|
139
|
+
flat_options[:width] = { dim => range.first }
|
140
|
+
flat_options[:height] = { dim => range.last }
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
flat_options
|
145
|
+
end
|
166
146
|
end
|
167
147
|
end
|
@@ -1,12 +1,15 @@
|
|
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/optionable.rb'
|
4
6
|
require_relative 'concerns/symbolizable.rb'
|
5
7
|
|
6
8
|
module ActiveStorageValidations
|
7
9
|
class LimitValidator < ActiveModel::EachValidator # :nodoc:
|
8
|
-
include
|
10
|
+
include ActiveStorageable
|
9
11
|
include Errorable
|
12
|
+
include Optionable
|
10
13
|
include Symbolizable
|
11
14
|
|
12
15
|
AVAILABLE_CHECKS = %i[max min].freeze
|
@@ -19,11 +22,11 @@ module ActiveStorageValidations
|
|
19
22
|
ensure_arguments_validity
|
20
23
|
end
|
21
24
|
|
22
|
-
def validate_each(record, attribute,
|
23
|
-
files =
|
24
|
-
flat_options =
|
25
|
+
def validate_each(record, attribute, _value)
|
26
|
+
files = attached_files(record, attribute).reject(&:blank?)
|
27
|
+
flat_options = set_flat_options(record)
|
25
28
|
|
26
|
-
return
|
29
|
+
return if files_count_valid?(files.count, flat_options)
|
27
30
|
|
28
31
|
errors_options = initialize_error_options(options)
|
29
32
|
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)
|