file_validators 2.3.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +5 -5
  2. data/.rubocop.yml +32 -0
  3. data/.tool-versions +1 -0
  4. data/.travis.yml +36 -6
  5. data/Appraisals +8 -8
  6. data/CHANGELOG.md +20 -0
  7. data/Gemfile +2 -0
  8. data/README.md +21 -16
  9. data/Rakefile +3 -1
  10. data/file_validators.gemspec +12 -6
  11. data/gemfiles/activemodel_3.2.gemfile +0 -0
  12. data/gemfiles/activemodel_4.0.gemfile +0 -0
  13. data/gemfiles/activemodel_5.0.gemfile +0 -0
  14. data/gemfiles/{activemodel_4.1.gemfile → activemodel_6.0.gemfile} +1 -2
  15. data/gemfiles/{activemodel_4.2.gemfile → activemodel_6.1.gemfile} +1 -2
  16. data/lib/file_validators/error.rb +6 -0
  17. data/lib/file_validators/mime_type_analyzer.rb +106 -0
  18. data/lib/file_validators/validators/file_content_type_validator.rb +33 -42
  19. data/lib/file_validators/validators/file_size_validator.rb +43 -19
  20. data/lib/file_validators/version.rb +3 -1
  21. data/lib/file_validators.rb +6 -7
  22. data/spec/integration/combined_validators_integration_spec.rb +3 -1
  23. data/spec/integration/file_content_type_validation_integration_spec.rb +73 -17
  24. data/spec/integration/file_size_validator_integration_spec.rb +43 -16
  25. data/spec/lib/file_validators/mime_type_analyzer_spec.rb +139 -0
  26. data/spec/lib/file_validators/validators/file_content_type_validator_spec.rb +90 -32
  27. data/spec/lib/file_validators/validators/file_size_validator_spec.rb +78 -30
  28. data/spec/spec_helper.rb +4 -0
  29. data/spec/support/fakeio.rb +17 -0
  30. data/spec/support/helpers.rb +7 -0
  31. data/spec/support/matchers/allow_content_type.rb +2 -0
  32. data/spec/support/matchers/allow_file_size.rb +2 -0
  33. metadata +82 -23
  34. data/lib/file_validators/utils/content_type_detector.rb +0 -67
  35. data/lib/file_validators/utils/media_type_spoof_detector.rb +0 -46
  36. data/spec/lib/file_validators/utils/content_type_detector_spec.rb +0 -27
  37. data/spec/lib/file_validators/utils/media_type_spoof_detector_spec.rb +0 -31
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 0353fd8c53123663ec484c7172c2df4d572f95dd
4
- data.tar.gz: b1eb811cfea7257288ff5e588621f78aeff39896
2
+ SHA256:
3
+ metadata.gz: 3a55a08bc74303b69e964f61e3726e8ee00278c4cd0cd6fc217691c85114bcfb
4
+ data.tar.gz: be3b6d628de09d262a97abf3099ff66fd2e89dd27eec3030523a7f36c82e17df
5
5
  SHA512:
6
- metadata.gz: 002f91a5243bf85650572ecb80a5283a25aafd6cdda3d4e9fe19bba988f597f3db228e583dc113bb54fb1c7973bc0caa9fc409592f10ff1c75c075438959fcf7
7
- data.tar.gz: a1860e71ee7054624f2cf368802e1af2ee302886ecfca99c0ff43b948c6cf3af3d9264e62b1c877220fb465c18a5df6e11b507d42dbe03c29612e0e2f8513dd7
6
+ metadata.gz: cf5b75fb78eaf84f42abef75255572b1917012fbdbdd70d771da14c366619677fb69f4c45d688b8851d5b99dbba44ccd08da6c7025c3819c40259e23502f5c61
7
+ data.tar.gz: 92e9c49f94d8cbc1c2f8aa7425527e84555e94d25c12b6ca1b51e4d5e51f765defb274264996174e6757d8606270f5513232be64d234717075abab8680d8f7c9
data/.rubocop.yml ADDED
@@ -0,0 +1,32 @@
1
+ Bundler/OrderedGems:
2
+ Enabled: false
3
+
4
+ Style/Documentation:
5
+ Enabled: false
6
+
7
+ Style/MissingRespondToMissing:
8
+ Enabled: false
9
+
10
+ Style/CaseEquality:
11
+ Enabled: false
12
+
13
+ Style/GuardClause:
14
+ Enabled: false
15
+
16
+ Style/RegexpLiteral:
17
+ Enabled: false
18
+
19
+ Style/Next:
20
+ Enabled: false
21
+
22
+ Metrics/LineLength:
23
+ Max: 110
24
+
25
+ Metrics/ModuleLength:
26
+ Enabled: false
27
+
28
+ Metrics/BlockLength:
29
+ Enabled: false
30
+
31
+ Metrics/MethodLength:
32
+ Enabled: false
data/.tool-versions ADDED
@@ -0,0 +1 @@
1
+ ruby 2.3.1
data/.travis.yml CHANGED
@@ -1,25 +1,55 @@
1
1
  language: ruby
