active_storage_validations 1.2.0 → 1.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +35 -22
  3. data/config/locales/da.yml +1 -2
  4. data/config/locales/de.yml +1 -1
  5. data/config/locales/en.yml +1 -1
  6. data/config/locales/es.yml +1 -1
  7. data/config/locales/fr.yml +1 -1
  8. data/config/locales/it.yml +1 -1
  9. data/config/locales/ja.yml +1 -1
  10. data/config/locales/nl.yml +1 -1
  11. data/config/locales/pl.yml +1 -1
  12. data/config/locales/pt-BR.yml +1 -1
  13. data/config/locales/ru.yml +1 -1
  14. data/config/locales/sv.yml +1 -1
  15. data/config/locales/tr.yml +1 -1
  16. data/config/locales/uk.yml +1 -1
  17. data/config/locales/vi.yml +1 -1
  18. data/config/locales/zh-CN.yml +1 -1
  19. data/lib/active_storage_validations/aspect_ratio_validator.rb +57 -66
  20. data/lib/active_storage_validations/attached_validator.rb +5 -3
  21. data/lib/active_storage_validations/base_size_validator.rb +4 -1
  22. data/lib/active_storage_validations/concerns/active_storageable.rb +28 -0
  23. data/lib/active_storage_validations/concerns/attachable.rb +134 -0
  24. data/lib/active_storage_validations/concerns/errorable.rb +4 -4
  25. data/lib/active_storage_validations/concerns/loggable.rb +9 -0
  26. data/lib/active_storage_validations/concerns/optionable.rb +27 -0
  27. data/lib/active_storage_validations/content_type_spoof_detector.rb +94 -0
  28. data/lib/active_storage_validations/content_type_validator.rb +113 -39
  29. data/lib/active_storage_validations/dimension_validator.rb +32 -52
  30. data/lib/active_storage_validations/limit_validator.rb +8 -5
  31. data/lib/active_storage_validations/marcel_extensor.rb +5 -0
  32. data/lib/active_storage_validations/matchers/concerns/attachable.rb +27 -9
  33. data/lib/active_storage_validations/matchers/concerns/messageable.rb +1 -1
  34. data/lib/active_storage_validations/matchers/content_type_validator_matcher.rb +7 -0
  35. data/lib/active_storage_validations/matchers/limit_validator_matcher.rb +127 -0
  36. data/lib/active_storage_validations/matchers/total_size_validator_matcher.rb +1 -10
  37. data/lib/active_storage_validations/matchers.rb +4 -15
  38. data/lib/active_storage_validations/metadata.rb +22 -26
  39. data/lib/active_storage_validations/processable_image_validator.rb +17 -32
  40. data/lib/active_storage_validations/size_validator.rb +3 -7
  41. data/lib/active_storage_validations/total_size_validator.rb +4 -8
  42. data/lib/active_storage_validations/version.rb +1 -1
  43. data/lib/active_storage_validations.rb +2 -1
  44. metadata +67 -21
  45. data/lib/active_storage_validations/option_proc_unfolding.rb +0 -16
@@ -8,6 +8,27 @@ module ActiveStorageValidations
8
8
  @subject.public_send(@attribute_name)
9
9
  end
10
10
 
11
+ def attach_files(count)
12
+ return unless count.positive?
13
+
14
+ file_array = Array.new(count, dummy_file)
15
+
16
+ @subject.public_send(@attribute_name).attach(file_array)
17
+ end
18
+
19
+ def detach_file
20
+ @subject.attachment_changes.delete(@attribute_name.to_s)
21
+ end
22
+ alias :detach_files :detach_file
23
+
24
+ def file_attached?
25
+ @subject.public_send(@attribute_name).attached?
26
+ end
27
+
28
+ def dummy_blob
29
+ ActiveStorage::Blob.create_and_upload!(**dummy_file)
30
+ end
31
+
11
32
  def dummy_file
