##// END OF EJS Templates
Restrict the length attachment filenames on disk (#24186)....
Jean-Philippe Lang -
r15701:20be00e437a5
parent child
Show More
@@ -1,428 +1,428
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require "digest/md5"
18 require "digest/md5"
19 require "fileutils"
19 require "fileutils"
20
20
21 class Attachment < ActiveRecord::Base
21 class Attachment < ActiveRecord::Base
22 include Redmine::SafeAttributes
22 include Redmine::SafeAttributes
23 belongs_to :container, :polymorphic => true
23 belongs_to :container, :polymorphic => true
24 belongs_to :author, :class_name => "User"
24 belongs_to :author, :class_name => "User"
25
25
26 validates_presence_of :filename, :author
26 validates_presence_of :filename, :author
27 validates_length_of :filename, :maximum => 255
27 validates_length_of :filename, :maximum => 255
28 validates_length_of :disk_filename, :maximum => 255
28 validates_length_of :disk_filename, :maximum => 255
29 validates_length_of :description, :maximum => 255
29 validates_length_of :description, :maximum => 255
30 validate :validate_max_file_size, :validate_file_extension
30 validate :validate_max_file_size, :validate_file_extension
31 attr_protected :id
31 attr_protected :id
32
32
33 acts_as_event :title => :filename,
33 acts_as_event :title => :filename,
34 :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}}
34 :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}}
35
35
36 acts_as_activity_provider :type => 'files',
36 acts_as_activity_provider :type => 'files',
37 :permission => :view_files,
37 :permission => :view_files,
38 :author_key => :author_id,
38 :author_key => :author_id,
39 :scope => select("#{Attachment.table_name}.*").
39 :scope => select("#{Attachment.table_name}.*").
40 joins("LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " +
40 joins("LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " +
41 "LEFT JOIN #{Project.table_name} ON #{Version.table_name}.project_id = #{Project.table_name}.id OR ( #{Attachment.table_name}.container_type='Project' AND #{Attachment.table_name}.container_id = #{Project.table_name}.id )")
41 "LEFT JOIN #{Project.table_name} ON #{Version.table_name}.project_id = #{Project.table_name}.id OR ( #{Attachment.table_name}.container_type='Project' AND #{Attachment.table_name}.container_id = #{Project.table_name}.id )")
42
42
43 acts_as_activity_provider :type => 'documents',
43 acts_as_activity_provider :type => 'documents',
44 :permission => :view_documents,
44 :permission => :view_documents,
45 :author_key => :author_id,
45 :author_key => :author_id,
46 :scope => select("#{Attachment.table_name}.*").
46 :scope => select("#{Attachment.table_name}.*").
47 joins("LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " +
47 joins("LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " +
48 "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id")
48 "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id")
49
49
50 cattr_accessor :storage_path
50 cattr_accessor :storage_path
51 @@storage_path = Redmine::Configuration['attachments_storage_path'] || File.join(Rails.root, "files")
51 @@storage_path = Redmine::Configuration['attachments_storage_path'] || File.join(Rails.root, "files")
52
52
53 cattr_accessor :thumbnails_storage_path
53 cattr_accessor :thumbnails_storage_path
54 @@thumbnails_storage_path = File.join(Rails.root, "tmp", "thumbnails")
54 @@thumbnails_storage_path = File.join(Rails.root, "tmp", "thumbnails")
55
55
56 before_create :files_to_final_location
56 before_create :files_to_final_location
57 after_rollback :delete_from_disk, :on => :create
57 after_rollback :delete_from_disk, :on => :create
58 after_commit :delete_from_disk, :on => :destroy
58 after_commit :delete_from_disk, :on => :destroy
59
59
60 safe_attributes 'filename', 'content_type', 'description'
60 safe_attributes 'filename', 'content_type', 'description'
61
61
62 # Returns an unsaved copy of the attachment
62 # Returns an unsaved copy of the attachment
63 def copy(attributes=nil)
63 def copy(attributes=nil)
64 copy = self.class.new
64 copy = self.class.new
65 copy.attributes = self.attributes.dup.except("id", "downloads")
65 copy.attributes = self.attributes.dup.except("id", "downloads")
66 copy.attributes = attributes if attributes
66 copy.attributes = attributes if attributes
67 copy
67 copy
68 end
68 end
69
69
70 def validate_max_file_size
70 def validate_max_file_size
71 if @temp_file && self.filesize > Setting.attachment_max_size.to_i.kilobytes
71 if @temp_file && self.filesize > Setting.attachment_max_size.to_i.kilobytes
72 errors.add(:base, l(:error_attachment_too_big, :max_size => Setting.attachment_max_size.to_i.kilobytes))
72 errors.add(:base, l(:error_attachment_too_big, :max_size => Setting.attachment_max_size.to_i.kilobytes))
73 end
73 end
74 end
74 end
75
75
76 def validate_file_extension
76 def validate_file_extension
77 if @temp_file
77 if @temp_file
78 extension = File.extname(filename)
78 extension = File.extname(filename)
79 unless self.class.valid_extension?(extension)
79 unless self.class.valid_extension?(extension)
80 errors.add(:base, l(:error_attachment_extension_not_allowed, :extension => extension))
80 errors.add(:base, l(:error_attachment_extension_not_allowed, :extension => extension))
81 end
81 end
82 end
82 end
83 end
83 end
84
84
85 def file=(incoming_file)
85 def file=(incoming_file)
86 unless incoming_file.nil?
86 unless incoming_file.nil?
87 @temp_file = incoming_file
87 @temp_file = incoming_file
88 if @temp_file.size > 0
88 if @temp_file.size > 0
89 if @temp_file.respond_to?(:original_filename)
89 if @temp_file.respond_to?(:original_filename)
90 self.filename = @temp_file.original_filename
90 self.filename = @temp_file.original_filename
91 self.filename.force_encoding("UTF-8")
91 self.filename.force_encoding("UTF-8")
92 end
92 end
93 if @temp_file.respond_to?(:content_type)
93 if @temp_file.respond_to?(:content_type)
94 self.content_type = @temp_file.content_type.to_s.chomp
94 self.content_type = @temp_file.content_type.to_s.chomp
95 end
95 end
96 self.filesize = @temp_file.size
96 self.filesize = @temp_file.size
97 end
97 end
98 end
98 end
99 end
99 end
100
100
101 def file
101 def file
102 nil
102 nil
103 end
103 end
104
104
105 def filename=(arg)
105 def filename=(arg)
106 write_attribute :filename, sanitize_filename(arg.to_s)
106 write_attribute :filename, sanitize_filename(arg.to_s)
107 filename
107 filename
108 end
108 end
109
109
110 # Copies the temporary file to its final location
110 # Copies the temporary file to its final location
111 # and computes its MD5 hash
111 # and computes its MD5 hash
112 def files_to_final_location
112 def files_to_final_location
113 if @temp_file && (@temp_file.size > 0)
113 if @temp_file && (@temp_file.size > 0)
114 self.disk_directory = target_directory
114 self.disk_directory = target_directory
115 self.disk_filename = Attachment.disk_filename(filename, disk_directory)
115 self.disk_filename = Attachment.disk_filename(filename, disk_directory)
116 logger.info("Saving attachment '#{self.diskfile}' (#{@temp_file.size} bytes)") if logger
116 logger.info("Saving attachment '#{self.diskfile}' (#{@temp_file.size} bytes)") if logger
117 path = File.dirname(diskfile)
117 path = File.dirname(diskfile)
118 unless File.directory?(path)
118 unless File.directory?(path)
119 FileUtils.mkdir_p(path)
119 FileUtils.mkdir_p(path)
120 end
120 end
121 md5 = Digest::MD5.new
121 md5 = Digest::MD5.new
122 File.open(diskfile, "wb") do |f|
122 File.open(diskfile, "wb") do |f|
123 if @temp_file.respond_to?(:read)
123 if @temp_file.respond_to?(:read)
124 buffer = ""
124 buffer = ""
125 while (buffer = @temp_file.read(8192))
125 while (buffer = @temp_file.read(8192))
126 f.write(buffer)
126 f.write(buffer)
127 md5.update(buffer)
127 md5.update(buffer)
128 end
128 end
129 else
129 else
130 f.write(@temp_file)
130 f.write(@temp_file)
131 md5.update(@temp_file)
131 md5.update(@temp_file)
132 end
132 end
133 end
133 end
134 self.digest = md5.hexdigest
134 self.digest = md5.hexdigest
135 end
135 end
136 @temp_file = nil
136 @temp_file = nil
137
137
138 if content_type.blank? && filename.present?
138 if content_type.blank? && filename.present?
139 self.content_type = Redmine::MimeType.of(filename)
139 self.content_type = Redmine::MimeType.of(filename)
140 end
140 end
141 # Don't save the content type if it's longer than the authorized length
141 # Don't save the content type if it's longer than the authorized length
142 if self.content_type && self.content_type.length > 255
142 if self.content_type && self.content_type.length > 255
143 self.content_type = nil
143 self.content_type = nil
144 end
144 end
145 end
145 end
146
146
147 # Deletes the file from the file system if it's not referenced by other attachments
147 # Deletes the file from the file system if it's not referenced by other attachments
148 def delete_from_disk
148 def delete_from_disk
149 if Attachment.where("disk_filename = ? AND id <> ?", disk_filename, id).empty?
149 if Attachment.where("disk_filename = ? AND id <> ?", disk_filename, id).empty?
150 delete_from_disk!
150 delete_from_disk!
151 end
151 end
152 end
152 end
153
153
154 # Returns file's location on disk
154 # Returns file's location on disk
155 def diskfile
155 def diskfile
156 File.join(self.class.storage_path, disk_directory.to_s, disk_filename.to_s)
156 File.join(self.class.storage_path, disk_directory.to_s, disk_filename.to_s)
157 end
157 end
158
158
159 def title
159 def title
160 title = filename.to_s
160 title = filename.to_s
161 if description.present?
161 if description.present?
162 title << " (#{description})"
162 title << " (#{description})"
163 end
163 end
164 title
164 title
165 end
165 end
166
166
167 def increment_download
167 def increment_download
168 increment!(:downloads)
168 increment!(:downloads)
169 end
169 end
170
170
171 def project
171 def project
172 container.try(:project)
172 container.try(:project)
173 end
173 end
174
174
175 def visible?(user=User.current)
175 def visible?(user=User.current)
176 if container_id
176 if container_id
177 container && container.attachments_visible?(user)
177 container && container.attachments_visible?(user)
178 else
178 else
179 author == user
179 author == user
180 end
180 end
181 end
181 end
182
182
183 def editable?(user=User.current)
183 def editable?(user=User.current)
184 if container_id
184 if container_id
185 container && container.attachments_editable?(user)
185 container && container.attachments_editable?(user)
186 else
186 else
187 author == user
187 author == user
188 end
188 end
189 end
189 end
190
190
191 def deletable?(user=User.current)
191 def deletable?(user=User.current)
192 if container_id
192 if container_id
193 container && container.attachments_deletable?(user)
193 container && container.attachments_deletable?(user)
194 else
194 else
195 author == user
195 author == user
196 end
196 end
197 end
197 end
198
198
199 def image?
199 def image?
200 !!(self.filename =~ /\.(bmp|gif|jpg|jpe|jpeg|png)$/i)
200 !!(self.filename =~ /\.(bmp|gif|jpg|jpe|jpeg|png)$/i)
201 end
201 end
202
202
203 def thumbnailable?
203 def thumbnailable?
204 image?
204 image?
205 end
205 end
206
206
207 # Returns the full path the attachment thumbnail, or nil
207 # Returns the full path the attachment thumbnail, or nil
208 # if the thumbnail cannot be generated.
208 # if the thumbnail cannot be generated.
209 def thumbnail(options={})
209 def thumbnail(options={})
210 if thumbnailable? && readable?
210 if thumbnailable? && readable?
211 size = options[:size].to_i
211 size = options[:size].to_i
212 if size > 0
212 if size > 0
213 # Limit the number of thumbnails per image
213 # Limit the number of thumbnails per image
214 size = (size / 50) * 50
214 size = (size / 50) * 50
215 # Maximum thumbnail size
215 # Maximum thumbnail size
216 size = 800 if size > 800
216 size = 800 if size > 800
217 else
217 else
218 size = Setting.thumbnails_size.to_i
218 size = Setting.thumbnails_size.to_i
219 end
219 end
220 size = 100 unless size > 0
220 size = 100 unless size > 0
221 target = File.join(self.class.thumbnails_storage_path, "#{id}_#{digest}_#{size}.thumb")
221 target = File.join(self.class.thumbnails_storage_path, "#{id}_#{digest}_#{size}.thumb")
222
222
223 begin
223 begin
224 Redmine::Thumbnail.generate(self.diskfile, target, size)
224 Redmine::Thumbnail.generate(self.diskfile, target, size)
225 rescue => e
225 rescue => e
226 logger.error "An error occured while generating thumbnail for #{disk_filename} to #{target}\nException was: #{e.message}" if logger
226 logger.error "An error occured while generating thumbnail for #{disk_filename} to #{target}\nException was: #{e.message}" if logger
227 return nil
227 return nil
228 end
228 end
229 end
229 end
230 end
230 end
231
231
232 # Deletes all thumbnails
232 # Deletes all thumbnails
233 def self.clear_thumbnails
233 def self.clear_thumbnails
234 Dir.glob(File.join(thumbnails_storage_path, "*.thumb")).each do |file|
234 Dir.glob(File.join(thumbnails_storage_path, "*.thumb")).each do |file|
235 File.delete file
235 File.delete file
236 end
236 end
237 end
237 end
238
238
239 def is_text?
239 def is_text?
240 Redmine::MimeType.is_type?('text', filename)
240 Redmine::MimeType.is_type?('text', filename)
241 end
241 end
242
242
243 def is_image?
243 def is_image?
244 Redmine::MimeType.is_type?('image', filename)
244 Redmine::MimeType.is_type?('image', filename)
245 end
245 end
246
246
247 def is_diff?
247 def is_diff?
248 self.filename =~ /\.(patch|diff)$/i
248 self.filename =~ /\.(patch|diff)$/i
249 end
249 end
250
250
251 def is_pdf?
251 def is_pdf?
252 Redmine::MimeType.of(filename) == "application/pdf"
252 Redmine::MimeType.of(filename) == "application/pdf"
253 end
253 end
254
254
255 # Returns true if the file is readable
255 # Returns true if the file is readable
256 def readable?
256 def readable?
257 File.readable?(diskfile)
257 File.readable?(diskfile)
258 end
258 end
259
259
260 # Returns the attachment token
260 # Returns the attachment token
261 def token
261 def token
262 "#{id}.#{digest}"
262 "#{id}.#{digest}"
263 end
263 end
264
264
265 # Finds an attachment that matches the given token and that has no container
265 # Finds an attachment that matches the given token and that has no container
266 def self.find_by_token(token)
266 def self.find_by_token(token)
267 if token.to_s =~ /^(\d+)\.([0-9a-f]+)$/
267 if token.to_s =~ /^(\d+)\.([0-9a-f]+)$/
268 attachment_id, attachment_digest = $1, $2
268 attachment_id, attachment_digest = $1, $2
269 attachment = Attachment.where(:id => attachment_id, :digest => attachment_digest).first
269 attachment = Attachment.where(:id => attachment_id, :digest => attachment_digest).first
270 if attachment && attachment.container.nil?
270 if attachment && attachment.container.nil?
271 attachment
271 attachment
272 end
272 end
273 end
273 end
274 end
274 end
275
275
276 # Bulk attaches a set of files to an object
276 # Bulk attaches a set of files to an object
277 #
277 #
278 # Returns a Hash of the results:
278 # Returns a Hash of the results:
279 # :files => array of the attached files
279 # :files => array of the attached files
280 # :unsaved => array of the files that could not be attached
280 # :unsaved => array of the files that could not be attached
281 def self.attach_files(obj, attachments)
281 def self.attach_files(obj, attachments)
282 result = obj.save_attachments(attachments, User.current)
282 result = obj.save_attachments(attachments, User.current)
283 obj.attach_saved_attachments
283 obj.attach_saved_attachments
284 result
284 result
285 end
285 end
286
286
287 # Updates the filename and description of a set of attachments
287 # Updates the filename and description of a set of attachments
288 # with the given hash of attributes. Returns true if all
288 # with the given hash of attributes. Returns true if all
289 # attachments were updated.
289 # attachments were updated.
290 #
290 #
291 # Example:
291 # Example:
292 # Attachment.update_attachments(attachments, {
292 # Attachment.update_attachments(attachments, {
293 # 4 => {:filename => 'foo'},
293 # 4 => {:filename => 'foo'},
294 # 7 => {:filename => 'bar', :description => 'file description'}
294 # 7 => {:filename => 'bar', :description => 'file description'}
295 # })
295 # })
296 #
296 #
297 def self.update_attachments(attachments, params)
297 def self.update_attachments(attachments, params)
298 params = params.transform_keys {|key| key.to_i}
298 params = params.transform_keys {|key| key.to_i}
299
299
300 saved = true
300 saved = true
301 transaction do
301 transaction do
302 attachments.each do |attachment|
302 attachments.each do |attachment|
303 if p = params[attachment.id]
303 if p = params[attachment.id]
304 attachment.filename = p[:filename] if p.key?(:filename)
304 attachment.filename = p[:filename] if p.key?(:filename)
305 attachment.description = p[:description] if p.key?(:description)
305 attachment.description = p[:description] if p.key?(:description)
306 saved &&= attachment.save
306 saved &&= attachment.save
307 end
307 end
308 end
308 end
309 unless saved
309 unless saved
310 raise ActiveRecord::Rollback
310 raise ActiveRecord::Rollback
311 end
311 end
312 end
312 end
313 saved
313 saved
314 end
314 end
315
315
316 def self.latest_attach(attachments, filename)
316 def self.latest_attach(attachments, filename)
317 attachments.sort_by(&:created_on).reverse.detect do |att|
317 attachments.sort_by(&:created_on).reverse.detect do |att|
318 filename.casecmp(att.filename) == 0
318 filename.casecmp(att.filename) == 0
319 end
319 end
320 end
320 end
321
321
322 def self.prune(age=1.day)
322 def self.prune(age=1.day)
323 Attachment.where("created_on < ? AND (container_type IS NULL OR container_type = '')", Time.now - age).destroy_all
323 Attachment.where("created_on < ? AND (container_type IS NULL OR container_type = '')", Time.now - age).destroy_all
324 end
324 end
325
325
326 # Moves an existing attachment to its target directory
326 # Moves an existing attachment to its target directory
327 def move_to_target_directory!
327 def move_to_target_directory!
328 return unless !new_record? & readable?
328 return unless !new_record? & readable?
329
329
330 src = diskfile
330 src = diskfile
331 self.disk_directory = target_directory
331 self.disk_directory = target_directory
332 dest = diskfile
332 dest = diskfile
333
333
334 return if src == dest
334 return if src == dest
335
335
336 if !FileUtils.mkdir_p(File.dirname(dest))
336 if !FileUtils.mkdir_p(File.dirname(dest))
337 logger.error "Could not create directory #{File.dirname(dest)}" if logger
337 logger.error "Could not create directory #{File.dirname(dest)}" if logger
338 return
338 return
339 end
339 end
340
340
341 if !FileUtils.mv(src, dest)
341 if !FileUtils.mv(src, dest)
342 logger.error "Could not move attachment from #{src} to #{dest}" if logger
342 logger.error "Could not move attachment from #{src} to #{dest}" if logger
343 return
343 return
344 end
344 end
345
345
346 update_column :disk_directory, disk_directory
346 update_column :disk_directory, disk_directory
347 end
347 end
348
348
349 # Moves existing attachments that are stored at the root of the files
349 # Moves existing attachments that are stored at the root of the files
350 # directory (ie. created before Redmine 2.3) to their target subdirectories
350 # directory (ie. created before Redmine 2.3) to their target subdirectories
351 def self.move_from_root_to_target_directory
351 def self.move_from_root_to_target_directory
352 Attachment.where("disk_directory IS NULL OR disk_directory = ''").find_each do |attachment|
352 Attachment.where("disk_directory IS NULL OR disk_directory = ''").find_each do |attachment|
353 attachment.move_to_target_directory!
353 attachment.move_to_target_directory!
354 end
354 end
355 end
355 end
356
356
357 # Returns true if the extension is allowed regarding allowed/denied
357 # Returns true if the extension is allowed regarding allowed/denied
358 # extensions defined in application settings, otherwise false
358 # extensions defined in application settings, otherwise false
359 def self.valid_extension?(extension)
359 def self.valid_extension?(extension)
360 denied, allowed = [:attachment_extensions_denied, :attachment_extensions_allowed].map do |setting|
360 denied, allowed = [:attachment_extensions_denied, :attachment_extensions_allowed].map do |setting|
361 Setting.send(setting)
361 Setting.send(setting)
362 end
362 end
363 if denied.present? && extension_in?(extension, denied)
363 if denied.present? && extension_in?(extension, denied)
364 return false
364 return false
365 end
365 end
366 if allowed.present? && !extension_in?(extension, allowed)
366 if allowed.present? && !extension_in?(extension, allowed)
367 return false
367 return false
368 end
368 end
369 true
369 true
370 end
370 end
371
371
372 # Returns true if extension belongs to extensions list.
372 # Returns true if extension belongs to extensions list.
373 def self.extension_in?(extension, extensions)
373 def self.extension_in?(extension, extensions)
374 extension = extension.downcase.sub(/\A\.+/, '')
374 extension = extension.downcase.sub(/\A\.+/, '')
375
375
376 unless extensions.is_a?(Array)
376 unless extensions.is_a?(Array)
377 extensions = extensions.to_s.split(",").map(&:strip)
377 extensions = extensions.to_s.split(",").map(&:strip)
378 end
378 end
379 extensions = extensions.map {|s| s.downcase.sub(/\A\.+/, '')}.reject(&:blank?)
379 extensions = extensions.map {|s| s.downcase.sub(/\A\.+/, '')}.reject(&:blank?)
380 extensions.include?(extension)
380 extensions.include?(extension)
381 end
381 end
382
382
383 # Returns true if attachment's extension belongs to extensions list.
383 # Returns true if attachment's extension belongs to extensions list.
384 def extension_in?(extensions)
384 def extension_in?(extensions)
385 self.class.extension_in?(File.extname(filename), extensions)
385 self.class.extension_in?(File.extname(filename), extensions)
386 end
386 end
387
387
388 private
388 private
389
389
390 # Physically deletes the file from the file system
390 # Physically deletes the file from the file system
391 def delete_from_disk!
391 def delete_from_disk!
392 if disk_filename.present? && File.exist?(diskfile)
392 if disk_filename.present? && File.exist?(diskfile)
393 File.delete(diskfile)
393 File.delete(diskfile)
394 end
394 end
395 end
395 end
396
396
397 def sanitize_filename(value)
397 def sanitize_filename(value)
398 # get only the filename, not the whole path
398 # get only the filename, not the whole path
399 just_filename = value.gsub(/\A.*(\\|\/)/m, '')
399 just_filename = value.gsub(/\A.*(\\|\/)/m, '')
400
400
401 # Finally, replace invalid characters with underscore
401 # Finally, replace invalid characters with underscore
402 just_filename.gsub(/[\/\?\%\*\:\|\"\'<>\n\r]+/, '_')
402 just_filename.gsub(/[\/\?\%\*\:\|\"\'<>\n\r]+/, '_')
403 end
403 end
404
404
405 # Returns the subdirectory in which the attachment will be saved
405 # Returns the subdirectory in which the attachment will be saved
406 def target_directory
406 def target_directory
407 time = created_on || DateTime.now
407 time = created_on || DateTime.now
408 time.strftime("%Y/%m")
408 time.strftime("%Y/%m")
409 end
409 end
410
410
411 # Returns an ASCII or hashed filename that do not
411 # Returns an ASCII or hashed filename that do not
412 # exists yet in the given subdirectory
412 # exists yet in the given subdirectory
413 def self.disk_filename(filename, directory=nil)
413 def self.disk_filename(filename, directory=nil)
414 timestamp = DateTime.now.strftime("%y%m%d%H%M%S")
414 timestamp = DateTime.now.strftime("%y%m%d%H%M%S")
415 ascii = ''
415 ascii = ''
416 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
416 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$} && filename.length <= 50
417 ascii = filename
417 ascii = filename
418 else
418 else
419 ascii = Digest::MD5.hexdigest(filename)
419 ascii = Digest::MD5.hexdigest(filename)
420 # keep the extension if any
420 # keep the extension if any
421 ascii << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
421 ascii << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
422 end
422 end
423 while File.exist?(File.join(storage_path, directory.to_s, "#{timestamp}_#{ascii}"))
423 while File.exist?(File.join(storage_path, directory.to_s, "#{timestamp}_#{ascii}"))
424 timestamp.succ!
424 timestamp.succ!
425 end
425 end
426 "#{timestamp}_#{ascii}"
426 "#{timestamp}_#{ascii}"
427 end
427 end
428 end
428 end
@@ -1,421 +1,434
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 require File.expand_path('../../test_helper', __FILE__)
20 require File.expand_path('../../test_helper', __FILE__)
21
21
22 class AttachmentTest < ActiveSupport::TestCase
22 class AttachmentTest < ActiveSupport::TestCase
23 fixtures :users, :email_addresses, :projects, :roles, :members, :member_roles,
23 fixtures :users, :email_addresses, :projects, :roles, :members, :member_roles,
24 :enabled_modules, :issues, :trackers, :attachments
24 :enabled_modules, :issues, :trackers, :attachments
25
25
26 # TODO: remove this with Rails 5 that supports after_commit callbacks
26 # TODO: remove this with Rails 5 that supports after_commit callbacks
27 # in transactional fixtures (https://github.com/rails/rails/pull/18458)
27 # in transactional fixtures (https://github.com/rails/rails/pull/18458)
28 self.use_transactional_fixtures = false
28 self.use_transactional_fixtures = false
29
29
30 class MockFile
30 class MockFile
31 attr_reader :original_filename, :content_type, :content, :size
31 attr_reader :original_filename, :content_type, :content, :size
32
32
33 def initialize(attributes)
33 def initialize(attributes)
34 @original_filename = attributes[:original_filename]
34 @original_filename = attributes[:original_filename]
35 @content_type = attributes[:content_type]
35 @content_type = attributes[:content_type]
36 @content = attributes[:content] || "Content"
36 @content = attributes[:content] || "Content"
37 @size = content.size
37 @size = content.size
38 end
38 end
39 end
39 end
40
40
41 def setup
41 def setup
42 set_tmp_attachments_directory
42 set_tmp_attachments_directory
43 end
43 end
44
44
45 def test_container_for_new_attachment_should_be_nil
45 def test_container_for_new_attachment_should_be_nil
46 assert_nil Attachment.new.container
46 assert_nil Attachment.new.container
47 end
47 end
48
48
49 def test_filename_should_remove_eols
49 def test_filename_should_remove_eols
50 assert_equal "line_feed", Attachment.new(:filename => "line\nfeed").filename
50 assert_equal "line_feed", Attachment.new(:filename => "line\nfeed").filename
51 assert_equal "line_feed", Attachment.new(:filename => "some\npath/line\nfeed").filename
51 assert_equal "line_feed", Attachment.new(:filename => "some\npath/line\nfeed").filename
52 assert_equal "carriage_return", Attachment.new(:filename => "carriage\rreturn").filename
52 assert_equal "carriage_return", Attachment.new(:filename => "carriage\rreturn").filename
53 assert_equal "carriage_return", Attachment.new(:filename => "some\rpath/carriage\rreturn").filename
53 assert_equal "carriage_return", Attachment.new(:filename => "some\rpath/carriage\rreturn").filename
54 end
54 end
55
55
56 def test_create
56 def test_create
57 a = Attachment.new(:container => Issue.find(1),
57 a = Attachment.new(:container => Issue.find(1),
58 :file => uploaded_test_file("testfile.txt", "text/plain"),
58 :file => uploaded_test_file("testfile.txt", "text/plain"),
59 :author => User.find(1))
59 :author => User.find(1))
60 assert a.save
60 assert a.save
61 assert_equal 'testfile.txt', a.filename
61 assert_equal 'testfile.txt', a.filename
62 assert_equal 59, a.filesize
62 assert_equal 59, a.filesize
63 assert_equal 'text/plain', a.content_type
63 assert_equal 'text/plain', a.content_type
64 assert_equal 0, a.downloads
64 assert_equal 0, a.downloads
65 assert_equal '1478adae0d4eb06d35897518540e25d6', a.digest
65 assert_equal '1478adae0d4eb06d35897518540e25d6', a.digest
66
66
67 assert a.disk_directory
67 assert a.disk_directory
68 assert_match %r{\A\d{4}/\d{2}\z}, a.disk_directory
68 assert_match %r{\A\d{4}/\d{2}\z}, a.disk_directory
69
69
70 assert File.exist?(a.diskfile)
70 assert File.exist?(a.diskfile)
71 assert_equal 59, File.size(a.diskfile)
71 assert_equal 59, File.size(a.diskfile)
72 end
72 end
73
73
74 def test_create_should_clear_content_type_if_too_long
74 def test_create_should_clear_content_type_if_too_long
75 a = Attachment.new(:container => Issue.find(1),
75 a = Attachment.new(:container => Issue.find(1),
76 :file => uploaded_test_file("testfile.txt", "text/plain"),
76 :file => uploaded_test_file("testfile.txt", "text/plain"),
77 :author => User.find(1),
77 :author => User.find(1),
78 :content_type => 'a'*300)
78 :content_type => 'a'*300)
79 assert a.save
79 assert a.save
80 a.reload
80 a.reload
81 assert_nil a.content_type
81 assert_nil a.content_type
82 end
82 end
83
83
84 def test_shorted_filename_if_too_long
85 file = uploaded_test_file("testfile.txt", "text/plain")
86 file.instance_variable_set('@original_filename', "#{'a'*251}.txt")
87 assert 255, file.original_filename.length
88
89 a = Attachment.new(:container => Issue.find(1),
90 :file => file,
91 :author => User.find(1))
92 assert a.save
93 a.reload
94 assert_equal 12 + 1 + 32 + 4, a.disk_filename.length
95 end
96
84 def test_copy_should_preserve_attributes
97 def test_copy_should_preserve_attributes
85 a = Attachment.find(1)
98 a = Attachment.find(1)
86 copy = a.copy
99 copy = a.copy
87
100
88 assert_save copy
101 assert_save copy
89 copy = Attachment.order('id DESC').first
102 copy = Attachment.order('id DESC').first
90 %w(filename filesize content_type author_id created_on description digest disk_filename disk_directory diskfile).each do |attribute|
103 %w(filename filesize content_type author_id created_on description digest disk_filename disk_directory diskfile).each do |attribute|
91 assert_equal a.send(attribute), copy.send(attribute), "#{attribute} was different"
104 assert_equal a.send(attribute), copy.send(attribute), "#{attribute} was different"
92 end
105 end
93 end
106 end
94
107
95 def test_size_should_be_validated_for_new_file
108 def test_size_should_be_validated_for_new_file
96 with_settings :attachment_max_size => 0 do
109 with_settings :attachment_max_size => 0 do
97 a = Attachment.new(:container => Issue.find(1),
110 a = Attachment.new(:container => Issue.find(1),
98 :file => uploaded_test_file("testfile.txt", "text/plain"),
111 :file => uploaded_test_file("testfile.txt", "text/plain"),
99 :author => User.find(1))
112 :author => User.find(1))
100 assert !a.save
113 assert !a.save
101 end
114 end
102 end
115 end
103
116
104 def test_size_should_not_be_validated_when_copying
117 def test_size_should_not_be_validated_when_copying
105 a = Attachment.create!(:container => Issue.find(1),
118 a = Attachment.create!(:container => Issue.find(1),
106 :file => uploaded_test_file("testfile.txt", "text/plain"),
119 :file => uploaded_test_file("testfile.txt", "text/plain"),
107 :author => User.find(1))
120 :author => User.find(1))
108 with_settings :attachment_max_size => 0 do
121 with_settings :attachment_max_size => 0 do
109 copy = a.copy
122 copy = a.copy
110 assert copy.save
123 assert copy.save
111 end
124 end
112 end
125 end
113
126
114 def test_filesize_greater_than_2gb_should_be_supported
127 def test_filesize_greater_than_2gb_should_be_supported
115 with_settings :attachment_max_size => (50.gigabyte / 1024) do
128 with_settings :attachment_max_size => (50.gigabyte / 1024) do
116 a = Attachment.create!(:container => Issue.find(1),
129 a = Attachment.create!(:container => Issue.find(1),
117 :file => uploaded_test_file("testfile.txt", "text/plain"),
130 :file => uploaded_test_file("testfile.txt", "text/plain"),
118 :author => User.find(1))
131 :author => User.find(1))
119 a.filesize = 20.gigabyte
132 a.filesize = 20.gigabyte
120 a.save!
133 a.save!
121 assert_equal 20.gigabyte, a.reload.filesize
134 assert_equal 20.gigabyte, a.reload.filesize
122 end
135 end
123 end
136 end
124
137
125 def test_extension_should_be_validated_against_allowed_extensions
138 def test_extension_should_be_validated_against_allowed_extensions
126 with_settings :attachment_extensions_allowed => "txt, png" do
139 with_settings :attachment_extensions_allowed => "txt, png" do
127 a = Attachment.new(:container => Issue.find(1),
140 a = Attachment.new(:container => Issue.find(1),
128 :file => mock_file_with_options(:original_filename => "test.png"),
141 :file => mock_file_with_options(:original_filename => "test.png"),
129 :author => User.find(1))
142 :author => User.find(1))
130 assert_save a
143 assert_save a
131
144
132 a = Attachment.new(:container => Issue.find(1),
145 a = Attachment.new(:container => Issue.find(1),
133 :file => mock_file_with_options(:original_filename => "test.jpeg"),
146 :file => mock_file_with_options(:original_filename => "test.jpeg"),
134 :author => User.find(1))
147 :author => User.find(1))
135 assert !a.save
148 assert !a.save
136 end
149 end
137 end
150 end
138
151
139 def test_extension_should_be_validated_against_denied_extensions
152 def test_extension_should_be_validated_against_denied_extensions
140 with_settings :attachment_extensions_denied => "txt, png" do
153 with_settings :attachment_extensions_denied => "txt, png" do
141 a = Attachment.new(:container => Issue.find(1),
154 a = Attachment.new(:container => Issue.find(1),
142 :file => mock_file_with_options(:original_filename => "test.jpeg"),
155 :file => mock_file_with_options(:original_filename => "test.jpeg"),
143 :author => User.find(1))
156 :author => User.find(1))
144 assert_save a
157 assert_save a
145
158
146 a = Attachment.new(:container => Issue.find(1),
159 a = Attachment.new(:container => Issue.find(1),
147 :file => mock_file_with_options(:original_filename => "test.png"),
160 :file => mock_file_with_options(:original_filename => "test.png"),
148 :author => User.find(1))
161 :author => User.find(1))
149 assert !a.save
162 assert !a.save
150 end
163 end
151 end
164 end
152
165
153 def test_valid_extension_should_be_case_insensitive
166 def test_valid_extension_should_be_case_insensitive
154 with_settings :attachment_extensions_allowed => "txt, Png" do
167 with_settings :attachment_extensions_allowed => "txt, Png" do
155 assert Attachment.valid_extension?(".pnG")
168 assert Attachment.valid_extension?(".pnG")
156 assert !Attachment.valid_extension?(".jpeg")
169 assert !Attachment.valid_extension?(".jpeg")
157 end
170 end
158 with_settings :attachment_extensions_denied => "txt, Png" do
171 with_settings :attachment_extensions_denied => "txt, Png" do
159 assert !Attachment.valid_extension?(".pnG")
172 assert !Attachment.valid_extension?(".pnG")
160 assert Attachment.valid_extension?(".jpeg")
173 assert Attachment.valid_extension?(".jpeg")
161 end
174 end
162 end
175 end
163
176
164 def test_description_length_should_be_validated
177 def test_description_length_should_be_validated
165 a = Attachment.new(:description => 'a' * 300)
178 a = Attachment.new(:description => 'a' * 300)
166 assert !a.save
179 assert !a.save
167 assert_not_equal [], a.errors[:description]
180 assert_not_equal [], a.errors[:description]
168 end
181 end
169
182
170 def test_destroy
183 def test_destroy
171 a = Attachment.new(:container => Issue.find(1),
184 a = Attachment.new(:container => Issue.find(1),
172 :file => uploaded_test_file("testfile.txt", "text/plain"),
185 :file => uploaded_test_file("testfile.txt", "text/plain"),
173 :author => User.find(1))
186 :author => User.find(1))
174 assert a.save
187 assert a.save
175 assert_equal 'testfile.txt', a.filename
188 assert_equal 'testfile.txt', a.filename
176 assert_equal 59, a.filesize
189 assert_equal 59, a.filesize
177 assert_equal 'text/plain', a.content_type
190 assert_equal 'text/plain', a.content_type
178 assert_equal 0, a.downloads
191 assert_equal 0, a.downloads
179 assert_equal '1478adae0d4eb06d35897518540e25d6', a.digest
192 assert_equal '1478adae0d4eb06d35897518540e25d6', a.digest
180 diskfile = a.diskfile
193 diskfile = a.diskfile
181 assert File.exist?(diskfile)
194 assert File.exist?(diskfile)
182 assert_equal 59, File.size(a.diskfile)
195 assert_equal 59, File.size(a.diskfile)
183 assert a.destroy
196 assert a.destroy
184 assert !File.exist?(diskfile)
197 assert !File.exist?(diskfile)
185 end
198 end
186
199
187 def test_destroy_should_not_delete_file_referenced_by_other_attachment
200 def test_destroy_should_not_delete_file_referenced_by_other_attachment
188 a = Attachment.create!(:container => Issue.find(1),
201 a = Attachment.create!(:container => Issue.find(1),
189 :file => uploaded_test_file("testfile.txt", "text/plain"),
202 :file => uploaded_test_file("testfile.txt", "text/plain"),
190 :author => User.find(1))
203 :author => User.find(1))
191 diskfile = a.diskfile
204 diskfile = a.diskfile
192
205
193 copy = a.copy
206 copy = a.copy
194 copy.save!
207 copy.save!
195
208
196 assert File.exists?(diskfile)
209 assert File.exists?(diskfile)
197 a.destroy
210 a.destroy
198 assert File.exists?(diskfile)
211 assert File.exists?(diskfile)
199 copy.destroy
212 copy.destroy
200 assert !File.exists?(diskfile)
213 assert !File.exists?(diskfile)
201 end
214 end
202
215
203 def test_create_should_auto_assign_content_type
216 def test_create_should_auto_assign_content_type
204 a = Attachment.new(:container => Issue.find(1),
217 a = Attachment.new(:container => Issue.find(1),
205 :file => uploaded_test_file("testfile.txt", ""),
218 :file => uploaded_test_file("testfile.txt", ""),
206 :author => User.find(1))
219 :author => User.find(1))
207 assert a.save
220 assert a.save
208 assert_equal 'text/plain', a.content_type
221 assert_equal 'text/plain', a.content_type
209 end
222 end
210
223
211 def test_identical_attachments_at_the_same_time_should_not_overwrite
224 def test_identical_attachments_at_the_same_time_should_not_overwrite
212 a1 = Attachment.create!(:container => Issue.find(1),
225 a1 = Attachment.create!(:container => Issue.find(1),
213 :file => uploaded_test_file("testfile.txt", ""),
226 :file => uploaded_test_file("testfile.txt", ""),
214 :author => User.find(1))
227 :author => User.find(1))
215 a2 = Attachment.create!(:container => Issue.find(1),
228 a2 = Attachment.create!(:container => Issue.find(1),
216 :file => uploaded_test_file("testfile.txt", ""),
229 :file => uploaded_test_file("testfile.txt", ""),
217 :author => User.find(1))
230 :author => User.find(1))
218 assert a1.disk_filename != a2.disk_filename
231 assert a1.disk_filename != a2.disk_filename
219 end
232 end
220
233
221 def test_filename_should_be_basenamed
234 def test_filename_should_be_basenamed
222 a = Attachment.new(:file => MockFile.new(:original_filename => "path/to/the/file"))
235 a = Attachment.new(:file => MockFile.new(:original_filename => "path/to/the/file"))
223 assert_equal 'file', a.filename
236 assert_equal 'file', a.filename
224 end
237 end
225
238
226 def test_filename_should_be_sanitized
239 def test_filename_should_be_sanitized
227 a = Attachment.new(:file => MockFile.new(:original_filename => "valid:[] invalid:?%*|\"'<>chars"))
240 a = Attachment.new(:file => MockFile.new(:original_filename => "valid:[] invalid:?%*|\"'<>chars"))
228 assert_equal 'valid_[] invalid_chars', a.filename
241 assert_equal 'valid_[] invalid_chars', a.filename
229 end
242 end
230
243
231 def test_diskfilename
244 def test_diskfilename
232 assert Attachment.disk_filename("test_file.txt") =~ /^\d{12}_test_file.txt$/
245 assert Attachment.disk_filename("test_file.txt") =~ /^\d{12}_test_file.txt$/
233 assert_equal 'test_file.txt', Attachment.disk_filename("test_file.txt")[13..-1]
246 assert_equal 'test_file.txt', Attachment.disk_filename("test_file.txt")[13..-1]
234 assert_equal '770c509475505f37c2b8fb6030434d6b.txt', Attachment.disk_filename("test_accentuΓ©.txt")[13..-1]
247 assert_equal '770c509475505f37c2b8fb6030434d6b.txt', Attachment.disk_filename("test_accentuΓ©.txt")[13..-1]
235 assert_equal 'f8139524ebb8f32e51976982cd20a85d', Attachment.disk_filename("test_accentuΓ©")[13..-1]
248 assert_equal 'f8139524ebb8f32e51976982cd20a85d', Attachment.disk_filename("test_accentuΓ©")[13..-1]
236 assert_equal 'cbb5b0f30978ba03731d61f9f6d10011', Attachment.disk_filename("test_accentuΓ©.Γ§a")[13..-1]
249 assert_equal 'cbb5b0f30978ba03731d61f9f6d10011', Attachment.disk_filename("test_accentuΓ©.Γ§a")[13..-1]
237 end
250 end
238
251
239 def test_title
252 def test_title
240 a = Attachment.new(:filename => "test.png")
253 a = Attachment.new(:filename => "test.png")
241 assert_equal "test.png", a.title
254 assert_equal "test.png", a.title
242
255
243 a = Attachment.new(:filename => "test.png", :description => "Cool image")
256 a = Attachment.new(:filename => "test.png", :description => "Cool image")
244 assert_equal "test.png (Cool image)", a.title
257 assert_equal "test.png (Cool image)", a.title
245 end
258 end
246
259
247 def test_new_attachment_should_be_editable_by_author
260 def test_new_attachment_should_be_editable_by_author
248 user = User.find(1)
261 user = User.find(1)
249 a = Attachment.new(:author => user)
262 a = Attachment.new(:author => user)
250 assert_equal true, a.editable?(user)
263 assert_equal true, a.editable?(user)
251 end
264 end
252
265
253 def test_prune_should_destroy_old_unattached_attachments
266 def test_prune_should_destroy_old_unattached_attachments
254 Attachment.create!(:file => uploaded_test_file("testfile.txt", ""), :author_id => 1, :created_on => 2.days.ago)
267 Attachment.create!(:file => uploaded_test_file("testfile.txt", ""), :author_id => 1, :created_on => 2.days.ago)
255 Attachment.create!(:file => uploaded_test_file("testfile.txt", ""), :author_id => 1, :created_on => 2.days.ago)
268 Attachment.create!(:file => uploaded_test_file("testfile.txt", ""), :author_id => 1, :created_on => 2.days.ago)
256 Attachment.create!(:file => uploaded_test_file("testfile.txt", ""), :author_id => 1)
269 Attachment.create!(:file => uploaded_test_file("testfile.txt", ""), :author_id => 1)
257
270
258 assert_difference 'Attachment.count', -2 do
271 assert_difference 'Attachment.count', -2 do
259 Attachment.prune
272 Attachment.prune
260 end
273 end
261 end
274 end
262
275
263 def test_move_from_root_to_target_directory_should_move_root_files
276 def test_move_from_root_to_target_directory_should_move_root_files
264 a = Attachment.find(20)
277 a = Attachment.find(20)
265 assert a.disk_directory.blank?
278 assert a.disk_directory.blank?
266 # Create a real file for this fixture
279 # Create a real file for this fixture
267 File.open(a.diskfile, "w") do |f|
280 File.open(a.diskfile, "w") do |f|
268 f.write "test file at the root of files directory"
281 f.write "test file at the root of files directory"
269 end
282 end
270 assert a.readable?
283 assert a.readable?
271 Attachment.move_from_root_to_target_directory
284 Attachment.move_from_root_to_target_directory
272
285
273 a.reload
286 a.reload
274 assert_equal '2012/05', a.disk_directory
287 assert_equal '2012/05', a.disk_directory
275 assert a.readable?
288 assert a.readable?
276 end
289 end
277
290
278 test "Attachmnet.attach_files should attach the file" do
291 test "Attachmnet.attach_files should attach the file" do
279 issue = Issue.first
292 issue = Issue.first
280 assert_difference 'Attachment.count' do
293 assert_difference 'Attachment.count' do
281 Attachment.attach_files(issue,
294 Attachment.attach_files(issue,
282 '1' => {
295 '1' => {
283 'file' => uploaded_test_file('testfile.txt', 'text/plain'),
296 'file' => uploaded_test_file('testfile.txt', 'text/plain'),
284 'description' => 'test'
297 'description' => 'test'
285 })
298 })
286 end
299 end
287 attachment = Attachment.order('id DESC').first
300 attachment = Attachment.order('id DESC').first
288 assert_equal issue, attachment.container
301 assert_equal issue, attachment.container
289 assert_equal 'testfile.txt', attachment.filename
302 assert_equal 'testfile.txt', attachment.filename
290 assert_equal 59, attachment.filesize
303 assert_equal 59, attachment.filesize
291 assert_equal 'test', attachment.description
304 assert_equal 'test', attachment.description
292 assert_equal 'text/plain', attachment.content_type
305 assert_equal 'text/plain', attachment.content_type
293 assert File.exists?(attachment.diskfile)
306 assert File.exists?(attachment.diskfile)
294 assert_equal 59, File.size(attachment.diskfile)
307 assert_equal 59, File.size(attachment.diskfile)
295 end
308 end
296
309
297 test "Attachmnet.attach_files should add unsaved files to the object as unsaved attachments" do
310 test "Attachmnet.attach_files should add unsaved files to the object as unsaved attachments" do
298 # Max size of 0 to force Attachment creation failures
311 # Max size of 0 to force Attachment creation failures
299 with_settings(:attachment_max_size => 0) do
312 with_settings(:attachment_max_size => 0) do
300 @project = Project.find(1)
313 @project = Project.find(1)
301 response = Attachment.attach_files(@project, {
314 response = Attachment.attach_files(@project, {
302 '1' => {'file' => mock_file, 'description' => 'test'},
315 '1' => {'file' => mock_file, 'description' => 'test'},
303 '2' => {'file' => mock_file, 'description' => 'test'}
316 '2' => {'file' => mock_file, 'description' => 'test'}
304 })
317 })
305
318
306 assert response[:unsaved].present?
319 assert response[:unsaved].present?
307 assert_equal 2, response[:unsaved].length
320 assert_equal 2, response[:unsaved].length
308 assert response[:unsaved].first.new_record?
321 assert response[:unsaved].first.new_record?
309 assert response[:unsaved].second.new_record?
322 assert response[:unsaved].second.new_record?
310 assert_equal response[:unsaved], @project.unsaved_attachments
323 assert_equal response[:unsaved], @project.unsaved_attachments
311 end
324 end
312 end
325 end
313
326
314 test "Attachment.attach_files should preserve the content_type of attachments added by token" do
327 test "Attachment.attach_files should preserve the content_type of attachments added by token" do
315 @project = Project.find(1)
328 @project = Project.find(1)
316 attachment = Attachment.create!(:file => uploaded_test_file("testfile.txt", ""), :author_id => 1, :created_on => 2.days.ago)
329 attachment = Attachment.create!(:file => uploaded_test_file("testfile.txt", ""), :author_id => 1, :created_on => 2.days.ago)
317 assert_equal 'text/plain', attachment.content_type
330 assert_equal 'text/plain', attachment.content_type
318 Attachment.attach_files(@project, { '1' => {'token' => attachment.token } })
331 Attachment.attach_files(@project, { '1' => {'token' => attachment.token } })
319 attachment.reload
332 attachment.reload
320 assert_equal 'text/plain', attachment.content_type
333 assert_equal 'text/plain', attachment.content_type
321 end
334 end
322
335
323 def test_update_attachments
336 def test_update_attachments
324 attachments = Attachment.where(:id => [2, 3]).to_a
337 attachments = Attachment.where(:id => [2, 3]).to_a
325
338
326 assert Attachment.update_attachments(attachments, {
339 assert Attachment.update_attachments(attachments, {
327 '2' => {:filename => 'newname.txt', :description => 'New description'},
340 '2' => {:filename => 'newname.txt', :description => 'New description'},
328 3 => {:filename => 'othername.txt'}
341 3 => {:filename => 'othername.txt'}
329 })
342 })
330
343
331 attachment = Attachment.find(2)
344 attachment = Attachment.find(2)
332 assert_equal 'newname.txt', attachment.filename
345 assert_equal 'newname.txt', attachment.filename
333 assert_equal 'New description', attachment.description
346 assert_equal 'New description', attachment.description
334
347
335 attachment = Attachment.find(3)
348 attachment = Attachment.find(3)
336 assert_equal 'othername.txt', attachment.filename
349 assert_equal 'othername.txt', attachment.filename
337 end
350 end
338
351
339 def test_update_attachments_with_failure
352 def test_update_attachments_with_failure
340 attachments = Attachment.where(:id => [2, 3]).to_a
353 attachments = Attachment.where(:id => [2, 3]).to_a
341
354
342 assert !Attachment.update_attachments(attachments, {
355 assert !Attachment.update_attachments(attachments, {
343 '2' => {:filename => '', :description => 'New description'},
356 '2' => {:filename => '', :description => 'New description'},
344 3 => {:filename => 'othername.txt'}
357 3 => {:filename => 'othername.txt'}
345 })
358 })
346
359
347 attachment = Attachment.find(3)
360 attachment = Attachment.find(3)
348 assert_equal 'logo.gif', attachment.filename
361 assert_equal 'logo.gif', attachment.filename
349 end
362 end
350
363
351 def test_update_attachments_should_sanitize_filename
364 def test_update_attachments_should_sanitize_filename
352 attachments = Attachment.where(:id => 2).to_a
365 attachments = Attachment.where(:id => 2).to_a
353
366
354 assert Attachment.update_attachments(attachments, {
367 assert Attachment.update_attachments(attachments, {
355 2 => {:filename => 'newname?.txt'},
368 2 => {:filename => 'newname?.txt'},
356 })
369 })
357
370
358 attachment = Attachment.find(2)
371 attachment = Attachment.find(2)
359 assert_equal 'newname_.txt', attachment.filename
372 assert_equal 'newname_.txt', attachment.filename
360 end
373 end
361
374
362 def test_latest_attach
375 def test_latest_attach
363 set_fixtures_attachments_directory
376 set_fixtures_attachments_directory
364 a1 = Attachment.find(16)
377 a1 = Attachment.find(16)
365 assert_equal "testfile.png", a1.filename
378 assert_equal "testfile.png", a1.filename
366 assert a1.readable?
379 assert a1.readable?
367 assert (! a1.visible?(User.anonymous))
380 assert (! a1.visible?(User.anonymous))
368 assert a1.visible?(User.find(2))
381 assert a1.visible?(User.find(2))
369 a2 = Attachment.find(17)
382 a2 = Attachment.find(17)
370 assert_equal "testfile.PNG", a2.filename
383 assert_equal "testfile.PNG", a2.filename
371 assert a2.readable?
384 assert a2.readable?
372 assert (! a2.visible?(User.anonymous))
385 assert (! a2.visible?(User.anonymous))
373 assert a2.visible?(User.find(2))
386 assert a2.visible?(User.find(2))
374 assert a1.created_on < a2.created_on
387 assert a1.created_on < a2.created_on
375
388
376 la1 = Attachment.latest_attach([a1, a2], "testfile.png")
389 la1 = Attachment.latest_attach([a1, a2], "testfile.png")
377 assert_equal 17, la1.id
390 assert_equal 17, la1.id
378 la2 = Attachment.latest_attach([a1, a2], "Testfile.PNG")
391 la2 = Attachment.latest_attach([a1, a2], "Testfile.PNG")
379 assert_equal 17, la2.id
392 assert_equal 17, la2.id
380
393
381 set_tmp_attachments_directory
394 set_tmp_attachments_directory
382 end
395 end
383
396
384 def test_latest_attach_should_not_error_with_string_with_invalid_encoding
397 def test_latest_attach_should_not_error_with_string_with_invalid_encoding
385 string = "width:50\xFE-Image.jpg".force_encoding('UTF-8')
398 string = "width:50\xFE-Image.jpg".force_encoding('UTF-8')
386 assert_equal false, string.valid_encoding?
399 assert_equal false, string.valid_encoding?
387
400
388 Attachment.latest_attach(Attachment.limit(2).to_a, string)
401 Attachment.latest_attach(Attachment.limit(2).to_a, string)
389 end
402 end
390
403
391 def test_thumbnailable_should_be_true_for_images
404 def test_thumbnailable_should_be_true_for_images
392 assert_equal true, Attachment.new(:filename => 'test.jpg').thumbnailable?
405 assert_equal true, Attachment.new(:filename => 'test.jpg').thumbnailable?
393 end
406 end
394
407
395 def test_thumbnailable_should_be_true_for_non_images
408 def test_thumbnailable_should_be_true_for_non_images
396 assert_equal false, Attachment.new(:filename => 'test.txt').thumbnailable?
409 assert_equal false, Attachment.new(:filename => 'test.txt').thumbnailable?
397 end
410 end
398
411
399 if convert_installed?
412 if convert_installed?
400 def test_thumbnail_should_generate_the_thumbnail
413 def test_thumbnail_should_generate_the_thumbnail
401 set_fixtures_attachments_directory
414 set_fixtures_attachments_directory
402 attachment = Attachment.find(16)
415 attachment = Attachment.find(16)
403 Attachment.clear_thumbnails
416 Attachment.clear_thumbnails
404
417
405 assert_difference "Dir.glob(File.join(Attachment.thumbnails_storage_path, '*.thumb')).size" do
418 assert_difference "Dir.glob(File.join(Attachment.thumbnails_storage_path, '*.thumb')).size" do
406 thumbnail = attachment.thumbnail
419 thumbnail = attachment.thumbnail
407 assert_equal "16_8e0294de2441577c529f170b6fb8f638_100.thumb", File.basename(thumbnail)
420 assert_equal "16_8e0294de2441577c529f170b6fb8f638_100.thumb", File.basename(thumbnail)
408 assert File.exists?(thumbnail)
421 assert File.exists?(thumbnail)
409 end
422 end
410 end
423 end
411
424
412 def test_thumbnail_should_return_nil_if_generation_fails
425 def test_thumbnail_should_return_nil_if_generation_fails
413 Redmine::Thumbnail.expects(:generate).raises(SystemCallError, 'Something went wrong')
426 Redmine::Thumbnail.expects(:generate).raises(SystemCallError, 'Something went wrong')
414 set_fixtures_attachments_directory
427 set_fixtures_attachments_directory
415 attachment = Attachment.find(16)
428 attachment = Attachment.find(16)
416 assert_nil attachment.thumbnail
429 assert_nil attachment.thumbnail
417 end
430 end
418 else
431 else
419 puts '(ImageMagick convert not available)'
432 puts '(ImageMagick convert not available)'
420 end
433 end
421 end
434 end
General Comments 0
You need to be logged in to leave comments. Login now