carrierwave 3.1.4 → 4.0.0.rc

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.
@@ -28,6 +28,7 @@ module CarrierWave
28
28
 
29
29
  # fog
30
30
  add_deprecated_config :fog_provider
31
+ add_config :fog_acl
31
32
  add_config :fog_attributes
32
33
  add_config :fog_credentials
33
34
  add_config :fog_directory
@@ -45,10 +46,12 @@ module CarrierWave
45
46
  add_config :validate_processing
46
47
  add_config :validate_download
47
48
  add_config :mount_on
49
+ add_config :metadata_column
48
50
  add_config :cache_only
49
51
  add_config :download_retry_count
50
52
  add_config :download_retry_wait_time
51
53
  add_config :skip_ssrf_protection
54
+ add_config :ssrf_filter_options
52
55
 
53
56
  # set default values
54
57
  reset_config
@@ -220,6 +223,7 @@ module CarrierWave
220
223
  config.download_retry_count = 0
221
224
  config.download_retry_wait_time = 5
222
225
  config.skip_ssrf_protection = false
226
+ config.ssrf_filter_options = {}
223
227
  end
224
228
  end
225
229
  end
@@ -0,0 +1,51 @@
1
+ module CarrierWave
2
+ module Uploader
3
+ module DirectUpload
4
+ extend ActiveSupport::Concern
5
+
6
+ include CarrierWave::Uploader::Callbacks
7
+ include CarrierWave::Uploader::Configuration
8
+ include CarrierWave::Uploader::Cache
9
+
10
+ ##
11
+ # Returns what a client needs to put a file into the cache itself, without the
12
+ # bytes passing through the application. Assign the returned #cache_name to
13
+ # #{column}_cache once the client is done, as with any cached file.
14
+ #
15
+ # === Parameters
16
+ #
17
+ # [filename (String)] the name the file is to be cached under
18
+ # [expires_in (Integer)] seconds the upload stays possible, defaults to
19
+ # fog_authenticated_url_expiration
20
+ # [content_type (String)] the type the client has to send, if it is to be fixed
21
+ #
22
+ # === Returns
23
+ #
24
+ # [CarrierWave::DirectUpload] where and how to upload, and the name to hand back
25
+ #
26
+ def direct_upload(filename:, expires_in: nil, content_type: nil)
27
+ raise CarrierWave::InvalidParameter, "invalid filename" if filename.blank?
28
+
29
+ filename = CarrierWave::SanitizedFile.new(filename).filename
30
+
31
+ upload = nil
32
+ with_callbacks(:direct_upload, filename) do
33
+ self.cache_id = CarrierWave.generate_cache_id
34
+ self.original_filename = filename
35
+ headers = content_type ? { 'Content-Type' => content_type } : {}
36
+
37
+ upload = ::CarrierWave::DirectUpload.new(
38
+ url: cache_storage.direct_upload_url(
39
+ cache_path,
40
+ expires_in: expires_in || fog_authenticated_url_expiration,
41
+ headers: headers.dup # the storage may add its own
42
+ ),
43
+ headers: headers,
44
+ cache_name: cache_name
45
+ )
46
+ end
47
+ upload
48
+ end
49
+ end # DirectUpload
50
+ end # Uploader
51
+ end # CarrierWave
@@ -0,0 +1,100 @@
1
+ require "active_support/core_ext/hash/keys"
2
+
3
+ module CarrierWave
4
+ module Uploader
5
+ module Metadata
6
+ extend ActiveSupport::Concern
7
+
8
+ include CarrierWave::Uploader::Versions
9
+
10
+ ##
11
+ # Facts about the file which were determined when it was stored, as recorded in
12
+ # the column given to the +metadata+ option of the mount. They are the answer to
13
+ # questions the storage cannot answer, like whether a conditional version was
14
+ # created, and they save asking it the ones it can.
15
+ #
16
+ # Returns an empty Hash when nothing was recorded, in which case the facts are
17
+ # derived from the file and the uploader definition as they have always been.
18
+ #
19
+ # === Returns
20
+ #
21
+ # [Hash] the recorded facts
22
+ #
23
+ def metadata
24
+ return @metadata || {} unless parent_version
25
+
26
+ parent_version.metadata.dig('versions', self.class.version_names.last.to_s) || {}
27
+ end
28
+
29
+ def metadata=(metadata)
30
+ @metadata = metadata&.deep_stringify_keys
31
+ end
32
+
33
+ ##
34
+ # Collects the facts to record about the file being stored. Override to record
35
+ # your own, merging them into the result of +super+.
36
+ #
37
+ # === Returns
38
+ #
39
+ # [Hash] the facts to record
40
+ #
41
+ def build_metadata
42
+ {
43
+ 'filename' => full_filename(identifier),
44
+ 'size' => size,
45
+ 'content_type' => content_type,
46
+ 'versions' => active_versions.map { |name, version| [name.to_s, version.build_metadata] }.to_h
47
+ }
48
+ end
49
+
50
+ ##
51
+ # === Returns
52
+ #
53
+ # [Boolean] Whether what is stored gets recorded, which the mount decides
54
+ #
55
+ def metadata_recorded?
56
+ model.class.respond_to?(:uploader_option) &&
57
+ model.class.uploaders.has_key?(mounted_as) &&
58
+ !!model.class.uploader_option(mounted_as, :metadata_column)
59
+ end
60
+
61
+ def size
62
+ metadata['size'] || super
63
+ end
64
+
65
+ def content_type
66
+ metadata['content_type'] || super
67
+ end
68
+
69
+ ##
70
+ # The conditions decide which versions to create, so which ones were created is
71
+ # settled once the file is stored. Consult the record instead of asking them
72
+ # again, which is not only costly but can give a different answer.
73
+ #
74
+ def version_active?(name)
75
+ recorded_versions = metadata['versions']
76
+ return super unless recorded_versions
77
+
78
+ versions.has_key?(name.to_sym) && recorded_versions.has_key?(name.to_s)
79
+ end
80
+
81
+ def cache!(*)
82
+ @metadata = nil
83
+ super
84
+ end
85
+
86
+ def recreate_versions!(*)
87
+ @metadata = nil
88
+ super
89
+ end
90
+
91
+ private
92
+
93
+ def full_filename(for_file)
94
+ return super unless for_file == identifier
95
+
96
+ metadata['filename'] || super
97
+ end
98
+ end # Metadata
99
+ end # Uploader
100
+ end # CarrierWave
@@ -10,6 +10,7 @@ module CarrierWave
10
10
  self.processors = []