12
33
  {
13
34
  io: io,
@@ -18,8 +39,8 @@ module ActiveStorageValidations
18
39
 
19
40
  def processable_image
20
41
  {
21
- io: File.open(Rails.root.join('public', 'image_1920x1080.png')),
22
- filename: 'image_1920x1080_file.png',
42
+ io: StringIO.new(image_data),
43
+ filename: 'processable_image.png',
23
44
  content_type: 'image/png'
24
45
  }
25
46
  end
@@ -27,7 +48,7 @@ module ActiveStorageValidations
27
48
  def not_processable_image
28
49
  {
29
50
  io: Tempfile.new('.'),
30
- filename: 'processable.txt',
51
+ filename: 'not_processable_image.txt',
31
52
  content_type: 'text/plain'
32
53
  }
33
54
  end
@@ -36,12 +57,9 @@ module ActiveStorageValidations
36
57
  @io ||= Tempfile.new('Hello world!')
37
58
  end
38
59
 
39
- def detach_file
40
- @subject.attachment_changes.delete(@attribute_name.to_s)
41
- end
42
-
43
- def file_attached?
44
- @subject.public_send(@attribute_name).attached?
60
+ def image_data
61
+ # Binary data for a 1x1 transparent PNG image
62
+ "\x89PNG\r\n\x1A\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1F\x15\xC4\x89\x00\x00\x00\nIDATx\x9Cc\x00\x01\x00\x00\x05\x00\x01\r\n\x2D\xB4\x00\x00\x00\x00IEND\xAE\x42\x60\x82"
45
63
  end
46
64
  end
47
65
  end
@@ -18,7 +18,7 @@ module ActiveStorageValidations
18
18
 
19
19
  def has_an_error_message_which_is_custom_message?
20
20
  validator_errors_for_attribute.one? do |error|
21
- error[:error] == @custom_message
21
+ error[:custom_message] == @custom_message
22
22
  end
23
23
  end
24
24
  end
@@ -130,6 +130,13 @@ module ActiveStorageValidations
130
130
  content_type: type
131
131
  }
132
132
  end
133
+
134
+ # Due to the way we build test attachments in #attachment_for
135
+ # (ie spoofed file basically), we need to ignore the error related to
136
+ # content type spoofing in our matcher to pass the tests
137
+ def validator_errors_for_attribute
138
+ super.reject { |hash| hash[:error] == :spoofed_content_type }
139
+ end
133
140
  end
134
141
  end
135
142
  end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'concerns/active_storageable'
4
+ require_relative 'concerns/allow_blankable'
5
+ require_relative 'concerns/attachable'
6
+ require_relative 'concerns/contextable'
7
+ require_relative 'concerns/messageable'
8
+ require_relative 'concerns/rspecable'
9
+ require_relative 'concerns/validatable'
10
+
11
+ module ActiveStorageValidations
12
+ module Matchers
13
+ def validate_limits_of(name)
14
+ LimitValidatorMatcher.new(name)
15
+ end
16
+
17
+ class LimitValidatorMatcher
18
+ include ActiveStorageable
19
+ include AllowBlankable
20
+ include Attachable
21
+ include Contextable
22
+ include Messageable
23
+ include Rspecable
24
+ include Validatable
25
+
26
+ def initialize(attribute_name)
27
+ initialize_allow_blankable
28
+ initialize_contextable
29
+ initialize_messageable
30
+ initialize_rspecable
31
+ @attribute_name = attribute_name
32
+ @min = @max = nil
33
+ end
34
+
35
+ def description
36
+ "validate the limit files of :#{@attribute_name}"
37
+ end
38
+
39
+ def failure_message
40
+ message = ["is expected to validate limit file of :#{@attribute_name}"]
41
+ build_failure_message(message)
42
+ message.join("\n")
43
+ end
44
+
45
+ def min(count)
46
+ @min = count
47
+ self
48
+ end
49
+
50
+ def max(count)
51
+ @max = count
52
+ self
53
+ end
54
+
55
+ def matches?(subject)
56
+ @subject = subject.is_a?(Class) ? subject.new : subject
57
+
58
+ is_a_valid_active_storage_attribute? &&
59
+ is_context_valid? &&
60
+ is_custom_message_valid? &&
61
+ file_count_not_smaller_than_min? &&
62
+ file_count_equal_min? &&
63
+ file_count_larger_than_min? &&
64
+ file_count_smaller_than_max? &&
65
+ file_count_equal_max? &&
66
+ file_count_not_larger_than_max?
67
+ end
68
+
69
+ private
70
+
71
+ def build_failure_message(message)
72
+ return unless @failure_message_artefacts.present?
73
+
74
+ message << " but there seem to have issues with the matcher methods you used, since:"
75
+ @failure_message_artefacts.each do |error_case|
76
+ message << " validation failed when provided with #{error_case[:count]} file(s)"
77
+ end
78
+ message << " whereas it should have passed"
79
+ end
80
+
81
+ def file_count_not_smaller_than_min?
82
+ @min.nil? || @min.zero? || !passes_validation_with_limits(@min - 1)
83
+ end
84
+
85
+ def file_count_equal_min?
86
+ @min.nil? || @min.zero? || passes_validation_with_limits(@min)
87
+ end
88
+
89
+ def file_count_larger_than_min?
90
+ @min.nil? || @min.zero? || @min == @max || passes_validation_with_limits(@min + 1)
91
+ end
92
+
93
+ def file_count_smaller_than_max?
94
+ @max.nil? || @min == @max || passes_validation_with_limits(@max - 1)
95
+ end
96
+
97
+ def file_count_equal_max?
98
+ @max.nil? || passes_validation_with_limits(@max)
99
+ end
100
+
101
+ def file_count_not_larger_than_max?
102
+ @max.nil? || !passes_validation_with_limits(@max + 1)
103
+ end
104
+
105
+ def passes_validation_with_limits(count)
106
+ attach_files(count)
107
+ validate
108
+ detach_files
109
+ is_valid? || add_failure_message_artefact(count)
110
+ end
111
+
112
+ def is_custom_message_valid?
113
+ return true if !@custom_message || (@min&.zero? && @max.nil?)
114
+
115
+ @min.nil? ? attach_files(@max + 1) : attach_files(@min - 1)
116
+ validate
117
+ detach_files
118
+ has_an_error_message_which_is_custom_message?
119
+ end
120
+
121
+ def add_failure_message_artefact(count)
122
+ @failure_message_artefacts << { count: count }
123
+ false
124
+ end
125
+ end
126
+ end
127
+ end
@@ -22,19 +22,10 @@ module ActiveStorageValidations
22
22
  protected
