carrierwave 2.2.6 → 3.1.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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +180 -62
  3. data/lib/carrierwave/compatibility/paperclip.rb +4 -2
  4. data/lib/carrierwave/downloader/base.rb +20 -12
  5. data/lib/carrierwave/downloader/remote_file.rb +13 -10
  6. data/lib/carrierwave/locale/en.yml +5 -3
  7. data/lib/carrierwave/mount.rb +36 -50
  8. data/lib/carrierwave/mounter.rb +118 -50
  9. data/lib/carrierwave/orm/activerecord.rb +21 -62
  10. data/lib/carrierwave/processing/mini_magick.rb +45 -14
  11. data/lib/carrierwave/processing/rmagick.rb +47 -20
  12. data/lib/carrierwave/processing/vips.rb +43 -12
  13. data/lib/carrierwave/sanitized_file.rb +58 -77
  14. data/lib/carrierwave/storage/abstract.rb +5 -5
  15. data/lib/carrierwave/storage/file.rb +6 -5
  16. data/lib/carrierwave/storage/fog.rb +86 -65
  17. data/lib/carrierwave/test/matchers.rb +11 -7
  18. data/lib/carrierwave/uploader/cache.rb +19 -11
  19. data/lib/carrierwave/uploader/callbacks.rb +1 -1
  20. data/lib/carrierwave/uploader/configuration.rb +18 -8
  21. data/lib/carrierwave/uploader/{content_type_whitelist.rb → content_type_allowlist.rb} +17 -15
  22. data/lib/carrierwave/uploader/{content_type_blacklist.rb → content_type_denylist.rb} +20 -15
  23. data/lib/carrierwave/uploader/dimension.rb +66 -0
  24. data/lib/carrierwave/uploader/{extension_whitelist.rb → extension_allowlist.rb} +17 -15
  25. data/lib/carrierwave/uploader/{extension_blacklist.rb → extension_denylist.rb} +19 -14
  26. data/lib/carrierwave/uploader/file_size.rb +2 -2
  27. data/lib/carrierwave/uploader/processing.rb +34 -6
  28. data/lib/carrierwave/uploader/proxy.rb +16 -3
  29. data/lib/carrierwave/uploader/store.rb +70 -6
  30. data/lib/carrierwave/uploader/url.rb +1 -1
  31. data/lib/carrierwave/uploader/versions.rb +158 -138
  32. data/lib/carrierwave/uploader.rb +10 -8
  33. data/lib/carrierwave/utilities/file_name.rb +47 -0
  34. data/lib/carrierwave/utilities/uri.rb +14 -11
  35. data/lib/carrierwave/utilities.rb +1 -0
  36. data/lib/carrierwave/validations/active_model.rb +4 -6
  37. data/lib/carrierwave/version.rb +1 -1
  38. data/lib/carrierwave.rb +18 -17
  39. data/lib/generators/templates/{uploader.rb → uploader.rb.erb} +1 -1
  40. data/lib/generators/uploader_generator.rb +3 -3
  41. metadata +33 -59
@@ -1,10 +1,10 @@
1
1
  module CarrierWave
2
2
  module Uploader
3
- module ContentTypeBlacklist
3
+ module ContentTypeDenylist
4
4
  extend ActiveSupport::Concern
5
5
 
6
6
  included do
7
- before :cache, :check_content_type_blacklist!
7
+ before :cache, :check_content_type_denylist!
8
8
  end
9
9
 
10
10
  ##
@@ -29,29 +29,34 @@ module CarrierWave
29
29
  # end
30
30
  #
31
31
  def content_type_denylist
32
- if respond_to?(:content_type_blacklist)
33
- ActiveSupport::Deprecation.warn "#content_type_blacklist is deprecated, use #content_type_denylist instead." unless instance_variable_defined?(:@content_type_blacklist_warned)
34
- @content_type_blacklist_warned = true
35
- content_type_blacklist
36
- end
37
32
  end
38
33
 
39
34
  private
40
35
 
