##// END OF EJS Templates
Send the content type as parameter when uploading a file....
Jean-Philippe Lang -
r13406:93690ee83032
parent child
Show More
@@ -1,190 +1,191
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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 class AttachmentsController < ApplicationController
18 class AttachmentsController < ApplicationController
19 before_filter :find_attachment, :only => [:show, :download, :thumbnail, :destroy]
19 before_filter :find_attachment, :only => [:show, :download, :thumbnail, :destroy]
20 before_filter :find_editable_attachments, :only => [:edit, :update]
20 before_filter :find_editable_attachments, :only => [:edit, :update]
21 before_filter :file_readable, :read_authorize, :only => [:show, :download, :thumbnail]
21 before_filter :file_readable, :read_authorize, :only => [:show, :download, :thumbnail]
22 before_filter :delete_authorize, :only => :destroy
22 before_filter :delete_authorize, :only => :destroy
23 before_filter :authorize_global, :only => :upload
23 before_filter :authorize_global, :only => :upload
24
24
25 accept_api_auth :show, :download, :upload
25 accept_api_auth :show, :download, :upload
26
26
27 def show
27 def show
28 respond_to do |format|
28 respond_to do |format|
29 format.html {
29 format.html {
30 if @attachment.is_diff?
30 if @attachment.is_diff?
31 @diff = File.new(@attachment.diskfile, "rb").read
31 @diff = File.new(@attachment.diskfile, "rb").read
32 @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
32 @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
33 @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
33 @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
34 # Save diff type as user preference
34 # Save diff type as user preference
35 if User.current.logged? && @diff_type != User.current.pref[:diff_type]
35 if User.current.logged? && @diff_type != User.current.pref[:diff_type]
36 User.current.pref[:diff_type] = @diff_type
36 User.current.pref[:diff_type] = @diff_type
37 User.current.preference.save
37 User.current.preference.save
38 end
38 end
39 render :action => 'diff'
39 render :action => 'diff'
40 elsif @attachment.is_text? && @attachment.filesize <= Setting.file_max_size_displayed.to_i.kilobyte
40 elsif @attachment.is_text? && @attachment.filesize <= Setting.file_max_size_displayed.to_i.kilobyte
41 @content = File.new(@attachment.diskfile, "rb").read
41 @content = File.new(@attachment.diskfile, "rb").read
42 render :action => 'file'
42 render :action => 'file'
43 else
43 else
44 download
44 download
45 end
45 end
46 }
46 }
47 format.api
47 format.api
48 end
48 end
49 end
49 end
50
50
51 def download
51 def download
52 if @attachment.container.is_a?(Version) || @attachment.container.is_a?(Project)
52 if @attachment.container.is_a?(Version) || @attachment.container.is_a?(Project)
53 @attachment.increment_download
53 @attachment.increment_download
54 end
54 end
55
55
56 if stale?(:etag => @attachment.digest)
56 if stale?(:etag => @attachment.digest)
57 # images are sent inline
57 # images are sent inline
58 send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename),
58 send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename),
59 :type => detect_content_type(@attachment),
59 :type => detect_content_type(@attachment),
60 :disposition => (@attachment.image? ? 'inline' : 'attachment')
60 :disposition => (@attachment.image? ? 'inline' : 'attachment')
61 end
61 end
62 end
62 end
63
63
64 def thumbnail
64 def thumbnail
65 if @attachment.thumbnailable? && tbnail = @attachment.thumbnail(:size => params[:size])
65 if @attachment.thumbnailable? && tbnail = @attachment.thumbnail(:size => params[:size])
66 if stale?(:etag => tbnail)
66 if stale?(:etag => tbnail)
67 send_file tbnail,
67 send_file tbnail,
68 :filename => filename_for_content_disposition(@attachment.filename),
68 :filename => filename_for_content_disposition(@attachment.filename),
69 :type => detect_content_type(@attachment),
69 :type => detect_content_type(@attachment),
70 :disposition => 'inline'
70 :disposition => 'inline'
71 end
71 end
72 else
72 else
73 # No thumbnail for the attachment or thumbnail could not be created
73 # No thumbnail for the attachment or thumbnail could not be created
74 render :nothing => true, :status => 404
74 render :nothing => true, :status => 404
75 end
75 end
76 end
76 end
77
77
78 def upload
78 def upload
79 # Make sure that API users get used to set this content type
79 # Make sure that API users get used to set this content type
80 # as it won't trigger Rails' automatic parsing of the request body for parameters
80 # as it won't trigger Rails' automatic parsing of the request body for parameters
81 unless request.content_type == 'application/octet-stream'
81 unless request.content_type == 'application/octet-stream'
82 render :nothing => true, :status => 406
82 render :nothing => true, :status => 406
83 return
83 return
84 end
84 end
85
85
86 @attachment = Attachment.new(:file => request.raw_post)
86 @attachment = Attachment.new(:file => request.raw_post)
87 @attachment.author = User.current
87 @attachment.author = User.current
88 @attachment.filename = params[:filename].presence || Redmine::Utils.random_hex(16)
88 @attachment.filename = params[:filename].presence || Redmine::Utils.random_hex(16)
89 @attachment.content_type = params[:content_type].presence
89 saved = @attachment.save
90 saved = @attachment.save
90
91
91 respond_to do |format|
92 respond_to do |format|
92 format.js
93 format.js
93 format.api {
94 format.api {
94 if saved
95 if saved
95 render :action => 'upload', :status => :created
96 render :action => 'upload', :status => :created
96 else
97 else
97 render_validation_errors(@attachment)
98 render_validation_errors(@attachment)
98 end
99 end
99 }
100 }
100 end
101 end
101 end
102 end
102
103
103 def edit
104 def edit
104 end
105 end
105
106
106 def update
107 def update
107 if params[:attachments].is_a?(Hash)
108 if params[:attachments].is_a?(Hash)
108 if Attachment.update_attachments(@attachments, params[:attachments])
109 if Attachment.update_attachments(@attachments, params[:attachments])
109 redirect_back_or_default home_path
110 redirect_back_or_default home_path
110 return
111 return
111 end
112 end
112 end
113 end
113 render :action => 'edit'
114 render :action => 'edit'
114 end
115 end
115
116
116 def destroy
117 def destroy
117 if @attachment.container.respond_to?(:init_journal)
118 if @attachment.container.respond_to?(:init_journal)
118 @attachment.container.init_journal(User.current)
119 @attachment.container.init_journal(User.current)
119 end
120 end
120 if @attachment.container
121 if @attachment.container
121 # Make sure association callbacks are called
122 # Make sure association callbacks are called
122 @attachment.container.attachments.delete(@attachment)
123 @attachment.container.attachments.delete(@attachment)
123 else
124 else
124 @attachment.destroy
125 @attachment.destroy
125 end
126 end
126
127
127 respond_to do |format|
128 respond_to do |format|
128 format.html { redirect_to_referer_or project_path(@project) }
129 format.html { redirect_to_referer_or project_path(@project) }
129 format.js
130 format.js
130 end
131 end
131 end
132 end
132
133
133 private
134 private
134
135
135 def find_attachment
136 def find_attachment
136 @attachment = Attachment.find(params[:id])
137 @attachment = Attachment.find(params[:id])
137 # Show 404 if the filename in the url is wrong
138 # Show 404 if the filename in the url is wrong
138 raise ActiveRecord::RecordNotFound if params[:filename] && params[:filename] != @attachment.filename
139 raise ActiveRecord::RecordNotFound if params[:filename] && params[:filename] != @attachment.filename
139 @project = @attachment.project
140 @project = @attachment.project
140 rescue ActiveRecord::RecordNotFound
141 rescue ActiveRecord::RecordNotFound
141 render_404
142 render_404
142 end
143 end
143
144
144 def find_editable_attachments
145 def find_editable_attachments
145 klass = params[:object_type].to_s.singularize.classify.constantize rescue nil
146 klass = params[:object_type].to_s.singularize.classify.constantize rescue nil
146 unless klass && klass.reflect_on_association(:attachments)
147 unless klass && klass.reflect_on_association(:attachments)
147 render_404
148 render_404
148 return
149 return
149 end
150 end
150
151
151 @container = klass.find(params[:object_id])
152 @container = klass.find(params[:object_id])
152 if @container.respond_to?(:visible?) && !@container.visible?
153 if @container.respond_to?(:visible?) && !@container.visible?
153 render_403
154 render_403
154 return
155 return
155 end
156 end
156 @attachments = @container.attachments.select(&:editable?)
157 @attachments = @container.attachments.select(&:editable?)
157 if @container.respond_to?(:project)
158 if @container.respond_to?(:project)
158 @project = @container.project
159 @project = @container.project
159 end
160 end
160 render_404 if @attachments.empty?
161 render_404 if @attachments.empty?
161 rescue ActiveRecord::RecordNotFound
162 rescue ActiveRecord::RecordNotFound
162 render_404
163 render_404
163 end
164 end
164
165
165 # Checks that the file exists and is readable
166 # Checks that the file exists and is readable
166 def file_readable
167 def file_readable
167 if @attachment.readable?
168 if @attachment.readable?
168 true
169 true
169 else
170 else
170 logger.error "Cannot send attachment, #{@attachment.diskfile} does not exist or is unreadable."
171 logger.error "Cannot send attachment, #{@attachment.diskfile} does not exist or is unreadable."
171 render_404
172 render_404
172 end
173 end
173 end
174 end
174
175
175 def read_authorize
176 def read_authorize
176 @attachment.visible? ? true : deny_access
177 @attachment.visible? ? true : deny_access
177 end
178 end
178
179
179 def delete_authorize
180 def delete_authorize
180 @attachment.deletable? ? true : deny_access
181 @attachment.deletable? ? true : deny_access
181 end
182 end
182
183
183 def detect_content_type(attachment)
184 def detect_content_type(attachment)
184 content_type = attachment.content_type
185 content_type = attachment.content_type
185 if content_type.blank?
186 if content_type.blank?
186 content_type = Redmine::MimeType.of(attachment.filename)
187 content_type = Redmine::MimeType.of(attachment.filename)
187 end
188 end
188 content_type.to_s
189 content_type.to_s
189 end
190 end
190 end
191 end
@@ -1,190 +1,191
1 /* Redmine - project management software
1 /* Redmine - project management software
2 Copyright (C) 2006-2014 Jean-Philippe Lang */
2 Copyright (C) 2006-2014 Jean-Philippe Lang */
3
3
4 function addFile(inputEl, file, eagerUpload) {
4 function addFile(inputEl, file, eagerUpload) {
5
5
6 if ($('#attachments_fields').children().length < 10) {
6 if ($('#attachments_fields').children().length < 10) {
7
7
8 var attachmentId = addFile.nextAttachmentId++;
8 var attachmentId = addFile.nextAttachmentId++;
9
9
10 var fileSpan = $('<span>', { id: 'attachments_' + attachmentId });
10 var fileSpan = $('<span>', { id: 'attachments_' + attachmentId });
11
11
12 fileSpan.append(
12 fileSpan.append(
13 $('<input>', { type: 'text', 'class': 'filename readonly', name: 'attachments[' + attachmentId + '][filename]', readonly: 'readonly'} ).val(file.name),
13 $('<input>', { type: 'text', 'class': 'filename readonly', name: 'attachments[' + attachmentId + '][filename]', readonly: 'readonly'} ).val(file.name),
14 $('<input>', { type: 'text', 'class': 'description', name: 'attachments[' + attachmentId + '][description]', maxlength: 255, placeholder: $(inputEl).data('description-placeholder') } ).toggle(!eagerUpload),
14 $('<input>', { type: 'text', 'class': 'description', name: 'attachments[' + attachmentId + '][description]', maxlength: 255, placeholder: $(inputEl).data('description-placeholder') } ).toggle(!eagerUpload),
15 $('<a>&nbsp</a>').attr({ href: "#", 'class': 'remove-upload' }).click(removeFile).toggle(!eagerUpload)
15 $('<a>&nbsp</a>').attr({ href: "#", 'class': 'remove-upload' }).click(removeFile).toggle(!eagerUpload)
16 ).appendTo('#attachments_fields');
16 ).appendTo('#attachments_fields');
17
17
18 if(eagerUpload) {
18 if(eagerUpload) {
19 ajaxUpload(file, attachmentId, fileSpan, inputEl);
19 ajaxUpload(file, attachmentId, fileSpan, inputEl);
20 }
20 }
21
21
22 return attachmentId;
22 return attachmentId;
23 }
23 }
24 return null;
24 return null;
25 }
25 }
26
26
27 addFile.nextAttachmentId = 1;
27 addFile.nextAttachmentId = 1;
28
28
29 function ajaxUpload(file, attachmentId, fileSpan, inputEl) {
29 function ajaxUpload(file, attachmentId, fileSpan, inputEl) {
30
30
31 function onLoadstart(e) {
31 function onLoadstart(e) {
32 fileSpan.removeClass('ajax-waiting');
32 fileSpan.removeClass('ajax-waiting');
33 fileSpan.addClass('ajax-loading');
33 fileSpan.addClass('ajax-loading');
34 $('input:submit', $(this).parents('form')).attr('disabled', 'disabled');
34 $('input:submit', $(this).parents('form')).attr('disabled', 'disabled');
35 }
35 }
36
36
37 function onProgress(e) {
37 function onProgress(e) {
38 if(e.lengthComputable) {
38 if(e.lengthComputable) {
39 this.progressbar( 'value', e.loaded * 100 / e.total );
39 this.progressbar( 'value', e.loaded * 100 / e.total );
40 }
40 }
41 }
41 }
42
42
43 function actualUpload(file, attachmentId, fileSpan, inputEl) {
43 function actualUpload(file, attachmentId, fileSpan, inputEl) {
44
44
45 ajaxUpload.uploading++;
45 ajaxUpload.uploading++;
46
46
47 uploadBlob(file, $(inputEl).data('upload-path'), attachmentId, {
47 uploadBlob(file, $(inputEl).data('upload-path'), attachmentId, {
48 loadstartEventHandler: onLoadstart.bind(progressSpan),
48 loadstartEventHandler: onLoadstart.bind(progressSpan),
49 progressEventHandler: onProgress.bind(progressSpan)
49 progressEventHandler: onProgress.bind(progressSpan)
50 })
50 })
51 .done(function(result) {
51 .done(function(result) {
52 progressSpan.progressbar( 'value', 100 ).remove();
52 progressSpan.progressbar( 'value', 100 ).remove();
53 fileSpan.find('input.description, a').css('display', 'inline-block');
53 fileSpan.find('input.description, a').css('display', 'inline-block');
54 })
54 })
55 .fail(function(result) {
55 .fail(function(result) {
56 progressSpan.text(result.statusText);
56 progressSpan.text(result.statusText);
57 }).always(function() {
57 }).always(function() {
58 ajaxUpload.uploading--;
58 ajaxUpload.uploading--;
59 fileSpan.removeClass('ajax-loading');
59 fileSpan.removeClass('ajax-loading');
60 var form = fileSpan.parents('form');
60 var form = fileSpan.parents('form');
61 if (form.queue('upload').length == 0 && ajaxUpload.uploading == 0) {
61 if (form.queue('upload').length == 0 && ajaxUpload.uploading == 0) {
62 $('input:submit', form).removeAttr('disabled');
62 $('input:submit', form).removeAttr('disabled');
63 }
63 }
64 form.dequeue('upload');
64 form.dequeue('upload');
65 });
65 });
66 }
66 }
67
67
68 var progressSpan = $('<div>').insertAfter(fileSpan.find('input.filename'));
68 var progressSpan = $('<div>').insertAfter(fileSpan.find('input.filename'));
69 progressSpan.progressbar();
69 progressSpan.progressbar();
70 fileSpan.addClass('ajax-waiting');
70 fileSpan.addClass('ajax-waiting');
71
71
72 var maxSyncUpload = $(inputEl).data('max-concurrent-uploads');
72 var maxSyncUpload = $(inputEl).data('max-concurrent-uploads');
73
73
74 if(maxSyncUpload == null || maxSyncUpload <= 0 || ajaxUpload.uploading < maxSyncUpload)
74 if(maxSyncUpload == null || maxSyncUpload <= 0 || ajaxUpload.uploading < maxSyncUpload)
75 actualUpload(file, attachmentId, fileSpan, inputEl);
75 actualUpload(file, attachmentId, fileSpan, inputEl);
76 else
76 else
77 $(inputEl).parents('form').queue('upload', actualUpload.bind(this, file, attachmentId, fileSpan, inputEl));
77 $(inputEl).parents('form').queue('upload', actualUpload.bind(this, file, attachmentId, fileSpan, inputEl));
78 }
78 }
79
79
80 ajaxUpload.uploading = 0;
80 ajaxUpload.uploading = 0;
81
81
82 function removeFile() {
82 function removeFile() {
83 $(this).parent('span').remove();
83 $(this).parent('span').remove();
84 return false;
84 return false;
85 }
85 }
86
86
87 function uploadBlob(blob, uploadUrl, attachmentId, options) {
87 function uploadBlob(blob, uploadUrl, attachmentId, options) {
88
88
89 var actualOptions = $.extend({
89 var actualOptions = $.extend({
90 loadstartEventHandler: $.noop,
90 loadstartEventHandler: $.noop,
91 progressEventHandler: $.noop
91 progressEventHandler: $.noop
92 }, options);
92 }, options);
93
93
94 uploadUrl = uploadUrl + '?attachment_id=' + attachmentId;
94 uploadUrl = uploadUrl + '?attachment_id=' + attachmentId;
95 if (blob instanceof window.File) {
95 if (blob instanceof window.File) {
96 uploadUrl += '&filename=' + encodeURIComponent(blob.name);
96 uploadUrl += '&filename=' + encodeURIComponent(blob.name);
97 uploadUrl += '&content_type=' + encodeURIComponent(blob.type);
97 }
98 }
98
99
99 return $.ajax(uploadUrl, {
100 return $.ajax(uploadUrl, {
100 type: 'POST',
101 type: 'POST',
101 contentType: 'application/octet-stream',
102 contentType: 'application/octet-stream',
102 beforeSend: function(jqXhr, settings) {
103 beforeSend: function(jqXhr, settings) {
103 jqXhr.setRequestHeader('Accept', 'application/js');
104 jqXhr.setRequestHeader('Accept', 'application/js');
104 // attach proper File object
105 // attach proper File object
105 settings.data = blob;
106 settings.data = blob;
106 },
107 },
107 xhr: function() {
108 xhr: function() {
108 var xhr = $.ajaxSettings.xhr();
109 var xhr = $.ajaxSettings.xhr();
109 xhr.upload.onloadstart = actualOptions.loadstartEventHandler;
110 xhr.upload.onloadstart = actualOptions.loadstartEventHandler;
110 xhr.upload.onprogress = actualOptions.progressEventHandler;
111 xhr.upload.onprogress = actualOptions.progressEventHandler;
111 return xhr;
112 return xhr;
112 },
113 },
113 data: blob,
114 data: blob,
114 cache: false,
115 cache: false,
115 processData: false
116 processData: false
116 });
117 });
117 }
118 }
118
119
119 function addInputFiles(inputEl) {
120 function addInputFiles(inputEl) {
120 var clearedFileInput = $(inputEl).clone().val('');
121 var clearedFileInput = $(inputEl).clone().val('');
121
122
122 if ($.ajaxSettings.xhr().upload && inputEl.files) {
123 if ($.ajaxSettings.xhr().upload && inputEl.files) {
123 // upload files using ajax
124 // upload files using ajax
124 uploadAndAttachFiles(inputEl.files, inputEl);
125 uploadAndAttachFiles(inputEl.files, inputEl);
125 $(inputEl).remove();
126 $(inputEl).remove();
126 } else {
127 } else {
127 // browser not supporting the file API, upload on form submission
128 // browser not supporting the file API, upload on form submission
128 var attachmentId;
129 var attachmentId;
129 var aFilename = inputEl.value.split(/\/|\\/);
130 var aFilename = inputEl.value.split(/\/|\\/);
130 attachmentId = addFile(inputEl, { name: aFilename[ aFilename.length - 1 ] }, false);
131 attachmentId = addFile(inputEl, { name: aFilename[ aFilename.length - 1 ] }, false);
131 if (attachmentId) {
132 if (attachmentId) {
132 $(inputEl).attr({ name: 'attachments[' + attachmentId + '][file]', style: 'display:none;' }).appendTo('#attachments_' + attachmentId);
133 $(inputEl).attr({ name: 'attachments[' + attachmentId + '][file]', style: 'display:none;' }).appendTo('#attachments_' + attachmentId);
133 }
134 }
134 }
135 }
135
136
136 clearedFileInput.insertAfter('#attachments_fields');
137 clearedFileInput.insertAfter('#attachments_fields');
137 }
138 }
138
139
139 function uploadAndAttachFiles(files, inputEl) {
140 function uploadAndAttachFiles(files, inputEl) {
140
141
141 var maxFileSize = $(inputEl).data('max-file-size');
142 var maxFileSize = $(inputEl).data('max-file-size');
142 var maxFileSizeExceeded = $(inputEl).data('max-file-size-message');
143 var maxFileSizeExceeded = $(inputEl).data('max-file-size-message');
143
144
144 var sizeExceeded = false;
145 var sizeExceeded = false;
145 $.each(files, function() {
146 $.each(files, function() {
146 if (this.size && maxFileSize != null && this.size > parseInt(maxFileSize)) {sizeExceeded=true;}
147 if (this.size && maxFileSize != null && this.size > parseInt(maxFileSize)) {sizeExceeded=true;}
147 });
148 });
148 if (sizeExceeded) {
149 if (sizeExceeded) {
149 window.alert(maxFileSizeExceeded);
150 window.alert(maxFileSizeExceeded);
150 } else {
151 } else {
151 $.each(files, function() {addFile(inputEl, this, true);});
152 $.each(files, function() {addFile(inputEl, this, true);});
152 }
153 }
153 }
154 }
154
155
155 function handleFileDropEvent(e) {
156 function handleFileDropEvent(e) {
156
157
157 $(this).removeClass('fileover');
158 $(this).removeClass('fileover');
158 blockEventPropagation(e);
159 blockEventPropagation(e);
159
160
160 if ($.inArray('Files', e.dataTransfer.types) > -1) {
161 if ($.inArray('Files', e.dataTransfer.types) > -1) {
161 uploadAndAttachFiles(e.dataTransfer.files, $('input:file.file_selector'));
162 uploadAndAttachFiles(e.dataTransfer.files, $('input:file.file_selector'));
162 }
163 }
163 }
164 }
164
165
165 function dragOverHandler(e) {
166 function dragOverHandler(e) {
166 $(this).addClass('fileover');
167 $(this).addClass('fileover');
167 blockEventPropagation(e);
168 blockEventPropagation(e);
168 }
169 }
169
170
170 function dragOutHandler(e) {
171 function dragOutHandler(e) {
171 $(this).removeClass('fileover');
172 $(this).removeClass('fileover');
172 blockEventPropagation(e);
173 blockEventPropagation(e);
173 }
174 }
174
175
175 function setupFileDrop() {
176 function setupFileDrop() {
176 if (window.File && window.FileList && window.ProgressEvent && window.FormData) {
177 if (window.File && window.FileList && window.ProgressEvent && window.FormData) {
177
178
178 $.event.fixHooks.drop = { props: [ 'dataTransfer' ] };
179 $.event.fixHooks.drop = { props: [ 'dataTransfer' ] };
179
180
180 $('form div.box').has('input:file').each(function() {
181 $('form div.box').has('input:file').each(function() {
181 $(this).on({
182 $(this).on({
182 dragover: dragOverHandler,
183 dragover: dragOverHandler,
183 dragleave: dragOutHandler,
184 dragleave: dragOutHandler,
184 drop: handleFileDropEvent
185 drop: handleFileDropEvent
185 });
186 });
186 });
187 });
187 }
188 }
188 }
189 }
189
190
190 $(document).ready(setupFileDrop);
191 $(document).ready(setupFileDrop);
@@ -1,142 +1,152
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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 File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class AttachmentsTest < Redmine::IntegrationTest
20 class AttachmentsTest < Redmine::IntegrationTest
21 fixtures :projects, :enabled_modules,
21 fixtures :projects, :enabled_modules,
22 :users, :roles, :members, :member_roles,
22 :users, :roles, :members, :member_roles,
23 :trackers, :projects_trackers,
23 :trackers, :projects_trackers,
24 :issue_statuses, :enumerations
24 :issue_statuses, :enumerations
25
25
26 def test_upload_should_set_default_content_type
26 def test_upload_should_set_default_content_type
27 log_user('jsmith', 'jsmith')
27 log_user('jsmith', 'jsmith')
28 assert_difference 'Attachment.count' do
28 assert_difference 'Attachment.count' do
29 post "/uploads.js?attachment_id=1&filename=foo.txt", "File content", {"CONTENT_TYPE" => 'application/octet-stream'}
29 post "/uploads.js?attachment_id=1&filename=foo.txt", "File content", {"CONTENT_TYPE" => 'application/octet-stream'}
30 assert_response :success
30 assert_response :success
31 end
31 end
32 attachment = Attachment.order(:id => :desc).first
32 attachment = Attachment.order(:id => :desc).first
33 assert_equal 'text/plain', attachment.content_type
33 assert_equal 'text/plain', attachment.content_type
34 end
34 end
35
35
36 def test_upload_should_accept_content_type_param
37 log_user('jsmith', 'jsmith')
38 assert_difference 'Attachment.count' do
39 post "/uploads.js?attachment_id=1&filename=foo&content_type=image/jpeg", "File content", {"CONTENT_TYPE" => 'application/octet-stream'}
40 assert_response :success
41 end
42 attachment = Attachment.order(:id => :desc).first
43 assert_equal 'image/jpeg', attachment.content_type
44 end
45
36 def test_upload_as_js_and_attach_to_an_issue
46 def test_upload_as_js_and_attach_to_an_issue
37 log_user('jsmith', 'jsmith')
47 log_user('jsmith', 'jsmith')
38
48
39 token = ajax_upload('myupload.txt', 'File content')
49 token = ajax_upload('myupload.txt', 'File content')
40
50
41 assert_difference 'Issue.count' do
51 assert_difference 'Issue.count' do
42 post '/projects/ecookbook/issues', {
52 post '/projects/ecookbook/issues', {
43 :issue => {:tracker_id => 1, :subject => 'Issue with upload'},
53 :issue => {:tracker_id => 1, :subject => 'Issue with upload'},
44 :attachments => {'1' => {:filename => 'myupload.txt', :description => 'My uploaded file', :token => token}}
54 :attachments => {'1' => {:filename => 'myupload.txt', :description => 'My uploaded file', :token => token}}
45 }
55 }
46 assert_response 302
56 assert_response 302
47 end
57 end
48
58
49 issue = Issue.order('id DESC').first
59 issue = Issue.order('id DESC').first
50 assert_equal 'Issue with upload', issue.subject
60 assert_equal 'Issue with upload', issue.subject
51 assert_equal 1, issue.attachments.count
61 assert_equal 1, issue.attachments.count
52
62
53 attachment = issue.attachments.first
63 attachment = issue.attachments.first
54 assert_equal 'myupload.txt', attachment.filename
64 assert_equal 'myupload.txt', attachment.filename
55 assert_equal 'My uploaded file', attachment.description
65 assert_equal 'My uploaded file', attachment.description
56 assert_equal 'File content'.length, attachment.filesize
66 assert_equal 'File content'.length, attachment.filesize
57 end
67 end
58
68
59 def test_upload_as_js_and_preview_as_inline_attachment
69 def test_upload_as_js_and_preview_as_inline_attachment
60 log_user('jsmith', 'jsmith')
70 log_user('jsmith', 'jsmith')
61
71
62 token = ajax_upload('myupload.jpg', 'JPEG content')
72 token = ajax_upload('myupload.jpg', 'JPEG content')
63
73
64 post '/issues/preview/new/ecookbook', {
74 post '/issues/preview/new/ecookbook', {
65 :issue => {:tracker_id => 1, :description => 'Inline upload: !myupload.jpg!'},
75 :issue => {:tracker_id => 1, :description => 'Inline upload: !myupload.jpg!'},
66 :attachments => {'1' => {:filename => 'myupload.jpg', :description => 'My uploaded file', :token => token}}
76 :attachments => {'1' => {:filename => 'myupload.jpg', :description => 'My uploaded file', :token => token}}
67 }
77 }
68 assert_response :success
78 assert_response :success
69
79
70 attachment_path = response.body.match(%r{<img src="(/attachments/download/\d+/myupload.jpg)"})[1]
80 attachment_path = response.body.match(%r{<img src="(/attachments/download/\d+/myupload.jpg)"})[1]
71 assert_not_nil token, "No attachment path found in response:\n#{response.body}"
81 assert_not_nil token, "No attachment path found in response:\n#{response.body}"
72
82
73 get attachment_path
83 get attachment_path
74 assert_response :success
84 assert_response :success
75 assert_equal 'JPEG content', response.body
85 assert_equal 'JPEG content', response.body
76 end
86 end
77
87
78 def test_upload_and_resubmit_after_validation_failure
88 def test_upload_and_resubmit_after_validation_failure
79 log_user('jsmith', 'jsmith')
89 log_user('jsmith', 'jsmith')
80
90
81 token = ajax_upload('myupload.txt', 'File content')
91 token = ajax_upload('myupload.txt', 'File content')
82
92
83 assert_no_difference 'Issue.count' do
93 assert_no_difference 'Issue.count' do
84 post '/projects/ecookbook/issues', {
94 post '/projects/ecookbook/issues', {
85 :issue => {:tracker_id => 1, :subject => ''},
95 :issue => {:tracker_id => 1, :subject => ''},
86 :attachments => {'1' => {:filename => 'myupload.txt', :description => 'My uploaded file', :token => token}}
96 :attachments => {'1' => {:filename => 'myupload.txt', :description => 'My uploaded file', :token => token}}
87 }
97 }
88 assert_response :success
98 assert_response :success
89 end
99 end
90 assert_select 'input[type=hidden][name=?][value=?]', 'attachments[p0][token]', token
100 assert_select 'input[type=hidden][name=?][value=?]', 'attachments[p0][token]', token
91 assert_select 'input[name=?][value=?]', 'attachments[p0][filename]', 'myupload.txt'
101 assert_select 'input[name=?][value=?]', 'attachments[p0][filename]', 'myupload.txt'
92 assert_select 'input[name=?][value=?]', 'attachments[p0][description]', 'My uploaded file'
102 assert_select 'input[name=?][value=?]', 'attachments[p0][description]', 'My uploaded file'
93
103
94 assert_difference 'Issue.count' do
104 assert_difference 'Issue.count' do
95 post '/projects/ecookbook/issues', {
105 post '/projects/ecookbook/issues', {
96 :issue => {:tracker_id => 1, :subject => 'Issue with upload'},
106 :issue => {:tracker_id => 1, :subject => 'Issue with upload'},
97 :attachments => {'p0' => {:filename => 'myupload.txt', :description => 'My uploaded file', :token => token}}
107 :attachments => {'p0' => {:filename => 'myupload.txt', :description => 'My uploaded file', :token => token}}
98 }
108 }
99 assert_response 302
109 assert_response 302
100 end
110 end
101
111
102 issue = Issue.order('id DESC').first
112 issue = Issue.order('id DESC').first
103 assert_equal 'Issue with upload', issue.subject
113 assert_equal 'Issue with upload', issue.subject
104 assert_equal 1, issue.attachments.count
114 assert_equal 1, issue.attachments.count
105
115
106 attachment = issue.attachments.first
116 attachment = issue.attachments.first
107 assert_equal 'myupload.txt', attachment.filename
117 assert_equal 'myupload.txt', attachment.filename
108 assert_equal 'My uploaded file', attachment.description
118 assert_equal 'My uploaded file', attachment.description
109 assert_equal 'File content'.length, attachment.filesize
119 assert_equal 'File content'.length, attachment.filesize
110 end
120 end
111
121
112 def test_upload_as_js_and_destroy
122 def test_upload_as_js_and_destroy
113 log_user('jsmith', 'jsmith')
123 log_user('jsmith', 'jsmith')
114
124
115 token = ajax_upload('myupload.txt', 'File content')
125 token = ajax_upload('myupload.txt', 'File content')
116
126
117 attachment = Attachment.order('id DESC').first
127 attachment = Attachment.order('id DESC').first
118 attachment_path = "/attachments/#{attachment.id}.js?attachment_id=1"
128 attachment_path = "/attachments/#{attachment.id}.js?attachment_id=1"
119 assert_include "href: '#{attachment_path}'", response.body, "Path to attachment: #{attachment_path} not found in response:\n#{response.body}"
129 assert_include "href: '#{attachment_path}'", response.body, "Path to attachment: #{attachment_path} not found in response:\n#{response.body}"
120
130
121 assert_difference 'Attachment.count', -1 do
131 assert_difference 'Attachment.count', -1 do
122 delete attachment_path
132 delete attachment_path
123 assert_response :success
133 assert_response :success
124 end
134 end
125
135
126 assert_include "$('#attachments_1').remove();", response.body
136 assert_include "$('#attachments_1').remove();", response.body
127 end
137 end
128
138
129 private
139 private
130
140
131 def ajax_upload(filename, content, attachment_id=1)
141 def ajax_upload(filename, content, attachment_id=1)
132 assert_difference 'Attachment.count' do
142 assert_difference 'Attachment.count' do
133 post "/uploads.js?attachment_id=#{attachment_id}&filename=#{filename}", content, {"CONTENT_TYPE" => 'application/octet-stream'}
143 post "/uploads.js?attachment_id=#{attachment_id}&filename=#{filename}", content, {"CONTENT_TYPE" => 'application/octet-stream'}
134 assert_response :success
144 assert_response :success
135 assert_equal 'text/javascript', response.content_type
145 assert_equal 'text/javascript', response.content_type
136 end
146 end
137
147
138 token = response.body.match(/\.val\('(\d+\.[0-9a-f]+)'\)/)[1]
148 token = response.body.match(/\.val\('(\d+\.[0-9a-f]+)'\)/)[1]
139 assert_not_nil token, "No upload token found in response:\n#{response.body}"
149 assert_not_nil token, "No upload token found in response:\n#{response.body}"
140 token
150 token
141 end
151 end
142 end
152 end
General Comments 0
You need to be logged in to leave comments. Login now