23
23
 
24
24
  def attach_file
25
- # We attach blobs instead of io for has_many_attached relation
25
+ # has_many_attached relation
26
26
  @subject.public_send(@attribute_name).attach([dummy_blob])
27
27
  @subject.public_send(@attribute_name)
28
28
  end
29
-
30
- def dummy_blob
31
- ActiveStorage::Blob.create_and_upload!(
32
- io: io,
33
- filename: 'test.png',
34
- content_type: 'image/png',
35
- service_name: 'test'
36
- )
37
- end
38
29
  end
39
30
  end
40
31
  end
@@ -3,6 +3,7 @@
3
3
  require 'active_storage_validations/matchers/aspect_ratio_validator_matcher'
4
4
  require 'active_storage_validations/matchers/attached_validator_matcher'
5
5
  require 'active_storage_validations/matchers/processable_image_validator_matcher'
6
+ require 'active_storage_validations/matchers/limit_validator_matcher'
6
7
  require 'active_storage_validations/matchers/content_type_validator_matcher'
7
8
  require 'active_storage_validations/matchers/dimension_validator_matcher'
8
9
  require 'active_storage_validations/matchers/size_validator_matcher'
@@ -25,21 +26,9 @@ module ActiveStorageValidations
25
26
  end
26
27
 
27
28
  def self.mock_metadata(attachment, width, height)
28
- if Rails.gem_version >= Gem::Version.new('6.0.0')
29
- # Mock the Metadata class for rails 6
30
- mock = OpenStruct.new(metadata: { width: width, height: height })
31
- stub_method(ActiveStorageValidations::Metadata, :new, mock) do
32
- yield
33
- end
34
- else
35
- # Stub the metadata analysis for rails 5
36
- stub_method(attachment, :analyze, true) do
37
- stub_method(attachment, :analyzed?, true) do
38
- stub_method(attachment, :metadata, { width: width, height: height }) do
39
- yield
40
- end
41
- end
42
- end
29
+ mock = Struct.new(:metadata).new({ width: width, height: height })
30
+ stub_method(ActiveStorageValidations::Metadata, :new, mock) do
31
+ yield
43
32
  end
44
33
  end
45
34
  end
@@ -1,14 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'concerns/loggable'
4
+
1
5
  module ActiveStorageValidations
2
6
  class Metadata
7
+ include Loggable
8
+
3
9
  class InvalidImageError < StandardError; end
