active_storage_validations 3.0.6 → 4.0.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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/MIT-LICENSE +1 -1
  3. data/README.md +178 -115
  4. data/Rakefile +13 -15
  5. data/lib/active_storage_validations/analyzer/audio_analyzer.rb +0 -1
  6. data/lib/active_storage_validations/analyzer/content_type_analyzer/file.rb +31 -0
  7. data/lib/active_storage_validations/analyzer/content_type_analyzer/magika.rb +44 -0
  8. data/lib/active_storage_validations/analyzer/content_type_analyzer.rb +15 -24
  9. data/lib/active_storage_validations/analyzer/image_analyzer/image_magick.rb +41 -7
  10. data/lib/active_storage_validations/analyzer/image_analyzer/vips.rb +36 -10
  11. data/lib/active_storage_validations/analyzer/pdf_analyzer.rb +3 -9
  12. data/lib/active_storage_validations/analyzer/shared/asv_ff_probable.rb +5 -4
  13. data/lib/active_storage_validations/analyzer/video_analyzer.rb +0 -1
  14. data/lib/active_storage_validations/analyzer.rb +35 -3
  15. data/lib/active_storage_validations/content_type_validator.rb +24 -6
  16. data/lib/active_storage_validations/dimension_validator.rb +4 -2
  17. data/lib/active_storage_validations/form_builder.rb +43 -0
  18. data/lib/active_storage_validations/matchers/aspect_ratio_validator_matcher.rb +8 -0
  19. data/lib/active_storage_validations/matchers/attached_validator_matcher.rb +4 -0
  20. data/lib/active_storage_validations/matchers/base_comparison_validator_matcher.rb +11 -1
  21. data/lib/active_storage_validations/matchers/content_type_validator_matcher.rb +12 -0
  22. data/lib/active_storage_validations/matchers/dimension_validator_matcher.rb +8 -0
  23. data/lib/active_storage_validations/matchers/duration_validator_matcher.rb +8 -0
  24. data/lib/active_storage_validations/matchers/limit_validator_matcher.rb +4 -0
  25. data/lib/active_storage_validations/matchers/pages_validator_matcher.rb +8 -0
  26. data/lib/active_storage_validations/matchers/processable_file_validator_matcher.rb +8 -0
  27. data/lib/active_storage_validations/matchers/shared/asv_except_onable.rb +57 -0
  28. data/lib/active_storage_validations/matchers/shared/asv_spoofing_protectable.rb +44 -0
  29. data/lib/active_storage_validations/matchers/shared/asv_timeoutable.rb +32 -0
  30. data/lib/active_storage_validations/matchers.rb +9 -1
  31. data/lib/active_storage_validations/railtie.rb +6 -0
  32. data/lib/active_storage_validations/shared/asv_analyzable.rb +66 -8
  33. data/lib/active_storage_validations/shared/asv_attachable.rb +2 -9
  34. data/lib/active_storage_validations/shared/asv_commandable.rb +95 -0
  35. data/lib/active_storage_validations/shared/asv_optionable.rb +5 -1
  36. data/lib/active_storage_validations/version.rb +1 -1
  37. data/lib/active_storage_validations.rb +22 -0
  38. metadata +25 -72
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorageValidations
4
+ # Content-type sniffer using the UNIX {file}[https://en.wikipedia.org/wiki/File_(command)] command.
5
+ #
6
+ # Override the binary path with +ActiveStorage.paths[:file]+.
7
+ class Analyzer::ContentTypeAnalyzer::File < Analyzer::ContentTypeAnalyzer
8
+ class CommandLineToolNotInstalledError < StandardError; end
9
+
10
+ private
11
+
12
+ def backend
13
+ :file
14
+ end
15
+
16
+ def media_from_path(path)
17
+ instrument("file") do |payload|
18
+ result = run_command(file_path, "-b", "--mime-type", path, payload: payload)
19
+ result.success? ? result.stdout.strip : nil
20
+ end
21
+ end
22
+
23
+ def file_path
24
+ ActiveStorage.paths[:file] || "file"
25
+ end
26
+
27
+ def missing_backend_error
28
+ CommandLineToolNotInstalledError.new("file command-line tool is not installed")
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module ActiveStorageValidations
6
+ # Content-type sniffer using the Google {Magika}[https://github.com/google/magika] CLI.
7
+ #
8
+ # Override the binary path with +ActiveStorage.paths[:magika]+.
9
+ class Analyzer::ContentTypeAnalyzer::Magika < Analyzer::ContentTypeAnalyzer
10
+ class CommandLineToolNotInstalledError < StandardError; end
11
+
12
+ private
13
+
14
+ def backend
15
+ :magika
16
+ end
17
+
18
+ def media_from_path(path)
19
+ instrument("magika") do |payload|
20
+ result = run_command(magika_path, "--json", path, payload: payload)
21
+ result.success? ? mime_type_from_magika_json(result.stdout) : nil
22
+ end
23
+ end
24
+
25
+ def mime_type_from_magika_json(stdout)
26
+ parsed = JSON.parse(stdout)
27
+ entry = parsed.is_a?(Array) ? parsed.first : parsed
28
+ return nil unless entry.is_a?(Hash)
29
+ return nil unless entry.dig("result", "status") == "ok"
30
+
31
+ entry.dig("result", "value", "output", "mime_type").presence
32
+ rescue JSON::ParserError
33
+ nil
34
+ end
35
+
36
+ def magika_path
37
+ ActiveStorage.paths[:magika] || "magika"
38
+ end
39
+
40
+ def missing_backend_error
41
+ CommandLineToolNotInstalledError.new("magika command-line tool is not installed")
42
+ end
43
+ end
44
+ end
@@ -1,26 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "open3"
4
-
5
3
  module ActiveStorageValidations
