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