4
10
 
5
- attr_reader :file
11
+ attr_reader :attachable
6
12
 
7
13
  DEFAULT_IMAGE_PROCESSOR = :mini_magick.freeze
8
14
 
9
- def initialize(file)
15
+ def initialize(attachable)
10
16
  require_image_processor
11
- @file = file
17
+ @attachable = attachable
12
18
  end
13
19
 
14
20
  def valid?
@@ -58,18 +64,9 @@ module ActiveStorageValidations
58
64
  end
59
65
 
60
66
  def read_image
61
- is_string = file.is_a?(String)
62
- if is_string || file.is_a?(ActiveStorage::Blob)
63
- blob =
64
- if is_string
65
- if Rails.gem_version < Gem::Version.new('6.1.0')
66
- ActiveStorage::Blob.find_signed(file)
67
- else
68
- ActiveStorage::Blob.find_signed!(file)
69
- end
70
- else
71
- file
72
- end
67
+ is_string = attachable.is_a?(String)
68
+ if is_string || attachable.is_a?(ActiveStorage::Blob)
69
+ blob = is_string ? ActiveStorage::Blob.find_signed!(attachable) : attachable
73
70
 
74
71
  tempfile = Tempfile.new(["ActiveStorage-#{blob.id}-", blob.filename.extension_with_delimiter])
75
72
  tempfile.binmode
@@ -105,9 +102,9 @@ module ActiveStorageValidations
105
102
  begin
106
103
  Vips::Image.new_from_file(path)
107
104
  rescue exception_class
108
- # We handle cases where an error is raised when reading the file
105
+ # We handle cases where an error is raised when reading the attachable
109
106
  # because Vips can throw errors rather than returning false
110
- # We stumble upon this issue while reading 0 byte size file
107
+ # We stumble upon this issue while reading 0 byte size attachable
111
108
  # https://github.com/janko/image_processing/issues/97
112
109
  false
113
110
  end
@@ -154,13 +151,13 @@ module ActiveStorageValidations
154
151
  end
155
152
 
156
153
  def read_file_path
157
- case file
154
+ case attachable
158
155
  when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile
159
- file.path
156
+ attachable.path
160
157
  when Hash
161
- io = file.fetch(:io)
158
+ io = attachable.fetch(:io)
162
159
  if io.is_a?(StringIO)
163
- tempfile = Tempfile.new([File.basename(file[:filename], '.*'), File.extname(file[:filename])])
160
+ tempfile = Tempfile.new([File.basename(attachable[:filename], '.*'), File.extname(attachable[:filename])])
164
161
  tempfile.binmode
165
162
  IO.copy_stream(io, tempfile)
166
163
  io.rewind
@@ -170,14 +167,13 @@ module ActiveStorageValidations
170
167
  else
171
168
  File.open(io).path
172
169
  end
170
+ when File
171
+ attachable.path
172
+ when Pathname
173
+ attachable.to_s
173
174
  else
174
175
  raise "Something wrong with params."
175
176
  end
176
177
  end
177
-
178
- def logger
179
- Rails.logger
180
- end
181
-
182
178
  end
183
179
  end
@@ -1,12 +1,14 @@
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'
4
6
  require_relative 'concerns/symbolizable.rb'
5
- require_relative 'metadata.rb'
6
7
 
7
8
  module ActiveStorageValidations
8
9
  class ProcessableImageValidator < ActiveModel::EachValidator # :nodoc
9
- include OptionProcUnfolding
10
+ include ActiveStorageable
11
+ include Attachable
10
12
  include Errorable
11
13
  include Symbolizable
12
14
 
@@ -14,36 +16,19 @@ module ActiveStorageValidations
14
16
  image_not_processable
15
17
  ].freeze
16
18
 
