@@ -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> </a>').attr({ href: "#", 'class': 'remove-upload' }).click(removeFile).toggle(!eagerUpload) |
|
15 | $('<a> </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