2
2
 
3
3
  rvm:
4
- - 2.0
5
4
  - 2.2.3
5
+ - 2.5.0
6
+ - 3.0.0
6
7
  - ruby-head
7
- - jruby-9.0.4.0
8
8
  - jruby-9.1.7.0
9
+ - jruby-9.2.13.0
9
10
 
10
11
  gemfile:
12
+ - gemfiles/activemodel_6.1.gemfile
13
+ - gemfiles/activemodel_6.0.gemfile
11
14
  - gemfiles/activemodel_5.0.gemfile
12
- - gemfiles/activemodel_4.2.gemfile
13
- - gemfiles/activemodel_4.1.gemfile
14
15
  - gemfiles/activemodel_4.0.gemfile
15
16
  - gemfiles/activemodel_3.2.gemfile
16
17
 
17
18
  matrix:
18
19
  exclude:
19
- - rvm: 2.0
20
+ - rvm: 2.2.3
21
+ gemfile: gemfiles/activemodel_6.0.gemfile
22
+ - rvm: 2.2.3
23
+ gemfile: gemfiles/activemodel_6.1.gemfile
24
+
25
+ - rvm: 2.5.0
26
+ gemfile: gemfiles/activemodel_3.2.gemfile
27
+ - rvm: 2.5.0
28
+ gemfile: gemfiles/activemodel_4.0.gemfile
29
+
30
+ - rvm: 3.0.0
31
+ gemfile: gemfiles/activemodel_3.2.gemfile
32
+ - rvm: 3.0.0
33
+ gemfile: gemfiles/activemodel_4.0.gemfile
34
+ - rvm: 3.0.0
20
35
  gemfile: gemfiles/activemodel_5.0.gemfile
21
- - rvm: jruby-9.0.4.0
36
+
37
+ - rvm: ruby-head
38
+ gemfile: gemfiles/activemodel_3.2.gemfile
39
+ - rvm: ruby-head
40
+ gemfile: gemfiles/activemodel_4.0.gemfile
41
+ - rvm: ruby-head
22
42
  gemfile: gemfiles/activemodel_5.0.gemfile
23
43
 
44
+ - rvm: jruby-9.1.7.0
45
+ gemfile: gemfiles/activemodel_6.0.gemfile
46
+ - rvm: jruby-9.1.7.0
47
+ gemfile: gemfiles/activemodel_6.1.gemfile
48
+
49
+ - rvm: jruby-9.2.13.0
50
+ gemfile: gemfiles/activemodel_3.2.gemfile
51
+ - rvm: jruby-9.2.13.0
52
+ gemfile: gemfiles/activemodel_4.0.gemfile
53
+
24
54
  allow_failures:
25
55
  - rvm: ruby-head
data/Appraisals CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  appraise 'activemodel-3.2' do
2
4
  gem 'activemodel', '3.2.22.5'
3
5
  gem 'rack', '1.6.5'
@@ -8,16 +10,14 @@ appraise 'activemodel-4.0' do
8
10
  gem 'rack', '1.6.5'
9
11
  end
10
12
 
11
- appraise 'activemodel-4.1' do
12
- gem 'activemodel', '4.1.6'
13
- gem 'rack', '1.6.5'
13
+ appraise 'activemodel-5.0' do
14
+ gem 'activemodel', '5.0.1'
14
15
  end