6
4
  # = ActiveStorageValidations ContentType \Analyzer
7
5
  #
8
- # Extracts the content type from an attachable. This is used to prevent content
9
- # type spoofing.
10
- #
11
- # Example:
6
+ # Abstract base for content-type sniffers used by +spoofing_protection+.
7
+ # Concrete backends:
12
8
  #
13
- # ActiveStorageValidations::Analyzer::ContentTypeAnalyzer.new(attachable).content_type
14
- # # => { content_type: "image/png" }
9
+ # ActiveStorageValidations::Analyzer::ContentTypeAnalyzer::File.new(attachable).content_type
10
+ # # => { content_type: "image/png", content_type_backend: "file" }
15
11
  #
16
- # This analyzer requires the {UNIX file}[https://en.wikipedia.org/wiki/File_(command)] command, which is not provided by \Rails. While it is available on most UNIX distributions, it may need to be installed explicitly on minimal or custom setups.
12
+ # ActiveStorageValidations::Analyzer::ContentTypeAnalyzer::Magika.new(attachable).content_type
13
+ # # => { content_type: "image/png", content_type_backend: "magika" }
17
14
  class Analyzer::ContentTypeAnalyzer < Analyzer
18
- class FileCommandLineToolNotInstalledError < StandardError; end
19
-
20
15
  def content_type
21
16
  read_media do |media|
22
17
  {
23
- content_type: media
18
+ content_type: media,
19
+ content_type_backend: backend.to_s
24
20
  }
25
21
  end
26
22
  end
@@ -33,7 +29,7 @@ module ActiveStorageValidations
33
29
  if media(tempfile).present?
34
30
  yield media(tempfile)
35
31
  else
36
- logger.info "Skipping file content_type analysis because Linux file command doesn't support the file"
32
+ logger.info "Skipping content_type analysis because #{backend} doesn't support the file"
37
33
  nil
38
34
  end
39
35
  ensure
@@ -41,20 +37,15 @@ module ActiveStorageValidations
41
37
  end
42
38
  end
43
39
  rescue Errno::ENOENT
44
- raise FileCommandLineToolNotInstalledError, "file command-line tool is not installed"
40
+ raise missing_backend_error
45
41
  end
46
42
 