17
- if Rails.gem_version >= Gem::Version.new('6.0.0')
18
- def validate_each(record, attribute, _value)
19
- return true unless record.send(attribute).attached?
20
-
21
- changes = record.attachment_changes[attribute.to_s]
22
- return true if changes.blank?
23
-
24
- files = Array.wrap(changes.is_a?(ActiveStorage::Attached::Changes::CreateMany) ? changes.attachables : changes.attachable)
25
-
26
- files.each do |file|
27
- if !Metadata.new(file).valid?
28
- errors_options = initialize_error_options(options, file)
29
- add_error(record, attribute, ERROR_TYPES.first , **errors_options) unless Metadata.new(file).valid?
30
- end
31
- end
32
- end
33
- else
34
- # Rails 5
35
- def validate_each(record, attribute, _value)
36
- return true unless record.send(attribute).attached?
37
-
38
- files = Array.wrap(record.send(attribute))
39
-
40
- files.each do |file|
41
- if !Metadata.new(file).valid?
42
- errors_options = initialize_error_options(options, file)
43
- add_error(record, attribute, ERROR_TYPES.first , **errors_options) unless Metadata.new(file).valid?
44
- end
45
- end
46
- end
19
+ def validate_each(record, attribute, _value)
20
+ return if no_attachments?(record, attribute)
21
+
22
+ validate_changed_files_from_metadata(record, attribute)
23
+ end
24
+
25
+ private
26
+
27
+ def is_valid?(record, attribute, attachable, metadata)
28
+ return if !metadata.empty?
29
+
30
+ errors_options = initialize_error_options(options, attachable)
31
+ add_error(record, attribute, ERROR_TYPES.first , **errors_options)
47
32
  end
48
33
  end
49
34
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'concerns/errorable.rb'
4
- require_relative 'concerns/symbolizable.rb'
5
3
  require_relative 'base_size_validator.rb'
6
4
 
7
5
  module ActiveStorageValidations
@@ -15,12 +13,11 @@ module ActiveStorageValidations
15
13
  ].freeze
16
14
 
17
15
  def validate_each(record, attribute, _value)
18
- return true unless record.send(attribute).attached?
16
+ return if no_attachments?(record, attribute)
19
17
 
20
- files = Array.wrap(record.send(attribute))
21
- flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS)
18
+ flat_options = set_flat_options(record)
22
19
 
23
- files.each do |file|
20
+ attached_files(record, attribute).each do |file|
24
21
  next if is_valid?(file.blob.byte_size, flat_options)
25
22
 
26
23
  errors_options = initialize_error_options(options, file)
@@ -31,7 +28,6 @@ module ActiveStorageValidations
31
28
  error_type = "file_size_not_#{keys.first}".to_sym
32
29
 
33
30
  add_error(record, attribute, error_type, **errors_options)
34
- break
35
31
  end
36
32
  end
37
33
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'concerns/errorable.rb'
4
- require_relative 'concerns/symbolizable.rb'
5
3
  require_relative 'base_size_validator.rb'
6
4
 
7
5
  module ActiveStorageValidations
@@ -17,14 +15,12 @@ module ActiveStorageValidations
17
15
  def validate_each(record, attribute, _value)
18
16
  custom_check_validity!(record, attribute)
19
17
 
20
- return true unless record.send(attribute).attached?
18
+ return if no_attachments?(record, attribute)
21
19
 
22
- files = Array.wrap(record.send(attribute))
23
- flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS)
20
+ total_file_size = attached_files(record, attribute).sum { |file| file.blob.byte_size }
21
+ flat_options = set_flat_options(record)
24
22
 
25
- total_file_size = files.sum { |file| file.blob.byte_size }
26
-
27
- return true if is_valid?(total_file_size, flat_options)
23
+ return if is_valid?(total_file_size, flat_options)
28
24
 
29
25
  errors_options = initialize_error_options(options, nil)
30
26
  populate_error_options(errors_options, flat_options)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveStorageValidations
4
- VERSION = '1.2.0'
4
+ VERSION = '1.3.1'
5
5
  end
@@ -5,7 +5,6 @@ require 'active_support/concern'
5
5
 
6
6
  require 'active_storage_validations/railtie'
7
7
  require 'active_storage_validations/engine'
8
- require 'active_storage_validations/option_proc_unfolding'
9
8
  require 'active_storage_validations/attached_validator'
10
9
  require 'active_storage_validations/content_type_validator'
11
10
  require 'active_storage_validations/limit_validator'
@@ -15,6 +14,8 @@ require 'active_storage_validations/processable_image_validator'
15
14
  require 'active_storage_validations/size_validator'
16
15
  require 'active_storage_validations/total_size_validator'
17
16
 
17
+ require 'active_storage_validations/marcel_extensor'
18
+
18
19
  ActiveSupport.on_load(:active_record) do
19
20
  send :include, ActiveStorageValidations
20
21
  end