15
16
 
16
- appraise 'activemodel-4.2' do
17
- gem 'activemodel', '4.2.7.1'
18
- gem 'rack', '1.6.5'
17
+ appraise 'activemodel-6.0' do
18
+ gem 'activemodel', '6.0.3'
19
19
  end
20
20
 
21
- appraise 'activemodel-5.0' do
22
- gem 'activemodel', '5.0.1'
21
+ appraise 'activemodel-6.1' do
22
+ gem 'activemodel', '6.1.0'
23
23
  end
data/CHANGELOG.md CHANGED
@@ -1,3 +1,23 @@
1
+ # 3.0.0
2
+
3
+ * [#32](https://github.com/musaffa/file_validators/pull/32) Removed cocaine/terrapin. Added options for choosing MIME type analyzers with `:tool` option.
4
+ * [#40](https://github.com/musaffa/file_validators/pull/40) Added Support for Ruby 3.
5
+ * Rubocop style guide
6
+
7
+ # 3.0.0.beta2
8
+
9
+ * [#32](https://github.com/musaffa/file_validators/pull/32) Removed terrapin. Added options for choosing MIME type analyzers with `:tool` option.
10
+
11
+ # 3.0.0.beta1
12
+
13
+ * [#29](https://github.com/musaffa/file_validators/pull/29) Upgrade cocaine to terrapin
14
+ * Rubocop style guide
15
+
16
+ # 2.3.0
17
+
18
+ * [#19](https://github.com/musaffa/file_validators/pull/19) Return false with blank size
19
+ * [#27](https://github.com/musaffa/file_validators/pull/27) Fix file size validator for ActiveStorage
20
+
1
21
  # 2.2.0-beta.1
2
22
 
3
23
  * [#17](https://github.com/musaffa/file_validators/pull/17) Now Supports multiple file uploads
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source 'https://rubygems.org'
2
4
 
3
5
  # Declare your gem's dependencies in file_validators.gemspec.
data/README.md CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/file_validators.svg)](http://badge.fury.io/rb/file_validators)
4
4
  [![Build Status](https://travis-ci.org/musaffa/file_validators.svg)](https://travis-ci.org/musaffa/file_validators)
5
- [![Dependency Status](https://gemnasium.com/musaffa/file_validators.svg)](https://gemnasium.com/musaffa/file_validators)
6
5
  [![Coverage Status](https://coveralls.io/repos/musaffa/file_validators/badge.png)](https://coveralls.io/r/musaffa/file_validators)
7
6
  [![Code Climate](https://codeclimate.com/github/musaffa/file_validators/badges/gpa.svg)](https://codeclimate.com/github/musaffa/file_validators)
8
7
  [![Inline docs](http://inch-ci.org/github/musaffa/file_validators.svg)](http://inch-ci.org/github/musaffa/file_validators)
@@ -12,8 +11,8 @@ Any module that uses ActiveModel, for example ActiveRecord, can use these file v
12
11
 
13
12
  ## Support
14
13
 
15
- * ActiveModel versions: 3.2, 4 and 5.
16
- * Rails versions: 3.2, 4 and 5.
14
+ * ActiveModel versions: 3.2, 4, 5 and 6.
15
+ * Rails versions: 3.2, 4, 5 and 6.
17
16
 
18
17
  As of version `2.2`, activemodel 3.0 and 3.1 will no longer be supported.
19
18
  For activemodel 3.0 and 3.1, please use file_validators version `<= 2.1`.
@@ -39,7 +38,7 @@ class Profile
39
38
 
40
39
  attr_accessor :avatar
41
40
  validates :avatar, file_size: { less_than_or_equal_to: 100.kilobytes },
42
- file_content_type: { allow: ['image/jpeg', 'image/png'] }
41
+ file_content_type: { allow: ['image/jpeg', 'image/png'] }
43
42
  end
44
43
  ```
45
44
  ActiveRecord example:
@@ -67,23 +66,23 @@ validates :avatar, file_size: { less_than: 2.gigabytes }
67
66
  ```
68
67
  * `less_than_or_equal_to`: Less than or equal to a number in bytes or a proc that returns a number
69
68
  ```ruby
70
- validates :avatar, file_size: { less_than_or_equal_to: 50.bytes }
69
+ validates :avatar, file_size: { less_than_or_equal_to: 50.bytes }
71
70
  ```
72
71
  * `greater_than`: greater than a number in bytes or a proc that returns a number
73
72
  ```ruby
74
- validates :avatar, file_size: { greater_than: 1.byte }
73
+ validates :avatar, file_size: { greater_than: 1.byte }
75
74
  ```
76
75
  * `greater_than_or_equal_to`: Greater than or equal to a number in bytes or a proc that returns a number
77
76
  ```ruby
78
- validates :avatar, file_size: { greater_than_or_equal_to: 50.bytes }
77
+ validates :avatar, file_size: { greater_than_or_equal_to: 50.bytes }
79
78
  ```
80
- * `message`: Error message to display. With all the options above except `:in`, you will get `count` as a replacement.
81
- With `:in` you will get `min` and `max` as replacements.
79
+ * `message`: Error message to display. With all the options above except `:in`, you will get `count` as a replacement.
80
+ With `:in` you will get `min` and `max` as replacements.
82
81
  `count`, `min` and `max` each will have its value and unit together.
83
82
  You can write error messages without using any replacement.
84
83
  ```ruby
85
84
  validates :avatar, file_size: { less_than: 100.kilobytes,
86
- message: 'avatar should be less than %{count}' }
85
+ message: 'avatar should be less than %{count}' }
87
86
  ```
88
87
  ```ruby
89
88
  validates :document, file_size: { in: 1.kilobyte..1.megabyte,
@@ -142,8 +141,13 @@ validates :video, file_content_type: { allow: lambda { |record| record.content_t
142
141
  can be a String or a Regexp. It also accepts `proc`. See `:allow` options examples.
143
142
  * `mode`: `:strict` or `:relaxed`. `:strict` mode can detect content type based on the contents
144
143
  of the files. It also detects media type spoofing (see more in [security](#security)).
145
- `:relaxed` mode uses file name to detect the content type using `mime-types` gem.
146
- If mode option is not set then the validator uses form supplied content type.
144
+ `:file` analyzer is used in `:strict` mode. `:relaxed` mode uses file name to detect
145
+ the content type. `mime_types` analyzer is used in `relaxed` mode. If mode option is not
146
+ set then the validator uses form supplied content type.
147
+ * `tool`: `:file`, `:fastimage`, `:filemagic`, `:mimemagic`, `:marcel`, `:mime_types`, `:mini_mime`.
148
+ You can choose one of these built-in MIME type analyzers. You have to install the analyzer gem you choose.
149
+ By default supplied content type is used to determine the MIME type. This option takes precedence
150
+ over `mode` option.
147
151
  ```ruby
148
152
  validates :avatar, file_content_type: { allow: 'image/jpeg', mode: :strict }
149
153
  validates :avatar, file_content_type: { allow: 'image/jpeg', mode: :relaxed }
@@ -172,7 +176,7 @@ validates :avatar, file_content_type: { allow: /^image\/.*/, exclude: ['image/pn
172
176
  This gem can use Unix file command to get the content type based on the content of the file rather
173
177
  than the extension. This prevents fake content types inserted in the request header.
174
178
 
175
- It also prevents file media type spoofing. For example, user may upload a .html document as
179
+ It also prevents file media type spoofing. For example, user may upload a .html document as
176
180
  a part of the EXIF header of a valid JPEG file. Content type validator will identify its content type
177
181
  as `image/jpeg` and, without spoof detection, it may pass the validation and be saved as .html document
178
182
  thus exposing your application to a security vulnerability. Media type spoof detector wont let that happen.
@@ -180,8 +184,8 @@ It will not allow a file having `image/jpeg` content type to be saved as `text/p
180
184
  type mismatch, for example `text` of `text/plain` and `image` of `image/jpeg`. So it will not prevent
181
185
  `image/jpeg` from saving as `image/png` as both have the same `image` media type.
182
186
 
183
- **note**: This security feature is disabled by default. To enable it, first add `cocaine` gem in
184
- your Gemfile and then add `mode: :strict` option in [content type validations](#file-content-type-validator).
187
+ **note**: This security feature is disabled by default. To enable it, add `mode: :strict` option
188
+ in [content type validations](#file-content-type-validator).
185
189
  `:strict` mode may not work in direct file uploading systems as the file is not passed along with the form.
186
190
 
187
191
  ## i18n Translations
@@ -201,7 +205,7 @@ of the file matches anyone of them. takes `types` as replacement.
201
205
 
202
206
  This gem provides `en` translations for this errors under `errors.messages` namespace.
203
207
  If you want to override and/or create other locales, you can
204
- check [this](https://github.com/musaffa/file_validators/blob/master/lib/file_validators/locale/en.yml) out to see how translations are done.
208
+ check [this](https://github.com/musaffa/file_validators/blob/master/lib/file_validators/locale/en.yml) out to see how translations are done.
205
209
 
206
210
  You can override all of them with the `:message` option.
207
211
 
@@ -254,6 +258,7 @@ uploaders start processing a file immediately after its assignment (even before
254
258
  $ rake
255
259
  $ rake test:unit
256
260
  $ rake test:integration
261
+ $ rubocop
257
262
 
258
263
  # test different active model versions
259
264
  $ bundle exec appraisal install
data/Rakefile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  begin
2
4
  require 'bundler/setup'
3
5
  rescue LoadError
@@ -16,7 +18,7 @@ namespace :test do
16
18
  end
17
19
  end
18
20
 
19
- task :default => ['test:unit', 'test:integration']
21
+ task default: ['test:unit', 'test:integration']
20
22
 
21
23
  # require 'rdoc/task'
22
24
 
@@ -1,4 +1,6 @@
1
- $:.push File.expand_path('../lib', __FILE__)
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.push File.expand_path('lib', __dir__)
2
4
 
3
5
  require 'file_validators/version'
4
6
 
@@ -12,17 +14,21 @@ Gem::Specification.new do |s|
12
14
  s.homepage = 'https://github.com/musaffa/file_validators'
13
15
  s.license = 'MIT'
14
16
 
15
- s.files = `git ls-files`.split($/)
17
+ s.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
16
18
  s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
19
  s.test_files = s.files.grep(%r{^spec/})
18
- s.require_paths = ['lib']
20
+ s.require_paths = ['lib']
19
21
 
20
22
  s.add_dependency 'activemodel', '>= 3.2'
21
23
  s.add_dependency 'mime-types', '>= 1.0'
22
24
 
23
- s.add_development_dependency 'cocaine', '~> 0.5.4'
24
- s.add_development_dependency 'rake'
25
- s.add_development_dependency 'rspec', '~> 3.5.0'
26
25
  s.add_development_dependency 'coveralls'
26
+ s.add_development_dependency 'fastimage'
27
+ s.add_development_dependency 'marcel', '~> 0.3' if RUBY_VERSION >= '2.2.0'
28
+ s.add_development_dependency 'mimemagic', '>= 0.3.2'
29
+ s.add_development_dependency 'mini_mime', '~> 1.0'
27
30
  s.add_development_dependency 'rack-test'
31
+ s.add_development_dependency 'rake'
32
+ s.add_development_dependency 'rspec', '~> 3.5.0'
33
+ s.add_development_dependency 'rubocop', '~> 0.58.2'
28
34
  end
File without changes
File without changes
File without changes
@@ -3,7 +3,6 @@
3
3
  source "https://rubygems.org"
4
4
 
5
5
  gem "appraisal"
6
- gem "activemodel", "4.1.6"
7
- gem "rack", "1.6.5"
6
+ gem "activemodel", "6.0.3"
8
7
 
9
8
  gemspec :path => "../"
@@ -3,7 +3,6 @@
3
3
  source "https://rubygems.org"
4
4
 
5
5
  gem "appraisal"
6
- gem "activemodel", "4.2.7.1"
7
- gem "rack", "1.6.5"
6
+ gem "activemodel", "6.1.0"
8
7
 
9
8
  gemspec :path => "../"
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FileValidators
4
+ class Error < StandardError
5
+ end
6
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Extracted from shrine/plugins/determine_mime_type.rb
4
+ module FileValidators
5
+ class MimeTypeAnalyzer
6
+ SUPPORTED_TOOLS = %i[fastimage file filemagic mimemagic marcel mime_types mini_mime].freeze
7
+ MAGIC_NUMBER = 256 * 1024
8
+
9
+ def initialize(tool)
10
+ raise Error, "unknown mime type analyzer #{tool.inspect}, supported analyzers are: #{SUPPORTED_TOOLS.join(',')}" unless SUPPORTED_TOOLS.include?(tool)
11
+
12
+ @tool = tool
13
+ end
14
+
15
+ def call(io)
16
+ mime_type = send(:"extract_with_#{@tool}", io)
17
+ io.rewind
18
+
19
+ mime_type
20
+ end
21
+
22
+ private
23
+
24
+ def extract_with_file(io)
25
+ require 'open3'
26
+
27
+ return nil if io.eof? # file command returns "application/x-empty" for empty files
28
+
29
+ Open3.popen3(*%W[file --mime-type --brief -]) do |stdin, stdout, stderr, thread|
30
+ begin
31
+ IO.copy_stream(io, stdin.binmode)
32
+ rescue Errno::EPIPE
33
+ end
34
+ stdin.close
35
+
36
+ status = thread.value
37
+
38
+ raise Error, "file command failed to spawn: #{stderr.read}" if status.nil?
39
+ raise Error, "file command failed: #{stderr.read}" unless status.success?
40
+ $stderr.print(stderr.read)
41
+
42
+ stdout.read.strip
43
+ end
44
+ rescue Errno::ENOENT
45
+ raise Error, 'file command-line tool is not installed'
46
+ end
47
+
48
+ def extract_with_fastimage(io)
49
+ require 'fastimage'
50
+
51
+ type = FastImage.type(io)
52
+ "image/#{type}" if type
53
+ end
54
+
55
+ def extract_with_filemagic(io)
56
+ require 'filemagic'
57
+
58
+ return nil if io.eof? # FileMagic returns "application/x-empty" for empty files
59
+
60
+ FileMagic.open(FileMagic::MAGIC_MIME_TYPE) do |filemagic|
61
+ filemagic.buffer(io.read(MAGIC_NUMBER))
62
+ end
63
+ end
64
+
65
+ def extract_with_mimemagic(io)
66
+ require 'mimemagic'
67
+
68
+ mime = MimeMagic.by_magic(io)
69
+ mime.type if mime
70
+ end
71
+
72
+ def extract_with_marcel(io)
73
+ require 'marcel'
74
+
75
+ return nil if io.eof? # marcel returns "application/octet-stream" for empty files
76
+
77
+ Marcel::MimeType.for(io)
78
+ end
79
+
80
+ def extract_with_mime_types(io)
81
+ require 'mime/types'
82
+
83
+ if filename = extract_filename(io)
84
+ mime_type = MIME::Types.of(filename).first
85
+ mime_type.content_type if mime_type
86
+ end
87
+ end
88
+
89
+ def extract_with_mini_mime(io)
90
+ require 'mini_mime'
91
+
92
+ if filename = extract_filename(io)
93
+ info = MiniMime.lookup_by_filename(filename)
94
+ info.content_type if info
95
+ end
96
+ end
97
+
98
+ def extract_filename(io)
99
+ if io.respond_to?(:original_filename)
100
+ io.original_filename
101
+ elsif io.respond_to?(:path)
102
+ File.basename(io.path)
103
+ end
104
+ end
105
+ end
106
+ end
@@ -1,25 +1,35 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveModel
2
4
  module Validations
3
-
4
5
  class FileContentTypeValidator < ActiveModel::EachValidator
5
- CHECKS = [:allow, :exclude].freeze
6
+ CHECKS = %i[allow exclude].freeze
7
+ SUPPORTED_MODES = { relaxed: :mime_types, strict: :file }.freeze
6
8
 
7
9
  def self.helper_method_name
8
10
  :validates_file_content_type
9
11
  end
10
12
 
11
13
  def validate_each(record, attribute, value)
12
- values = parse_values(value)
13
- unless values.empty?
14
- mode = option_value(record, :mode)
15
- allowed_types = option_content_types(record, :allow)
16
- forbidden_types = option_content_types(record, :exclude)
17
-
18
- values.each do |value|
19
- content_type = get_content_type(value, mode)
20
- validate_whitelist(record, attribute, content_type, allowed_types)
21
- validate_blacklist(record, attribute, content_type, forbidden_types)
22
- end
14
+ begin
15
+ values = parse_values(value)
16
+ rescue JSON::ParserError
17
+ record.errors.add attribute, :invalid
18
+ return
19
+ end
20
+
21
+ return if values.empty?
22
+
23
+ mode = option_value(record, :mode)
24
+ tool = option_value(record, :tool) || SUPPORTED_MODES[mode]
25
+
26
+ allowed_types = option_content_types(record, :allow)
27
+ forbidden_types = option_content_types(record, :exclude)
28
+
29
+ values.each do |val|
30
+ content_type = get_content_type(val, tool)
31
+ validate_whitelist(record, attribute, content_type, allowed_types)
32
+ validate_blacklist(record, attribute, content_type, forbidden_types)
23
33
  end
24
34
  end
25
35
 
@@ -42,34 +52,12 @@ module ActiveModel
42
52
 
43
53
  value = JSON.parse(value) if value.is_a?(String)
44
54
 
45
- Array.wrap(value).reject { |value| value.blank? }
55
+ Array.wrap(value).reject(&:blank?)
46
56
  end
47
57
 
48
- def get_file_path(value)
49
- if value.try(:path)
50
- value.path
51
- else
52
- raise ArgumentError, 'value must return a file path in order to validate file content type'
53
- end
54
- end
55
-
56
- def get_file_name(value)
57
- if value.try(:original_filename)
58
- value.original_filename
59
- else
60
- File.basename(get_file_path(value))
61
- end
62
- end
63
-
64
- def get_content_type(value, mode)
65
- case mode
66
- when :strict
67
- file_path = get_file_path(value)
68
- file_name = get_file_name(value)
69
- FileValidators::Utils::ContentTypeDetector.new(file_path, file_name).detect
70
- when :relaxed
71
- file_name = get_file_name(value)
72
- MIME::Types.type_for(file_name).first
58
+ def get_content_type(value, tool)
59
+ if tool.present?
60
+ FileValidators::MimeTypeAnalyzer.new(tool).call(value)
73
61
  else
74
62
  value = OpenStruct.new(value) if value.is_a?(Hash)
75
63
  value.content_type
@@ -85,7 +73,7 @@ module ActiveModel
85
73
  end
86
74
 
87
75
  def validate_whitelist(record, attribute, content_type, allowed_types)
88
- if allowed_types.present? and allowed_types.none? { |type| type === content_type }
76
+ if allowed_types.present? && allowed_types.none? { |type| type === content_type }
89
77
  mark_invalid record, attribute, :allowed_file_content_types, allowed_types
90
78
  end
91
79
  end
@@ -99,7 +87,7 @@ module ActiveModel
99
87
  def mark_invalid(record, attribute, error, option_types)
100
88
  error_options = options.merge(types: option_types.join(', '))
101
89
  unless record.errors.added?(attribute, error, error_options)
102
- record.errors.add attribute, error, error_options
90
+ record.errors.add attribute, error, **error_options
103
91
  end
104
92
  end
105
93
  end
@@ -123,6 +111,10 @@ module ActiveModel
123
111
  # :relaxed validates the content type based on the file name using
124
112
  # the mime-types gem. It's only for sanity check.
125
113
  # If mode is not set then it uses form supplied content type.
114
+ # * +tool+: :file, :fastimage, :filemagic, :mimemagic, :marcel, :mime_types, :mini_mime
115
+ # You can choose a different built-in MIME type analyzer
116
+ # By default supplied content type is used to determine the MIME type
117
+ # This option have precedence over mode option
126
118
  # * +if+: A lambda or name of an instance method. Validation will only
127
119
  # be run is this lambda or method returns true.
128
120
  # * +unless+: Same as +if+ but validates if lambda or method returns false.
@@ -130,6 +122,5 @@ module ActiveModel
130
122
  validates_with FileContentTypeValidator, _merge_attributes(attr_names)
131
123
  end
132
124
  end
133
-
134
125
  end
135
126
  end