47
- def media_from_path(path)
48
- instrument("file") do
49
- stdout, status = Open3.capture2(
50
- "file",
51
- "-b",
52
- "--mime-type",
53
- path
54
- )
43
+ def backend
44
+ raise NotImplementedError
45
+ end
55
46
 
56
- status.success? ? stdout.strip : nil
57
- end
47
+ def missing_backend_error
48
+ raise NotImplementedError
58
49
  end
59
50
  end
60
51
  end
@@ -1,17 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveStorageValidations
4
- # This analyzer relies on the third-party {MiniMagick}[https://github.com/minimagick/minimagick] gem.
4
+ # This analyzer relies on the third-party {MiniMagick}[https://github.com/minimagick/minimagick] gem
5
+ # only to resolve the ImageMagick / GraphicsMagick +identify+ argv. Commands are executed through
6
+ # {ASVCommandable#run_command} so +command_timeout+ can kill hung processes.
5
7
  # MiniMagick requires the {ImageMagick}[http://www.imagemagick.org] system library.
6
8
  # This is the default Rails image analyzer.
7
9
  class Analyzer::ImageAnalyzer::ImageMagick < Analyzer::ImageAnalyzer
10
+ ImageInfo = Struct.new(:width, :height, :orientation, keyword_init: true) do
11
+ def valid?
12
+ width.to_i.positive? && height.to_i.positive?
13
+ end
14
+
15
+ def [](key)
16
+ orientation if key == "%[orientation]"
17
+ end
18
+ end
19
+
8
20
  private
9
21
 
10
22
  def read_media
11
23
  Tempfile.create(binmode: true) do |tempfile|
12
24
  begin
13
- if media(tempfile).valid?
14
- yield media(tempfile)
25
+ info = media(tempfile)
26
+ if info&.valid?
27
+ yield info
15
28
  else
16
29
  logger.info "Skipping image analysis because ImageMagick doesn't support the file"
17
30
  {}
@@ -20,17 +33,38 @@ module ActiveStorageValidations
20
33
  tempfile.close
21
34
  end
22
35
  end
23
- rescue MiniMagick::Error => error
24
- logger.error "Skipping image analysis due to an ImageMagick error: #{error.message}"
36
+ rescue Errno::ENOENT
37
+ logger.info "Skipping image analysis because ImageMagick isn't installed"
25
38
  {}
26
39
  end
27
40
 
28
41
  def media_from_path(path)
29
- instrument("mini_magick") do
30
- MiniMagick::Image.new(path)
42
+ instrument("mini_magick") do |payload|
43
+ result = run_command(*identify_command(path), payload: payload)
44
+ return nil unless result.success?
45
+
46
+ parse_identify_output(result.stdout)
31
47
  end
32
48
  end
33
49
 
50
+ def identify_command(path)
51
+ require "mini_magick"
52
+
53
+ tool = ::MiniMagick::Tool.new("identify")
54
+ tool.format("%w\t%h\t%[orientation]")
55
+ tool << path
56
+ tool.command
57
+ end
58
+
59
+ def parse_identify_output(stdout)
60
+ width, height, orientation = stdout.to_s.strip.split("\t", 3)
61
+ ImageInfo.new(
62
+ width: width.to_i,
63
+ height: height.to_i,
64
+ orientation: orientation.to_s
65
+ )
66
+ end
67
+
34
68
  def rotated_image?(image)
35
69
  %w[ RightTop LeftBottom TopRight BottomLeft ].include?(image["%[orientation]"])
36
70
  end
@@ -16,6 +16,9 @@ module ActiveStorageValidations
16
16
  {}
17
17
  end
18
18
  ensure
19
+ # Best-effort: avoid unlinking the tempfile while a timed-out FFI load
20
+ # may still be using the path.
21
+ wait_for_vips_load_thread
19
22
  tempfile.close
20
23
  end
21
24
  end
@@ -25,19 +28,42 @@ module ActiveStorageValidations
25
28
  end
26
29
 
27
30
  def media_from_path(path)
