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.
- checksums.yaml +4 -4
- data/MIT-LICENSE +1 -1
- data/README.md +178 -115
- data/Rakefile +13 -15
- data/lib/active_storage_validations/analyzer/audio_analyzer.rb +0 -1
- data/lib/active_storage_validations/analyzer/content_type_analyzer/file.rb +31 -0
- data/lib/active_storage_validations/analyzer/content_type_analyzer/magika.rb +44 -0
- data/lib/active_storage_validations/analyzer/content_type_analyzer.rb +15 -24
- data/lib/active_storage_validations/analyzer/image_analyzer/image_magick.rb +41 -7
- data/lib/active_storage_validations/analyzer/image_analyzer/vips.rb +36 -10
- data/lib/active_storage_validations/analyzer/pdf_analyzer.rb +3 -9
- data/lib/active_storage_validations/analyzer/shared/asv_ff_probable.rb +5 -4
- data/lib/active_storage_validations/analyzer/video_analyzer.rb +0 -1
- data/lib/active_storage_validations/analyzer.rb +35 -3
- data/lib/active_storage_validations/content_type_validator.rb +24 -6
- data/lib/active_storage_validations/dimension_validator.rb +4 -2
- data/lib/active_storage_validations/form_builder.rb +43 -0
- data/lib/active_storage_validations/matchers/aspect_ratio_validator_matcher.rb +8 -0
- data/lib/active_storage_validations/matchers/attached_validator_matcher.rb +4 -0
- data/lib/active_storage_validations/matchers/base_comparison_validator_matcher.rb +11 -1
- data/lib/active_storage_validations/matchers/content_type_validator_matcher.rb +12 -0
- data/lib/active_storage_validations/matchers/dimension_validator_matcher.rb +8 -0
- data/lib/active_storage_validations/matchers/duration_validator_matcher.rb +8 -0
- data/lib/active_storage_validations/matchers/limit_validator_matcher.rb +4 -0
- data/lib/active_storage_validations/matchers/pages_validator_matcher.rb +8 -0
- data/lib/active_storage_validations/matchers/processable_file_validator_matcher.rb +8 -0
- data/lib/active_storage_validations/matchers/shared/asv_except_onable.rb +57 -0
- data/lib/active_storage_validations/matchers/shared/asv_spoofing_protectable.rb +44 -0
- data/lib/active_storage_validations/matchers/shared/asv_timeoutable.rb +32 -0
- data/lib/active_storage_validations/matchers.rb +9 -1
- data/lib/active_storage_validations/railtie.rb +6 -0
- data/lib/active_storage_validations/shared/asv_analyzable.rb +66 -8
- data/lib/active_storage_validations/shared/asv_attachable.rb +2 -9
- data/lib/active_storage_validations/shared/asv_commandable.rb +95 -0
- data/lib/active_storage_validations/shared/asv_optionable.rb +5 -1
- data/lib/active_storage_validations/version.rb +1 -1
- data/lib/active_storage_validations.rb +22 -0
- 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
|
-
#
|
|
9
|
-
#
|
|
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
|
-
#
|
|
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
|
|
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
|
|
40
|
+
raise missing_backend_error
|
|
45
41
|
end
|
|
46
42
|
|
|
47
|
-
def
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
"file",
|
|
51
|
-
"-b",
|
|
52
|
-
"--mime-type",
|
|
53
|
-
path
|
|
54
|
-
)
|
|
43
|
+
def backend
|
|
44
|
+
raise NotImplementedError
|
|
45
|
+
end
|
|
55
46
|
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
14
|
-
|
|
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
|
|
24
|
-
logger.
|
|
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
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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
|
-
|
|
42
|
+
result.success? ? JSON.parse(result.stdout) : nil
|
|
42
43
|
end
|
|
43
44
|
end
|
|
44
45
|
|
|
@@ -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
|
|
85
|
-
|
|
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
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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]
|
|
219
|
-
flat_options[:height]
|
|
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
|
|