11
11
 
12
12
  before :cache, :process!
13
+ before :direct_upload, :refuse_direct_upload_of_processed!
13
14
  end
14
15
 
15
16
  module ClassMethods
@@ -67,13 +68,17 @@ module CarrierWave
67
68
  new_processors.each do |processor, processor_args|
68
69
  self.processors += [[processor, processor_args, condition, condition_type]]
69
70
 
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
71
+ next unless processor == :convert
72
+
73
+ # Treat :convert specially, since it should trigger the file extension change.
74
+ # That has to be worked out again when the file is retrieved, where whether
75
+ # the condition held cannot be known, so a conditional one is refused.
76
+ if condition
77
+ raise ArgumentError, "`process convert:` cannot be given `:if` or `:unless`. The file extension follows the conversion, and it has to be worked out again when the file is retrieved, where whether the condition held is not known. " \
78
+ "Convert unconditionally, or put the conversion in a version of its own, or convert in a processor of your own and have #filename answer the extension the file ended up with."
76
79
  end
80
+
81
+ force_extension processor_args
77
82
  end
78
83
  end
79
84
  end # ClassMethods
@@ -116,6 +121,13 @@ module CarrierWave
116
121
 
117
122
  private
118
123
 
124
+ # Processing needs the file, which a direct upload never hands to the application
125
+ def refuse_direct_upload_of_processed!(filename)
126
+ return if !enable_processing || self.class.processors.empty?
127
+
128
+ raise "#{filename.inspect} can't be uploaded directly, as the processing of #{self.class} would be skipped"
129
+ end
130
+
119
131
  def forcing_extension(filename)
120
132
  if force_extension && filename
121
133
  Pathname.new(filename).sub_ext(".#{force_extension.to_s.delete_prefix('.')}").to_s
@@ -5,10 +5,26 @@ module CarrierWave
5
5
  ##
6
6
  # === Returns
7
7
  #
8
- # [Boolean] Whether the uploaded file is blank
8
+ # [Boolean] Whether a file is assigned. Use #exists? to ask the storage whether
9
+ # the file is actually there.
9
10
  #
10
11
  def blank?