28
- instrument("vips") do
29
- begin
30
- ::Vips::Image.new_from_file(path, access: :sequential)
31
- rescue ::Vips::Error
32
- # Vips throw errors rather than returning false when reading a not
33
- # supported attachable.
34
- # We stumbled upon this issue while reading 0 byte size attachable
35
- # https://github.com/janko/image_processing/issues/97
36
- nil
37
- end
31
+ instrument("vips") do |payload|
32
+ load_vips_image(path, payload)
33
+ end
34
+ end
35
+
36
+ def load_vips_image(path, payload)
37
+ timeout = timeout_in_seconds
38
+ return open_vips_image(path) if timeout.nil?
39
+
40
+ result = nil
41
+ @vips_load_thread = Thread.new { result = open_vips_image(path) }
42
+
43
+ if @vips_load_thread.join(timeout)
44
+ result
45
+ else
46
+ # libvips work may continue in the background until the C call returns.
47
+ mark_timed_out!(payload, "vips")
48
+ nil
38
49
  end
39
50
  end
40
51
 
52
+ def wait_for_vips_load_thread
53
+ return unless @vips_load_thread&.alive?
54
+
55
+ @vips_load_thread.join(0.5)
56
+ end
57
+
58
+ # Returns nil for unsupported / empty files instead of raising.
59
+ # Vips raises on unreadable input (e.g. 0-byte files) rather than returning
60
+ # a falsy image — see https://github.com/janko/image_processing/issues/97
61
+ def open_vips_image(path)
62
+ ::Vips::Image.new_from_file(path, access: :sequential)
63
+ rescue ::Vips::Error
64
+ nil
65
+ end
66
+
41
67
  ROTATIONS = /Right-top|Left-bottom|Top-right|Bottom-left/
42
68
  def rotated_image?(image)
43
69
  ROTATIONS === image.get("exif-ifd0-Orientation")
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "open3"
4
-
5
3
  module ActiveStorageValidations
6
4
  # = ActiveStorageValidations PDF \Analyzer
7
5
  #
@@ -49,13 +47,9 @@ module ActiveStorageValidations
49
47
  end
50
48
 
51
49
  def media_from_path(path)
52
- instrument(File.basename(pdfinfo_path)) do
53
- stdout, _stderr, status = Open3.capture3(
54
- pdfinfo_path,
55
- path
56
- )
57
-
58
- status.success? ? stdout_to_hash(stdout) : nil
50
+ instrument(File.basename(pdfinfo_path)) do |payload|
51
+ result = run_command(pdfinfo_path, path, payload: payload)
52
+ result.success? ? stdout_to_hash(result.stdout) : nil
59
53
  end
60
54
  end
61
55
 
@@ -28,17 +28,18 @@ module ActiveStorageValidations
28
28
  end
29
29
 
30
30
  def media_from_path(path)
