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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +84 -33
  3. data/config/locales/da.yml +33 -0
  4. data/config/locales/de.yml +6 -0
  5. data/config/locales/en.yml +6 -0
  6. data/config/locales/es.yml +6 -0
  7. data/config/locales/fr.yml +6 -0
  8. data/config/locales/it.yml +6 -0
  9. data/config/locales/ja.yml +6 -0
  10. data/config/locales/nl.yml +6 -0
  11. data/config/locales/pl.yml +6 -0
  12. data/config/locales/pt-BR.yml +6 -0
  13. data/config/locales/ru.yml +6 -0
  14. data/config/locales/sv.yml +11 -1
  15. data/config/locales/tr.yml +6 -0
  16. data/config/locales/uk.yml +6 -0
  17. data/config/locales/vi.yml +6 -0
  18. data/config/locales/zh-CN.yml +6 -0
  19. data/lib/active_storage_validations/aspect_ratio_validator.rb +10 -34
  20. data/lib/active_storage_validations/attached_validator.rb +6 -4
  21. data/lib/active_storage_validations/base_size_validator.rb +68 -0
  22. data/lib/active_storage_validations/concerns/active_storageable.rb +28 -0
  23. data/lib/active_storage_validations/concerns/errorable.rb +4 -5
  24. data/lib/active_storage_validations/concerns/loggable.rb +9 -0
  25. data/lib/active_storage_validations/concerns/metadatable.rb +31 -0
  26. data/lib/active_storage_validations/content_type_spoof_detector.rb +130 -0
  27. data/lib/active_storage_validations/content_type_validator.rb +56 -22
  28. data/lib/active_storage_validations/dimension_validator.rb +31 -52
  29. data/lib/active_storage_validations/limit_validator.rb +5 -3
  30. data/lib/active_storage_validations/marcel_extensor.rb +5 -0
  31. data/lib/active_storage_validations/matchers/aspect_ratio_validator_matcher.rb +6 -15
  32. data/lib/active_storage_validations/matchers/attached_validator_matcher.rb +5 -13
  33. data/lib/active_storage_validations/matchers/base_size_validator_matcher.rb +134 -0
  34. data/lib/active_storage_validations/matchers/concerns/attachable.rb +66 -0
  35. data/lib/active_storage_validations/matchers/concerns/contextable.rb +20 -8
  36. data/lib/active_storage_validations/matchers/concerns/messageable.rb +1 -1
  37. data/lib/active_storage_validations/matchers/concerns/validatable.rb +9 -3
  38. data/lib/active_storage_validations/matchers/content_type_validator_matcher.rb +12 -2
  39. data/lib/active_storage_validations/matchers/dimension_validator_matcher.rb +6 -15
  40. data/lib/active_storage_validations/matchers/limit_validator_matcher.rb +127 -0
  41. data/lib/active_storage_validations/matchers/processable_image_validator_matcher.rb +78 -0
  42. data/lib/active_storage_validations/matchers/size_validator_matcher.rb +4 -139
  43. data/lib/active_storage_validations/matchers/total_size_validator_matcher.rb +31 -0
  44. data/lib/active_storage_validations/matchers.rb +6 -15
  45. data/lib/active_storage_validations/metadata.rb +27 -12
  46. data/lib/active_storage_validations/processable_image_validator.rb +17 -32
  47. data/lib/active_storage_validations/size_validator.rb +6 -55
  48. data/lib/active_storage_validations/total_size_validator.rb +45 -0
  49. data/lib/active_storage_validations/version.rb +1 -1
  50. data/lib/active_storage_validations.rb +4 -1
  51. 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 OptionProcUnfolding
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
- if Rails.gem_version >= Gem::Version.new('6.0.0')
32
- def validate_each(record, attribute, _value)
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
- files = Array.wrap(changes.is_a?(ActiveStorage::Attached::Changes::CreateMany) ? changes.attachables : changes.attachable)
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, file, metadata)
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, file)
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(allow_nil allow_blank).each do |not_authorized_option|
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 this validator"
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.send(attribute).attached? &&
23
- !Array.wrap(record.send(attribute)).all? { |file| file.marked_for_destruction? }
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
- type = errors_options[:custom_message].presence || error_type
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, type, **errors_options)
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.filename.to_s
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,9 @@
1
+ module ActiveStorageValidations
2
+ module Loggable
3
+ extend ActiveSupport::Concern
4
+
5
+ def logger
6
+ Rails.logger
7
+ end
8
+ end
9
+ 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[content_type_invalid].freeze
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 true unless record.send(attribute).attached?
27
+ return if no_attachments?(record, attribute)
22
28
 
23
29
  types = authorized_types(record)
24
- return true if types.empty?
30
+ return if types.empty?
25
31
 
26
- files = Array.wrap(record.send(attribute))
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
- if type.is_a?(Regexp)
43
- type
44
- else
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
- file.blob.present? && file.blob.content_type
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 is_valid?(file, types)
65
+ def file_type_in_authorized_types?(record, attribute, file, types)
61
66
  file_type = content_type(file)
62
- types.any? do |type|
63
- type == file_type || (type.is_a?(Regexp) && type.match?(file_type.to_s))
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 find in Marcel::EXTENSIONS mimes
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 OptionProcUnfolding
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
- if Rails.gem_version >= Gem::Version.new('6.0.0')
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 = Array.wrap(record.send(attribute)).reject { |file| file.blank? }.compact.uniq
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 true if files_count_valid?(files.count, flat_options)
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)