11
- file.blank?
12
+ return true unless file
13
+ # A cache name can point at a cache which is gone, so it is verified. It is
14
+ # local and thus cheap, unlike asking a remote store.
15
+ return file.empty? if cached?
16
+
17
+ false
18
+ end
19
+
20
+ ##
21
+ # === Returns
22
+ #
23
+ # [Boolean] Whether the file exists in the storage. Asking costs a request when
24
+ # the storage is a remote one.
25
+ #
26
+ def exists?
27
+ !!file&.exists?
12
28
  end
13
29
 
14
30
  ##
@@ -94,6 +94,7 @@ module CarrierWave
94
94
  cache!(new_file) if new_file && !cached?
95
95
  if !cache_only && @file && @cache_id
96
96
  with_callbacks(:store, new_file) do
97
+ ensure_the_filename_can_be_worked_out_again
97
98
  new_file = storage.store!(@file)
98
99
  if delete_tmp_file_after_storage
99
100
  @file.delete unless move_to_store
@@ -116,8 +117,31 @@ module CarrierWave
116
117
  #
117
118
  def retrieve_from_store!(identifier)
118
119
  with_callbacks(:retrieve_from_store, identifier) do
119
- @file = storage.retrieve!(identifier)
120
+ # Has to come first, as the storage derives the path from what was recorded
121
+ # for the identifier
120
122
  @identifier = identifier
123
+ @file = storage.retrieve!(identifier)
124
+ end
125
+ end
126
+
127
+ ##
128
+ # Takes the file already stored with the given identifier as its own, for when
129
+ # something other than CarrierWave put it there.
130
+ #
131
+ # === Parameters
132
+ #
133
+ # [identifier (String)] identifies the file in the store
134
+ #
135
+ # === Raises
136
+ #
137
+ # [CarrierWave::InvalidParameter] if the identifier points out of the store dir
138
+ #
139
+ def adopt!(identifier)
140
+ # The file gets removed when replaced, so it must not point out of the store dir
141
+ raise CarrierWave::InvalidParameter, "invalid identifier" if identifier.start_with?('/') || identifier.split('/').include?('..')
142
+
143
+ with_callbacks(:adopt, identifier) do
144
+ retrieve_from_store!(identifier)
121
145
  end
122
146
  end
123
147
 
@@ -143,6 +167,29 @@ module CarrierWave
143
167
 
144
168
  private
145
169
 
170
+ ##
171
+ # A later request gets the identifier and nothing else, so the name the file is
172
+ # stored under has to follow from it. Ask a fresh uploader what it would work out,
173
+ # and refuse before anything is written rather than leave the file somewhere it
174
+ # would never be found. Versions are left out: as long as the original is found,
175
+ # they can be made again. So is a mount which records what it stored, which is
176
+ # spared having to work the name out at all.
177
+ #
178
+ def ensure_the_filename_can_be_worked_out_again
179
+ if filename.blank?
180
+ raise CarrierWave::FilenameNotReproducible, "The file has no name to be stored under. " \
181
+ "#filename has to give one, as a cached file doesn't have to carry a name of its own."
182
+ end
183
+ return if parent_version || metadata_recorded?
184
+
185
+ for_file = deduplicated_filename
186
+ would_look_for = self.class.new(model, mounted_as).send(:full_filename, for_file)
187
+ return if would_look_for == full_filename(for_file)
188
+
189
+ raise CarrierWave::FilenameNotReproducible, "The file would be stored as #{full_filename(for_file).inspect}, but a later request given only the identifier #{for_file.inspect} would look for #{would_look_for.inspect}. " \
190
+ "#full_filename has to follow from the identifier, as that is all such a request gets."
191
+ end
192
+
146
193
  def full_filename(for_file)
147
194
  forcing_extension(for_file)
148
195
  end
@@ -15,6 +15,9 @@ module CarrierWave
15
15
  # [String] the location where this file is accessible via a url
16
16
  #
17
17
  def url(options = {})
18
+ # The URL is to be fetched by a browser, which the file has to survive this request for
19
+ materialize_cache! if cached?
20
+
18
21
  if file.respond_to?(:url)
19
22
  tmp_url = file.method(:url).arity.zero? ? file.url : file.url(options)
20
23
  return tmp_url if tmp_url.present?
@@ -9,6 +9,7 @@ module CarrierWave
9
9
  @options = {}
10
10
  @blocks = []
11
11
  @klass = nil
12
+ @mutex = Mutex.new
12
13
  end
13
14
 
14
15
  def configure(options, &block)
@@ -18,55 +19,68 @@ module CarrierWave
18
19
  end
19
20
 
20
21
  def build(superclass)