31
- instrument(File.basename(ffprobe_path)) do
32
- stdout, _stderr, status = Open3.capture3(
31
+ instrument(File.basename(ffprobe_path)) do |payload|
32
+ result = run_command(
33
33
  ffprobe_path,
34
34
  "-print_format", "json",
35
35
  "-show_streams",
36
36
  "-show_format",
37
37
  "-v", "error",
38
- path
38
+ path,
39
+ payload: payload
39
40
  )
40
41
 
41
- status.success? ? JSON.parse(stdout) : nil
42
+ result.success? ? JSON.parse(result.stdout) : nil
42
43
  end
43
44
  end
44
45
 
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "open3"
4
3
  require_relative "shared/asv_ff_probable"
5
4
 
6
5
  module ActiveStorageValidations
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "shared/asv_attachable"
4
+ require_relative "shared/asv_commandable"
4
5
  require_relative "shared/asv_loggable"
5
6
 
6
7
  module ActiveStorageValidations
@@ -12,12 +13,14 @@ module ActiveStorageValidations
12
13
  # Heavily (not to say 100%) inspired by Rails own ActiveStorage::Analyzer
13
14
  class Analyzer
14
15
  include ASVAttachable
16
+ include ASVCommandable
15
17
  include ASVLoggable
16
18
 
17
19
  attr_reader :attachable
18
20
 
19
- def initialize(attachable)
21
+ def initialize(attachable, timeout: ActiveStorageValidations.command_timeout)
20
22
  @attachable = attachable
23
+ @timeout = timeout
21
24
  end
22
25
 
23
26
  # Override this method in a concrete subclass. Have it return a String content type.
@@ -81,8 +84,37 @@ module ActiveStorageValidations
81
84
  raise NotImplementedError
82
85
  end
83
86
 
84
- def instrument(analyzer, &block)
85
- ActiveSupport::Notifications.instrument("analyze.active_storage_validations", analyzer: analyzer, &block)
87
+ def timeout_in_seconds
88
+ return nil if @timeout.nil?
89
+
90
+ @timeout.to_f
91
+ end
92
+
93
+ def instrument(analyzer)
94
+ payload = {
95
+ analyzer: analyzer,
96
+ command: analyzer,
97
+ timeout: timeout_in_seconds,
98
+ timed_out: false
99
+ }
100
+
101
+ ActiveSupport::Notifications.instrument("analyze.active_storage_validations", payload) do
102
+ started_at = monotonic_time
103
+ result = yield payload
104
+ payload[:duration] = monotonic_time - started_at
105
+ result
106
+ end
107
+ end
108
+
109
+ def mark_timed_out!(payload, command)
110
+ payload[:timed_out] = true if payload
111
+ ActiveSupport::Notifications.instrument(
112
+ "timeout.active_storage_validations",
113
+ analyzer: payload&.[](:analyzer) || command,
114
+ command: command,
115
+ timeout: timeout_in_seconds
116
+ )
117
+ logger.info "Skipping file analysis because #{command} timed out after #{timeout_in_seconds} seconds"
86
118
  end
87
119
  end
88
120
  end
@@ -6,7 +6,6 @@ require_relative "shared/asv_attachable"
6
6
  require_relative "shared/asv_errorable"
7
7
  require_relative "shared/asv_optionable"
8
8
  require_relative "shared/asv_symbolizable"
9
- require_relative "analyzer/content_type_analyzer"
10
9
 
11
10
  module ActiveStorageValidations
12
11
  class ContentTypeValidator < ActiveModel::EachValidator # :nodoc:
@@ -18,6 +17,7 @@ module ActiveStorageValidations
18
17
  include ASVSymbolizable
19
18
 
20
19
  AVAILABLE_CHECKS = %i[with in].freeze
20
+ AVAILABLE_SPOOFING_PROTECTION_VALUES = [ true, false, :file, :magika ].freeze
21
21
  ERROR_TYPES = %i[
22
22
  content_type_invalid
23
23
  content_type_spoofed
@@ -27,6 +27,7 @@ module ActiveStorageValidations
27
27
  def check_validity!
28
28
  ensure_exactly_one_validator_option
29
29
  ensure_content_types_validity
30
+ ensure_spoofing_protection_validity
30
31
  end
31
32
 
32
33
  def validate_each(record, attribute, _value)
@@ -125,7 +126,25 @@ module ActiveStorageValidations
125
126
  end
126
127
 
127
128
  def enable_spoofing_protection?
128
- options[:spoofing_protection] == true
129
+ spoofing_protection_backend.present?
130
+ end
131
+
132
+ # +true+ and +:file+ both select the UNIX +file+ CLI; +:magika+ selects Magika.
133
+ def spoofing_protection_backend
134
+ case options[:spoofing_protection]
135
+ when true, :file then :file
136
+ when :magika then :magika
137
+ end
138
+ end
139
+
140
+ def ensure_spoofing_protection_validity
141
+ return unless options.key?(:spoofing_protection)
142
+ return if AVAILABLE_SPOOFING_PROTECTION_VALUES.include?(options[:spoofing_protection])
143
+
144
+ raise ArgumentError, <<~ERROR_MESSAGE
145
+ Unknown spoofing_protection option: #{options[:spoofing_protection].inspect}.
146
+ Allowed values: #{AVAILABLE_SPOOFING_PROTECTION_VALUES.map(&:inspect).join(", ")}
147
+ ERROR_MESSAGE
129
148
  end
130
149
 
131
150
  def attachable_content_type_vs_detected_content_type_mismatch?
@@ -134,10 +153,9 @@ module ActiveStorageValidations
134
153
  end
135
154
 
136
155
  def attachable_content_type_intersects_detected_content_type?
137
- # Ruby intersects? method is only available from 3.1
138
- enlarged_content_type(content_type_without_parameters(@attachable_content_type)).any? do |item|
139
- enlarged_content_type(content_type_without_parameters(@detected_content_type)).include?(item)
140
- end
156
+ enlarged_content_type(content_type_without_parameters(@attachable_content_type)).intersect?(
157
+ enlarged_content_type(content_type_without_parameters(@detected_content_type))
158
+ )
141
159
  end
142
160
 
143
161
  def enlarged_content_type(content_type)
@@ -215,8 +215,10 @@ module ActiveStorageValidations
215
215
 
216
216
  %i[min max].each do |bound|
217
217
  if (range = flat_options[bound])
218
- flat_options[:width] = { bound => range.first }
219
- flat_options[:height] = { bound => range.last }
218
+ flat_options[:width] ||= {}
219
+ flat_options[:height] ||= {}
220
+ flat_options[:width][bound] = range.first
221
+ flat_options[:height][bound] = range.last
220
222
  end
221
223
  end
222
224
 
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorageValidations
4
+ module FormBuilder
5
+ def file_field(method, options = {})
6
+ options = options.symbolize_keys
7
+ infer_accept = options.delete(:infer_accept)
8
+ infer_accept = ActiveStorageValidations.infer_file_field_accept if infer_accept.nil?
9
+
10
+ if infer_accept && !options.key?(:accept)
11
+ accept = inferred_accept_types(method)
12
+ options[:accept] = accept.join(",") if accept.any?
13
+ end
14
+
15
+ super(method, options)
16
+ end
17
+
18
+ private
19
+
20
+ def inferred_accept_types(method)
21
+ return [] unless @object.class.respond_to?(:validators_on)
22
+
23
+ content_type_validators = @object.class.validators_on(method).select do |v|
24
+ v.is_a?(ActiveStorageValidations::ContentTypeValidator)
25
+ end
26
+
27
+ content_type_validators.flat_map do |validator|
28
+ types = Array(validator.options[:with]) + Array(validator.options[:in])
29
+ types.filter_map do |type|
30
+ case type
31
+ when String
32
+ type.include?("/") ? type : Marcel::MimeType.for(declared_type: type, extension: type)
33
+ when Symbol
34
+ Marcel::MimeType.for(declared_type: type.to_s, extension: type.to_s)
35
+ when Regexp
36
+ match = type.source.match(%r{\A\\A([a-z]+)/\.\*\\z\z})
37
+ "#{match[1]}/*" if match
38
+ end
39
+ end
40
+ end.uniq
41
+ end
42
+ end
43
+ end
@@ -4,8 +4,10 @@ require_relative "shared/asv_active_storageable"
4
4
  require_relative "shared/asv_allow_blankable"
5
5
  require_relative "shared/asv_attachable"
6
6
  require_relative "shared/asv_contextable"
7
+ require_relative "shared/asv_except_onable"
7
8
  require_relative "shared/asv_messageable"
8
9
  require_relative "shared/asv_rspecable"
10
+ require_relative "shared/asv_timeoutable"
9
11
  require_relative "shared/asv_validatable"
10
12
 
11
13
  module ActiveStorageValidations
@@ -19,15 +21,19 @@ module ActiveStorageValidations
19
21
  include ASVAllowBlankable
20
22
  include ASVAttachable
21
23
  include ASVContextable
24
+ include ASVExceptOnable
22
25
  include ASVMessageable
23
26
  include ASVRspecable
27
+ include ASVTimeoutable
24
28
  include ASVValidatable
25
29
 
26
30
  def initialize(attribute_name)
27
31
  initialize_allow_blankable
28
32
  initialize_contextable
33
+ initialize_except_onable
29
34
  initialize_messageable
30
35
  initialize_rspecable
36
+ initialize_timeoutable
31
37
  @attribute_name = attribute_name
32
38
  @allowed_aspect_ratios = @rejected_aspect_ratios = []
33
39
  end
@@ -55,7 +61,9 @@ module ActiveStorageValidations
55
61
 
56
62
  is_a_valid_active_storage_attribute? &&
57
63
  is_context_valid? &&
64
+ is_except_on_valid? &&
58
65
  is_allowing_blank? &&
66
+ is_timeout_valid? &&
59
67
  is_custom_message_valid? &&
60
68
  all_allowed_aspect_ratios_allowed? &&
61
69
  all_rejected_aspect_ratios_rejected?
@@ -3,6 +3,7 @@
3
3
  require_relative "shared/asv_active_storageable"
4
4
  require_relative "shared/asv_attachable"
5
5
  require_relative "shared/asv_contextable"
6
+ require_relative "shared/asv_except_onable"
6
7
  require_relative "shared/asv_messageable"
7
8
  require_relative "shared/asv_rspecable"
8
9
  require_relative "shared/asv_validatable"
@@ -17,12 +18,14 @@ module ActiveStorageValidations
17
18
  include ASVActiveStorageable
18
19
  include ASVAttachable
19
20
  include ASVContextable
21
+ include ASVExceptOnable
20
22
  include ASVMessageable
21
23
  include ASVRspecable
22
24
  include ASVValidatable
23
25
 
24
26
  def initialize(attribute_name)
25
27
  initialize_contextable
28
+ initialize_except_onable
26
29
  initialize_messageable
27
30
  initialize_rspecable
28
31
  @attribute_name = attribute_name
@@ -41,6 +44,7 @@ module ActiveStorageValidations
41
44
 
42
45
  is_a_valid_active_storage_attribute? &&
43
46
  is_context_valid? &&
47
+ is_except_on_valid? &&
44
48
  is_custom_message_valid? &&
45
49
  is_valid_when_file_attached? &&
46
50
  is_invalid_when_file_not_attached?
@@ -7,6 +7,7 @@ require_relative "shared/asv_active_storageable"
7
7
  require_relative "shared/asv_allow_blankable"
8
8
  require_relative "shared/asv_attachable"
9
9
  require_relative "shared/asv_contextable"
10
+ require_relative "shared/asv_except_onable"
10
11
  require_relative "shared/asv_messageable"
11
12
  require_relative "shared/asv_rspecable"
12
13
  require_relative "shared/asv_validatable"
@@ -20,6 +21,7 @@ module ActiveStorageValidations
20
21
  include ASVAllowBlankable
21
22
  include ASVAttachable
22
23
  include ASVContextable
24
+ include ASVExceptOnable
23
25
  include ASVMessageable
24
26
  include ASVRspecable
25
27
  include ASVValidatable
@@ -27,6 +29,7 @@ module ActiveStorageValidations
27
29
  def initialize(attribute_name)
28
30
  initialize_allow_blankable
29
31
  initialize_contextable
32
+ initialize_except_onable
30
33
  initialize_messageable
31
34
  initialize_rspecable
32
35
  @attribute_name = attribute_name
@@ -68,17 +71,24 @@ module ActiveStorageValidations
68
71
 
69
72
  is_a_valid_active_storage_attribute? &&
70
73
  is_context_valid? &&
74
+ is_except_on_valid? &&
71
75
  is_allowing_blank? &&
72
76
  is_custom_message_valid? &&
73
77
  not_lower_than_min? &&
74
78
  higher_than_min? &&
75
79
  lower_than_max? &&
76
80
  not_higher_than_max? &&
77
- equal_to_exact?
81
+ equal_to_exact? &&
82
+ is_timeout_valid?
78
83
  end
79
84
 
80
85
  protected
81
86
 
87
+ # Hook for ASVTimeoutable (Duration / Pages). Size / TotalSize leave this as-is.
88
+ def is_timeout_valid?
89
+ true
90
+ end
91
+
82
92
  def build_failure_message(message)
83
93
  return unless @failure_message_artefacts.present?
84
94