41
- def check_content_type_blacklist!(new_file)
42
- return unless content_type_denylist
36
+ def check_content_type_denylist!(new_file)
37
+ denylist = content_type_denylist
38
+ if !denylist && respond_to?(:content_type_blacklist) && content_type_blacklist
39
+ CarrierWave.deprecator.warn "#content_type_blacklist is deprecated, use #content_type_denylist instead." unless instance_variable_defined?(:@content_type_blacklist_warned)
40
+ @content_type_blacklist_warned = true
41
+ denylist = content_type_blacklist
42
+ end
43
+
44
+ return unless denylist
45
+
46
+ CarrierWave.deprecator.warn "Use of #content_type_denylist is deprecated for the security reason, use #content_type_allowlist instead to explicitly state what are safe to accept" unless instance_variable_defined?(:@content_type_denylist_warned)
47
+ @content_type_denylist_warned = true
43
48
 
44
49
  content_type = new_file.content_type
45
- if blacklisted_content_type?(content_type)
46
- raise CarrierWave::IntegrityError, I18n.translate(:"errors.messages.content_type_blacklist_error",
47
- content_type: content_type, default: :"errors.messages.content_type_denylist_error")
50
+ if denylisted_content_type?(denylist, content_type)
51
+ raise CarrierWave::IntegrityError, I18n.translate(:"errors.messages.content_type_denylist_error",
52
+ content_type: content_type, default: :"errors.messages.content_type_blacklist_error")
48
53
  end
49
54
  end
50
55
 