22
+ # Double-checked locking pattern for thread-safe lazy initialization.
23
+ # Without synchronization, concurrent threads can observe a partially
24
+ # initialized @klass where the class is created but version_options
25
+ # is not yet set, causing NoMethodError in version_active?.
21
26
  return @klass if @klass
22
- @klass = Class.new(superclass)
23
- superclass.const_set("VersionUploader#{@name.to_s.camelize}", @klass)
24
-
25
- @klass.version_names += [@name]
26
- @klass.versions = {}
27
- @klass.processors = []
28
- @klass.version_options = @options
29
- @klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
30
- # Define the enable_processing method for versions so they get the
31
- # value from the parent class unless explicitly overwritten
32
- def self.enable_processing(value=nil)
33
- self.enable_processing = value if value
34
- if defined?(@enable_processing) && !@enable_processing.nil?
35
- @enable_processing
36
- else
37
- superclass.enable_processing
27
+
28
+ @mutex.synchronize do
29
+ return @klass if @klass
30
+
31
+ klass = Class.new(superclass)
32
+ superclass.const_set("VersionUploader#{@name.to_s.camelize}", klass)
33
+
34
+ klass.version_names += [@name]
35
+ klass.versions = {}
36
+ klass.processors = []
37
+ klass.version_options = @options
38
+ klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
39
+ # Define the enable_processing method for versions so they get the
40
+ # value from the parent class unless explicitly overwritten
41
+ def self.enable_processing(value=nil)
42
+ self.enable_processing = value if value
43
+ if defined?(@enable_processing) && !@enable_processing.nil?
44
+ @enable_processing
45
+ else
46
+ superclass.enable_processing
47
+ end
38
48
  end
39
- end
40
49
 
41
- # Regardless of what is set in the parent uploader, do not enforce the
42
- # move_to_cache config option on versions because it moves the original
43
- # file to the version's target file.
44
- #
45
- # If you want to enforce this setting on versions, override this method
46
- # in each version:
47
- #
48
- # version :thumb do
49
- # def move_to_cache
50
- # true
51
- # end
52
- # end
53
- #
54
- def move_to_cache
55
- false
56
- end
50
+ # Regardless of what is set in the parent uploader, do not enforce the
51
+ # move_to_cache config option on versions because it moves the original
52
+ # file to the version's target file.
53
+ #
54
+ # If you want to enforce this setting on versions, override this method
55
+ # in each version:
56
+ #
57
+ # version :thumb do
58
+ # def move_to_cache
59
+ # true
60
+ # end
61
+ # end
62
+ #
63
+ def move_to_cache
64
+ false
65
+ end
57
66
 
58
- # Need to rely on the parent version's identifier, as versions don't have its own one.
59
- def identifier
60
- parent_version.identifier
61
- end
62
- RUBY
63
- @blocks.each { |block| @klass.class_eval(&block) }
64
- @klass
67
+ # Need to rely on the parent version's identifier, as versions don't have its own one.
68
+ def identifier
69
+ parent_version.identifier
70
+ end
71
+ RUBY
72
+ @blocks.each { |block| klass.class_eval(&block) }
73
+
74
+ # Only assign to @klass after full initialization to ensure
75
+ # other threads never see a partially initialized class.
76
+ @klass = klass
77
+ end
65
78
  end
66
79
 
67
80
  def deep_dup
68
81
  other = dup
69
82
  other.instance_variable_set(:@blocks, @blocks.dup)
83
+ other.instance_variable_set(:@mutex, Mutex.new)
70
84
  other
71
85
  end
72
86
 
@@ -97,15 +111,18 @@ module CarrierWave
97
111
  attr_accessor :parent_version
98
112
 
99
113
  after :cache, :cache_versions!
114
+ after :materialize_cache, :materialize_versions_cache!
100
115
  after :store, :store_versions!
101
116
  after :remove, :remove_versions!
102
117
  after :retrieve_from_cache, :retrieve_versions_from_cache!
103
118
  after :retrieve_from_store, :retrieve_versions_from_store!
119
+ after :adopt, :adopt_versions!
120
+ before :direct_upload, :refuse_direct_upload_of_versions!
104
121
 
105
122
  prepend Module.new {
106
123
  def initialize(*)
107
124
  super
108
- @versions = nil
125
+ @versions = @deferred_version_retrieval = nil
109
126
  end
110
127
  }
111
128
  end
@@ -170,11 +187,16 @@ module CarrierWave
170
187
  # [Hash{Symbol => CarrierWave::Uploader}] a list of uploader instances
