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