51
- def blacklisted_content_type?(content_type)
52
- Array(content_type_denylist).any? { |item| content_type =~ /#{item}/ }
56
+ def denylisted_content_type?(denylist, content_type)
57
+ Array(denylist).any? { |item| content_type =~ /#{item}/ }
53
58
  end
54
59
 
55
- end # ContentTypeBlacklist
60
+ end # ContentTypeDenylist
56
61
  end # Uploader
57
62
  end # CarrierWave
@@ -0,0 +1,66 @@
1
+ require 'active_support'
2
+
3
+ module CarrierWave
4
+ module Uploader
5
+ module Dimension
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ before :cache, :check_dimensions!
10
+ end
11
+
12
+ ##
13
+ # Override this method in your uploader to provide a Range of width which
14
+ # are allowed to be uploaded.
15
+ # === Returns
16
+ #
17
+ # [NilClass, Range] a width range which are permitted to be uploaded
18
+ #
19
+ # === Examples
20
+ #
21
+ # def width_range
22
+ # 1000..2000
23
+ # end
24
+ #
25
+ def width_range; end
26
+
27
+ ##
28
+ # Override this method in your uploader to provide a Range of height which
29
+ # are allowed to be uploaded.
30
+ # === Returns
31
+ #
32
+ # [NilClass, Range] a height range which are permitted to be uploaded
33
+ #
34
+ # === Examples
35
+ #
36
+ # def height_range
37
+ # 1000..
38
+ # end
39
+ #
40
+ def height_range; end
41
+
42
+ private
43
+
44
+ def check_dimensions!(new_file)
45
+ # NOTE: Skip the check for resized images
46
+ return if version_name.present?
47
+ return unless width_range || height_range
48
+
49
+ unless respond_to?(:width) || respond_to?(:height)
50
+ raise 'You need to include one of CarrierWave::MiniMagick, CarrierWave::RMagick, or CarrierWave::Vips to perform image dimension validation'
51
+ end
52
+
53
+ if width_range&.begin && width < width_range.begin
54
+ raise CarrierWave::IntegrityError, I18n.translate(:"errors.messages.min_width_error", :min_width => ActiveSupport::NumberHelper.number_to_delimited(width_range.begin))
55
+ elsif width_range&.end && width > width_range.end
56
+ raise CarrierWave::IntegrityError, I18n.translate(:"errors.messages.max_width_error", :max_width => ActiveSupport::NumberHelper.number_to_delimited(width_range.end))
57
+ elsif height_range&.begin && height < height_range.begin
58
+ raise CarrierWave::IntegrityError, I18n.translate(:"errors.messages.min_height_error", :min_height => ActiveSupport::NumberHelper.number_to_delimited(height_range.begin))
59
+ elsif height_range&.end && height > height_range.end
60
+ raise CarrierWave::IntegrityError, I18n.translate(:"errors.messages.max_height_error", :max_height => ActiveSupport::NumberHelper.number_to_delimited(height_range.end))
61
+ end
62
+ end
63
+
64
+ end # Dimension
65
+ end # Uploader
66
+ end # CarrierWave
@@ -1,10 +1,10 @@
1
1
  module CarrierWave
2
2
  module Uploader
3
- module ExtensionWhitelist
3
+ module ExtensionAllowlist
4
4
  extend ActiveSupport::Concern
5
5
 
6
6
  included do
7
- before :cache, :check_extension_whitelist!
7
+ before :cache, :check_extension_allowlist!
8
8
  end
9
9
 
10
10
  ##
@@ -32,30 +32,32 @@ module CarrierWave
32
32
  # end
33
33
  #
34
34
  def extension_allowlist
35
- if respond_to?(:extension_whitelist)
36
- ActiveSupport::Deprecation.warn "#extension_whitelist is deprecated, use #extension_allowlist instead." unless instance_variable_defined?(:@extension_whitelist_warned)
37
- @extension_whitelist_warned = true
38
- extension_whitelist
39
- end
40
35
  end
41
36
 
42
37
  private
43
38
 
44
- def check_extension_whitelist!(new_file)
45
- return unless extension_allowlist
39
+ def check_extension_allowlist!(new_file)
40
+ allowlist = extension_allowlist
41
+ if !allowlist && respond_to?(:extension_whitelist) && extension_whitelist
42
+ CarrierWave.deprecator.warn "#extension_whitelist is deprecated, use #extension_allowlist instead." unless instance_variable_defined?(:@extension_whitelist_warned)
43
+ @extension_whitelist_warned = true
44
+ allowlist = extension_whitelist
45
+ end
46
+
47
+ return unless allowlist
46
48
 
47
49
  extension = new_file.extension.to_s
48
- if !whitelisted_extension?(extension)
50
+ if !allowlisted_extension?(allowlist, extension)
49
51
  # Look for whitelist first, then fallback to allowlist
50
- raise CarrierWave::IntegrityError, I18n.translate(:"errors.messages.extension_whitelist_error", extension: new_file.extension.inspect,
51
- allowed_types: Array(extension_allowlist).join(", "), default: :"errors.messages.extension_allowlist_error")
52
+ raise CarrierWave::IntegrityError, I18n.translate(:"errors.messages.extension_allowlist_error", extension: new_file.extension.inspect,
53
+ allowed_types: Array(allowlist).join(", "), default: :"errors.messages.extension_whitelist_error")
52
54
  end
53
55
  end
54
56
 
55
- def whitelisted_extension?(extension)
57
+ def allowlisted_extension?(allowlist, extension)
56
58
  downcase_extension = extension.downcase
57
- Array(extension_allowlist).any? { |item| downcase_extension =~ /\A#{item}\z/i }
59
+ Array(allowlist).any? { |item| downcase_extension =~ /\A#{item}\z/i }
58
60
  end
59
- end # ExtensionWhitelist
61
+ end # ExtensionAllowlist
60
62
  end # Uploader
61
63
  end # CarrierWave
@@ -1,10 +1,10 @@
1
1
  module CarrierWave
2
2
  module Uploader
3
- module ExtensionBlacklist
3
+ module ExtensionDenylist
4
4
  extend ActiveSupport::Concern
5
5
 
6
6
  included do
7
- before :cache, :check_extension_blacklist!
7
+ before :cache, :check_extension_denylist!
8
8
  end
9
9
 
10
10
  ##
@@ -32,27 +32,32 @@ module CarrierWave
32
32
  # end
33
33
  #
34
34
  def extension_denylist
35
- if respond_to?(:extension_blacklist)
36
- ActiveSupport::Deprecation.warn "#extension_blacklist is deprecated, use #extension_denylist instead." unless instance_variable_defined?(:@extension_blacklist_warned)
37
- @extension_blacklist_warned = true
38
- extension_blacklist
39
- end
40
35
  end
41
36
 
42
37
  private
43
38
 
44
- def check_extension_blacklist!(new_file)
45
- return unless extension_denylist
39
+ def check_extension_denylist!(new_file)
40
+ denylist = extension_denylist
41
+ if !denylist && respond_to?(:extension_blacklist) && extension_blacklist
42
+ CarrierWave.deprecator.warn "#extension_blacklist is deprecated, use #extension_denylist instead." unless instance_variable_defined?(:@extension_blacklist_warned)
43
+ @extension_blacklist_warned = true
44
+ denylist = extension_blacklist
45
+ end
46
+
47
+ return unless denylist
48
+
49
+ CarrierWave.deprecator.warn "Use of #extension_denylist is deprecated for the security reason, use #extension_allowlist instead to explicitly state what are safe to accept" unless instance_variable_defined?(:@extension_denylist_warned)
50
+ @extension_denylist_warned = true
46
51
 
47
52
  extension = new_file.extension.to_s
48
- if blacklisted_extension?(extension)
49
- raise CarrierWave::IntegrityError, I18n.translate(:"errors.messages.extension_blacklist_error", extension: new_file.extension.inspect,
50
- prohibited_types: Array(extension_denylist).join(", "), default: :"errors.messages.extension_denylist_error")
53
+ if denylisted_extension?(denylist, extension)
54
+ raise CarrierWave::IntegrityError, I18n.translate(:"errors.messages.extension_denylist_error", extension: new_file.extension.inspect,
55
+ prohibited_types: Array(extension_denylist).join(", "), default: :"errors.messages.extension_blacklist_error")
51
56
  end
52
57
  end
53
58
 
54
- def blacklisted_extension?(extension)
55
- Array(extension_denylist).any? { |item| extension =~ /\A#{item}\z/i }
59
+ def denylisted_extension?(denylist, extension)
60
+ Array(denylist).any? { |item| extension =~ /\A#{item}\z/i }
56
61
  end
57
62
  end
58
63
  end
@@ -14,7 +14,7 @@ module CarrierWave
14
14
  # are allowed to be uploaded.
15
15
  # === Returns
16
16
  #
17
- # [NilClass, Range] a size range which are permitted to be uploaded
17
+ # [NilClass, Range] a size range (in bytes) which are permitted to be uploaded
18
18
  #
19
19
  # === Examples
20
20
  #
@@ -24,7 +24,7 @@ module CarrierWave
24
24
  #
25
25
  def size_range; end
26
26
 
27
- private
27
+ private
28
28
 
29
29
  def check_size!(new_file)
30
30
  size = new_file.size
@@ -18,7 +18,7 @@ module CarrierWave
18
18
  # Adds a processor callback which applies operations as a file is uploaded.
19
19
  # The argument may be the name of any method of the uploader, expressed as a symbol,
20
20
  # or a list of such methods, or a hash where the key is a method and the value is
21
- # an array of arguments to call the method with
21
+ # an array of arguments to call the method with. Also accepts an :if or :unless condition
22
22
  #
23
23
  # === Parameters
24
24
  #
@@ -31,6 +31,7 @@ module CarrierWave
31
31
  # process :sepiatone, :vignette
32
32
  # process :scale => [200, 200]
33
33
  # process :scale => [200, 200], :if => :image?
34
+ # process :scale => [200, 200], :unless => :disallowed_image_type?
34
35
  # process :sepiatone, :if => :image?
35
36
  #
36
37
  # def sepiatone
@@ -49,6 +50,10 @@ module CarrierWave
49
50
  # ...
50
51
  # end
51
52
  #
53
+ # def disallowed_image_type?
54
+ # ...
55
+ # end
56
+ #
52
57
  # end
53
58
  #
54
59
  def process(*args)
@@ -57,12 +62,20 @@ module CarrierWave
57
62
  hash.merge!(arg)
58
63
  end
59
64
 
60
- condition = new_processors.delete(:if)
65
+ condition_type = new_processors.keys.detect { |key| [:if, :unless].include?(key) }
66
+ condition = new_processors.delete(:if) || new_processors.delete(:unless)
61
67
  new_processors.each do |processor, processor_args|
62
- self.processors += [[processor, processor_args, condition]]
68
+ self.processors += [[processor, processor_args, condition, condition_type]]
69
+
70
+ if processor == :convert
71
+ # Treat :convert specially, since it should trigger the file extension change
72
+ force_extension processor_args
73
+ if condition
74
+ warn "Use of 'process convert: format' with conditionals has an issue and doesn't work correctly. See https://github.com/carrierwaveuploader/carrierwave/issues/2723 for details. "
75
+ end
76
+ end
63
77
  end
64
78
  end
65
-
66
79
  end # ClassMethods
67
80
 
68
81
  ##
@@ -72,13 +85,19 @@ module CarrierWave
72
85
  return unless enable_processing
73
86
 
74
87
  with_callbacks(:process, new_file) do
75
- self.class.processors.each do |method, args, condition|
76
- if(condition)
88
+ self.class.processors.each do |method, args, condition, condition_type|
89
+ if condition && condition_type == :if
77
90
  if condition.respond_to?(:call)
78
91
  next unless condition.call(self, :args => args, :method => method, :file => new_file)
79
92
  else
80
93
  next unless self.send(condition, new_file)
81
94
  end
95
+ elsif condition && condition_type == :unless
96
+ if condition.respond_to?(:call)
97
+ next if condition.call(self, :args => args, :method => method, :file => new_file)
98
+ elsif self.send(condition, new_file)
99
+ next
100
+ end
82
101
  end
83
102
 
84
103
  if args.is_a? Array
@@ -95,6 +114,15 @@ module CarrierWave
95
114
  end
96
115
  end
97
116
 
117
+ private
118
+
119
+ def forcing_extension(filename)
120
+ if force_extension && filename
121
+ Pathname.new(filename).sub_ext(".#{force_extension.to_s.delete_prefix('.')}").to_s
122
+ else
123
+ filename
124
+ end
125
+ end
98
126
  end # Processing
99
127
  end # Uploader
100
128
  end # CarrierWave
@@ -30,7 +30,20 @@ module CarrierWave
30
30
  # [String] uniquely identifies a file
31
31
  #
32
32
  def identifier
33
- @identifier || storage.try(:identifier)
33
+ @identifier || (file && storage.try(:identifier))
34
+ end
35
+
36
+ ##
37
+ # Returns a String which is to be used as a temporary value which gets assigned to the column.
38
+ # The purpose is to mark the column that it will be updated. Finally before the save it will be
39
+ # overwritten by the #identifier value, which is usually #filename.
40
+ #
41
+ # === Returns
42
+ #
43
+ # [String] a temporary_identifier, by default the value of #cache_name is used
44
+ #
45
+ def temporary_identifier
46
+ cache_name || @identifier
34
47
  end
35
48
 
36
49
  ##
@@ -40,8 +53,8 @@ module CarrierWave
40
53
  #
41
54
  # [String] contents of the file
42
55
  #
43
- def read
44
- file.try(:read)
56
+ def read(*args)
57
+ file.try(:read, *args)
45
58
  end
46
59
 
47
60
  ##
@@ -11,9 +11,23 @@ module CarrierWave
11
11
  prepend Module.new {
12
12
  def initialize(*)
13
13
  super
14
- @file, @filename, @cache_id, @identifier = nil
14
+ @file, @filename, @cache_id, @identifier, @deduplication_index = nil
15
15
  end
16
16
  }
17
+
18
+ after :store, :show_warning_when_filename_is_unavailable
19
+
20
+ class_attribute :filename_safeguard_checked
21
+ end
22
+
23
+ module ClassMethods
24
+ private
25
+
26
+ def inherited(subclass)
27
+ # To perform the filename safeguard check once per a class
28
+ self.filename_safeguard_checked = false
29
+ super
30
+ end
17
31
  end
18
32
 
19
33
  ##
@@ -34,9 +48,26 @@ module CarrierWave
34
48
  @filename
35
49
  end
36
50
 
51
+ ##
52
+ # Returns a filename which doesn't conflict with already-stored files.
53
+ #
54
+ # === Returns
55
+ #
56
+ # [String] the filename with suffix added for deduplication
57
+ #
58
+ def deduplicated_filename
59
+ return unless filename
60
+ return filename unless @deduplication_index
61
+
62
+ parts = filename.split('.')
63
+ basename = parts.shift
64
+ basename.sub!(/ ?\(\d+\)\z/, '')
65
+ ([basename.to_s + (@deduplication_index > 1 ? "(#{@deduplication_index})" : '')] + parts).join('.')
66
+ end
67
+
37
68
  ##
38
69
  # Calculates the path where the file should be stored. If +for_file+ is given, it will be
39
- # used as the filename, otherwise +CarrierWave::Uploader#filename+ is assumed.
70
+ # used as the identifier, otherwise +CarrierWave::Uploader#identifier+ is assumed.
40
71
  #
41
72
  # === Parameters
42
73
  #
@@ -46,7 +77,7 @@ module CarrierWave
46
77
  #
47
78
  # [String] the store path
48
79
  #
49
- def store_path(for_file=filename)
80
+ def store_path(for_file=identifier)
50
81
  File.join([store_dir, full_filename(for_file)].compact)
51
82
  end
52
83
 
@@ -60,7 +91,7 @@ module CarrierWave
60
91
  # [new_file (File, IOString, Tempfile)] any kind of file object
61
92
  #
62
93
  def store!(new_file=nil)
63
- cache!(new_file) if new_file && ((@cache_id != parent_cache_id) || @cache_id.nil?)
94
+ cache!(new_file) if new_file && !cached?
64
95
  if !cache_only && @file && @cache_id
65
96
  with_callbacks(:store, new_file) do
66
97
  new_file = storage.store!(@file)
@@ -69,7 +100,8 @@ module CarrierWave
69
100
  cache_storage.delete_dir!(cache_path(nil))
70
101
  end
71
102
  @file = new_file
72
- @cache_id = @identifier = nil
103
+ @identifier = storage.identifier
104
+ @original_filename = @cache_id = @deduplication_index = nil
73
105
  @staged = false
74
106
  end
75
107
  end
@@ -89,10 +121,42 @@ module CarrierWave
89
121
  end
90
122
  end
91
123
 
124
+ ##
125
+ # Look for an identifier which doesn't collide with the given already-stored identifiers.
126
+ # It is done by adding a index number as the suffix.
127
+ # For example, if there's 'image.jpg' and the @deduplication_index is set to 2,
128
+ # The stored file will be named as 'image(2).jpg'.
129
+ #
130
+ # === Parameters
131
+ #
132
+ # [current_identifiers (Array[String])] List of identifiers for already-stored files
133
+ #
134
+ def deduplicate(current_identifiers)
135
+ @deduplication_index = nil
136
+ return unless current_identifiers.include?(identifier)
137
+
138
+ (1..current_identifiers.size + 1).each do |i|
139
+ @deduplication_index = i
140
+ break unless current_identifiers.include?(identifier)
141
+ end
142
+ end
143
+
92
144
  private
93
145
 
94
146
  def full_filename(for_file)
95
- for_file
147
+ forcing_extension(for_file)
148
+ end
149
+
150
+ def show_warning_when_filename_is_unavailable(_)
151
+ return if self.class.filename_safeguard_checked
152
+ self.class.filename_safeguard_checked = true
153
+ return if filename
154
+
155
+ warn <<~MESSAGE
156
+ [WARNING] Your uploader's #filename method defined at #{method(:filename).source_location.join(':')} didn't return value after storing the file.
157
+ It's likely that the method is safeguarded with `if original_filename`, which were necessary for pre-3.x CarrierWave but is no longer needed.
158
+ Removing it is recommended, as it is known to cause issues depending on the use case: https://github.com/carrierwaveuploader/carrierwave/issues/2708
159
+ MESSAGE
96
160
  end
97
161
 
98
162
  def storage
@@ -23,7 +23,7 @@ module CarrierWave
23
23
  if file.respond_to?(:path)
24
24
  path = encode_path(file.path.sub(File.expand_path(root), ''))
25
25
 
26
- if host = asset_host
26
+ if (host = asset_host)
27
27
  if host.respond_to? :call
28
28
  "#{host.call(file)}#{path}"
29
29
  else