171
188
  #
172
189
  def versions
173
- return @versions if @versions
174
- @versions = {}
175
- self.class.versions.each do |name, version|
176
- @versions[name] = version.build(self.class).new(model, mounted_as)
177
- @versions[name].parent_version = self
190
+ unless @versions
191
+ @versions = {}
192
+ self.class.versions.each do |name, version|
193
+ @versions[name] = version.build(self.class).new(model, mounted_as)
194
+ @versions[name].parent_version = self
195
+ end
196
+ end
197
+ if (retrieval = @deferred_version_retrieval)
198
+ @deferred_version_retrieval = nil
199
+ active_versions.each_value { |v| v.public_send(*retrieval) }
178
200
  end
179
201
  @versions
180
202
  end
@@ -231,7 +253,7 @@ module CarrierWave
231
253
  # that the cached files are generated by the single upload attempt.
232
254
  #
233
255
  def cache!(*args)
234
- self.cache_id = parent_version.cache_id if parent_version
256
+ self.cache_id = parent_version.cache_id if parent_version&.cached?
235
257
 
236
258
  super
237
259
  end
@@ -324,13 +346,18 @@ module CarrierWave
324
346
  end
325
347
 
326
348
  def full_original_filename
327
- [version_name, super].compact.join('_')
349
+ # nil stays nil, as a cached file doesn't have to carry a name of its own
350
+ [version_name, super].compact.join('_').presence
328
351
  end
329
352
 
330
353
  def cache_versions!(new_file)
331
354
  derived_versions.each_value { |v| v.cache!(new_file) }
332
355
  end
333
356
 
357
+ def materialize_versions_cache!
358
+ versions.each_value(&:materialize_cache!)
359
+ end
360
+
334
361
  def store_versions!(new_file)
335
362
  active_versions.each_value { |v| v.store!(new_file) }
336
363
  end
@@ -339,12 +366,28 @@ module CarrierWave
339
366
  versions.each_value { |v| v.remove! }
340
367
  end
341
368
 
369
+ # Retrieval is deferred until #versions is accessed, as evaluating the versions'
370
+ # conditions can be costly and is pointless when no version is used
342
371
  def retrieve_versions_from_cache!(cache_name)
343
- active_versions.each_value { |v| v.retrieve_from_cache!(cache_name) }
372
+ @deferred_version_retrieval = [:retrieve_from_cache!, cache_name]
344
373
  end
345
374
 
346
375
  def retrieve_versions_from_store!(identifier)
347
- active_versions.each_value { |v| v.retrieve_from_store!(identifier) }
376
+ @deferred_version_retrieval = [:retrieve_from_store!, identifier]
377
+ end
378
+
379
+ # Versions are created from a file the application has, which a direct upload never gives it
380
+ def refuse_direct_upload_of_versions!(filename)
381
+ return if versions.empty?
382
+
383
+ raise "#{filename.inspect} can't be uploaded directly, as nothing would create the versions #{versions.keys.join(', ')}"
384
+ end
385
+
386
+ # Versions are created only through the cache, which an adopted file never goes through
387
+ def adopt_versions!(identifier)
388
+ return if active_versions.empty?
389
+
390
+ raise "#{identifier.inspect} can't be adopted, as nothing would create the versions #{active_versions.keys.join(', ')}"
348
391
  end
349
392
 
350
393
  end # Versions
@@ -5,6 +5,7 @@ require "carrierwave/uploader/url"
5
5
  require "carrierwave/uploader/mountable"
6
6
  require "carrierwave/uploader/cache"
7
7
  require "carrierwave/uploader/store"
8
+ require "carrierwave/uploader/direct_upload"
8
9
  require "carrierwave/uploader/download"
9
10
  require "carrierwave/uploader/remove"
10
11
  require "carrierwave/uploader/extension_allowlist"
@@ -15,6 +16,7 @@ require "carrierwave/uploader/file_size"
15
16
  require "carrierwave/uploader/dimension"
16
17
  require "carrierwave/uploader/processing"
17
18
  require "carrierwave/uploader/versions"
19
+ require "carrierwave/uploader/metadata"
18
20
  require "carrierwave/uploader/default_url"
19
21
 
20
22
  require "carrierwave/uploader/serialization"
@@ -51,6 +53,7 @@ module CarrierWave
51
53
  include CarrierWave::Uploader::Mountable
52
54
  include CarrierWave::Uploader::Cache
53
55
  include CarrierWave::Uploader::Store
56
+ include CarrierWave::Uploader::DirectUpload
54
57
  include CarrierWave::Uploader::Download
55
58
  include CarrierWave::Uploader::Remove
56
59
  include CarrierWave::Uploader::ExtensionAllowlist
@@ -61,6 +64,7 @@ module CarrierWave
61
64
  include CarrierWave::Uploader::Dimension
62
65
  include CarrierWave::Uploader::Processing
63
66
  include CarrierWave::Uploader::Versions
67
+ include CarrierWave::Uploader::Metadata
64
68
  include CarrierWave::Uploader::DefaultUrl
65
69
  include CarrierWave::Uploader::Serialization
66
70
  end # Base
@@ -3,23 +3,48 @@ require 'uri'
3
3
  module CarrierWave
4
4
  module Utilities
5
5
  module Uri
6
+ # For encoding raw data into a URI component, where '%' is data and becomes '%25'.
6
7
  # based on Ruby < 2.0's URI.encode
7
8
  PATH_SAFE = URI::RFC2396_REGEXP::PATTERN::UNRESERVED + '\/'
8
9
  PATH_UNSAFE = Regexp.new("[^#{PATH_SAFE}]", false)
9
- NON_ASCII = /[^[:ascii:]]/.freeze
10
10
 
11
- private
11
+ # For repairing a string which is already a URI, following RFC 3986 per component.
12
+ UNRESERVED = 'A-Za-z0-9\-._~'.freeze
13
+ SUB_DELIMS = "!$&'()*+,;=".freeze
14
+ PCHAR = "#{UNRESERVED}#{SUB_DELIMS}:@".freeze
15
+ SANITIZE_PATH = Regexp.new("[#{PCHAR}/]").freeze
16
+ # Also allows [ ] { } | \ ^, which browsers and URI.parse accept in a query
17
+ SANITIZE_QUERY = Regexp.new("[#{PCHAR}/?\\[\\]{}|\\\\^]").freeze
18
+ SANITIZE_FRAGMENT = Regexp.new("[#{PCHAR}/?]").freeze
12
19
 
20
+ PERCENT_ENCODED = /%[0-9a-fA-F]{2}/.freeze
21
+
22
+ # URI::DEFAULT_PARSER#escape warns as obsolete, and URI::RFC2396_PARSER is missing in older uri gems
23
+ RFC2396_PARSER = URI::RFC2396_Parser.new
24
+
25
+ module_function
26
+
27
+ # Not idempotent, as '%' is escaped to '%25' every time
13
28
  def encode_path(path)
14
- URI::DEFAULT_PARSER.escape(path, PATH_UNSAFE)
29
+ RFC2396_PARSER.escape(path, PATH_UNSAFE)
30
+ end
31
+
32
+ # Only for strings not sent over the wire, like a filename to be shown to the user.
33
+ # CGI.unescape is for form encoding and would turn '+' into a space.
34
+ def decode_path(str)
35
+ RFC2396_PARSER.unescape(str)
15
36
  end
16
37
 
17
- def encode_non_ascii(str)
18
- URI::DEFAULT_PARSER.escape(str, NON_ASCII)
38
+ # Escapes only what cannot appear in the component, leaving existing %XX alone.
39
+ # Decoding first would make %2F and '/', %2B and '+' indistinguishable.
40
+ def sanitize_component(str, allowed)
41
+ str.gsub(/(#{PERCENT_ENCODED})|(.)/m) do
42
+ $1 || ($2.match?(allowed) ? $2 : escape_octets($2))
43
+ end
19
44
  end
20
45
 
21
- def decode_uri(str)
22
- URI::DEFAULT_PARSER.unescape(str)
46
+ def escape_octets(str)
47
+ str.b.bytes.map { |byte| format('%%%02X', byte) }.join
23
48
  end
24
49
  end # Uri
25
50
  end # Utilities
@@ -1,3 +1,3 @@
1
1
  module CarrierWave
2
- VERSION = "3.1.4".freeze
2
+ VERSION = "4.0.0.rc".freeze
3
3
  end
data/lib/carrierwave.rb CHANGED
@@ -98,6 +98,7 @@ end
98
98
  require "carrierwave/utilities"
99
99
  require "carrierwave/error"
100
100
  require "carrierwave/sanitized_file"
101
+ require "carrierwave/direct_upload"
101
102
  require "carrierwave/mounter"
102
103
  require "carrierwave/mount"
103
104
  require "carrierwave/processing"