@@ -16,11 +16,12 | |||||
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_project |
|
19 | before_filter :find_project, :except => :upload | |
20 |
before_filter :file_readable, :read_authorize, : |
|
20 | before_filter :file_readable, :read_authorize, :only => [:show, :download] | |
21 | before_filter :delete_authorize, :only => :destroy |
|
21 | before_filter :delete_authorize, :only => :destroy | |
|
22 | before_filter :authorize_global, :only => :upload | |||
22 |
|
23 | |||
23 | accept_api_auth :show, :download |
|
24 | accept_api_auth :show, :download, :upload | |
24 |
|
25 | |||
25 | def show |
|
26 | def show | |
26 | respond_to do |format| |
|
27 | respond_to do |format| | |
@@ -58,6 +59,29 class AttachmentsController < ApplicationController | |||||
58 |
|
59 | |||
59 | end |
|
60 | end | |
60 |
|
61 | |||
|
62 | def upload | |||
|
63 | # Make sure that API users get used to set this content type | |||
|
64 | # as it won't trigger Rails' automatic parsing of the request body for parameters | |||
|
65 | unless request.content_type == 'application/octet-stream' | |||
|
66 | render :nothing => true, :status => 406 | |||
|
67 | return | |||
|
68 | end | |||
|
69 | ||||
|
70 | @attachment = Attachment.new(:file => request.body) | |||
|
71 | @attachment.author = User.current | |||
|
72 | @attachment.filename = "test" #ActiveSupport::SecureRandom.hex(16) | |||
|
73 | ||||
|
74 | if @attachment.save | |||
|
75 | respond_to do |format| | |||
|
76 | format.api { render :action => 'upload', :status => :created } | |||
|
77 | end | |||
|
78 | else | |||
|
79 | respond_to do |format| | |||
|
80 | format.api { render_validation_errors(@attachment) } | |||
|
81 | end | |||
|
82 | end | |||
|
83 | end | |||
|
84 | ||||
61 | verify :method => :delete, :only => :destroy |
|
85 | verify :method => :delete, :only => :destroy | |
62 | def destroy |
|
86 | def destroy | |
63 | # Make sure association callbacks are called |
|
87 | # Make sure association callbacks are called |
@@ -149,7 +149,7 class IssuesController < ApplicationController | |||||
149 |
|
149 | |||
150 | def create |
|
150 | def create | |
151 | call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue }) |
|
151 | call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue }) | |
152 | @issue.save_attachments(params[:attachments]) |
|
152 | @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads])) | |
153 | if @issue.save |
|
153 | if @issue.save | |
154 | call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue}) |
|
154 | call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue}) | |
155 | respond_to do |format| |
|
155 | respond_to do |format| | |
@@ -181,7 +181,7 class IssuesController < ApplicationController | |||||
181 |
|
181 | |||
182 | def update |
|
182 | def update | |
183 | return unless update_issue_from_params |
|
183 | return unless update_issue_from_params | |
184 | @issue.save_attachments(params[:attachments]) |
|
184 | @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads])) | |
185 | saved = false |
|
185 | saved = false | |
186 | begin |
|
186 | begin | |
187 | saved = @issue.save_issue_with_child_records(params, @time_entry) |
|
187 | saved = @issue.save_issue_with_child_records(params, @time_entry) |
@@ -76,21 +76,32 class Attachment < ActiveRecord::Base | |||||
76 | unless incoming_file.nil? |
|
76 | unless incoming_file.nil? | |
77 | @temp_file = incoming_file |
|
77 | @temp_file = incoming_file | |
78 | if @temp_file.size > 0 |
|
78 | if @temp_file.size > 0 | |
79 |
|
|
79 | if @temp_file.respond_to?(:original_filename) | |
80 | self.disk_filename = Attachment.disk_filename(filename) |
|
80 | self.filename = @temp_file.original_filename | |
81 | self.content_type = @temp_file.content_type.to_s.chomp |
|
81 | end | |
82 | if content_type.blank? |
|
82 | if @temp_file.respond_to?(:content_type) | |
|
83 | self.content_type = @temp_file.content_type.to_s.chomp | |||
|
84 | end | |||
|
85 | if content_type.blank? && filename.present? | |||
83 | self.content_type = Redmine::MimeType.of(filename) |
|
86 | self.content_type = Redmine::MimeType.of(filename) | |
84 | end |
|
87 | end | |
85 | self.filesize = @temp_file.size |
|
88 | self.filesize = @temp_file.size | |
86 | end |
|
89 | end | |
87 | end |
|
90 | end | |
88 | end |
|
91 | end | |
89 |
|
92 | |||
90 | def file |
|
93 | def file | |
91 | nil |
|
94 | nil | |
92 | end |
|
95 | end | |
93 |
|
96 | |||
|
97 | def filename=(arg) | |||
|
98 | write_attribute :filename, sanitize_filename(arg.to_s) | |||
|
99 | if new_record? && disk_filename.blank? | |||
|
100 | self.disk_filename = Attachment.disk_filename(filename) | |||
|
101 | end | |||
|
102 | filename | |||
|
103 | end | |||
|
104 | ||||
94 | # Copies the temporary file to its final location |
|
105 | # Copies the temporary file to its final location | |
95 | # and computes its MD5 hash |
|
106 | # and computes its MD5 hash | |
96 | def files_to_final_location |
|
107 | def files_to_final_location |
@@ -409,6 +409,8 ActionController::Routing::Routes.draw do |map| | |||||
409 | :conditions => {:method => :get} |
|
409 | :conditions => {:method => :get} | |
410 | end |
|
410 | end | |
411 |
|
411 | |||
|
412 | map.connect 'uploads.:format', :controller => 'attachments', :action => 'upload', :conditions => {:method => :post} | |||
|
413 | ||||
412 | map.connect 'robots.txt', :controller => 'welcome', |
|
414 | map.connect 'robots.txt', :controller => 'welcome', | |
413 | :action => 'robots', :conditions => {:method => :get} |
|
415 | :action => 'robots', :conditions => {:method => :get} | |
414 |
|
416 |
@@ -67,13 +67,13 Redmine::AccessControl.map do |map| | |||||
67 | :journals => [:index, :diff], |
|
67 | :journals => [:index, :diff], | |
68 | :queries => :index, |
|
68 | :queries => :index, | |
69 | :reports => [:issue_report, :issue_report_details]} |
|
69 | :reports => [:issue_report, :issue_report_details]} | |
70 | map.permission :add_issues, {:issues => [:new, :create, :update_form]} |
|
70 | map.permission :add_issues, {:issues => [:new, :create, :update_form], :attachments => :upload} | |
71 | map.permission :edit_issues, {:issues => [:edit, :update, :bulk_edit, :bulk_update, :update_form], :journals => [:new]} |
|
71 | map.permission :edit_issues, {:issues => [:edit, :update, :bulk_edit, :bulk_update, :update_form], :journals => [:new], :attachments => :upload} | |
72 | map.permission :manage_issue_relations, {:issue_relations => [:index, :show, :create, :destroy]} |
|
72 | map.permission :manage_issue_relations, {:issue_relations => [:index, :show, :create, :destroy]} | |
73 | map.permission :manage_subtasks, {} |
|
73 | map.permission :manage_subtasks, {} | |
74 | map.permission :set_issues_private, {} |
|
74 | map.permission :set_issues_private, {} | |
75 | map.permission :set_own_issues_private, {}, :require => :loggedin |
|
75 | map.permission :set_own_issues_private, {}, :require => :loggedin | |
76 | map.permission :add_issue_notes, {:issues => [:edit, :update], :journals => [:new]} |
|
76 | map.permission :add_issue_notes, {:issues => [:edit, :update], :journals => [:new], :attachments => :upload} | |
77 | map.permission :edit_issue_notes, {:journals => :edit}, :require => :loggedin |
|
77 | map.permission :edit_issue_notes, {:journals => :edit}, :require => :loggedin | |
78 | map.permission :edit_own_issue_notes, {:journals => :edit}, :require => :loggedin |
|
78 | map.permission :edit_own_issue_notes, {:journals => :edit}, :require => :loggedin | |
79 | map.permission :move_issues, {:issues => [:bulk_edit, :bulk_update]}, :require => :loggedin |
|
79 | map.permission :move_issues, {:issues => [:bulk_edit, :bulk_update]}, :require => :loggedin |
@@ -82,4 +82,39 class ApiTest::AttachmentsTest < ActionController::IntegrationTest | |||||
82 | end |
|
82 | end | |
83 | end |
|
83 | end | |
84 | end |
|
84 | end | |
|
85 | ||||
|
86 | context "POST /uploads" do | |||
|
87 | should "return the token" do | |||
|
88 | set_tmp_attachments_directory | |||
|
89 | assert_difference 'Attachment.count' do | |||
|
90 | post '/uploads.xml', 'File content', {'Content-Type' => 'application/octet-stream'}.merge(credentials('jsmith')) | |||
|
91 | assert_response :created | |||
|
92 | assert_equal 'application/xml', response.content_type | |||
|
93 | ||||
|
94 | xml = Hash.from_xml(response.body) | |||
|
95 | assert_kind_of Hash, xml['upload'] | |||
|
96 | token = xml['upload']['token'] | |||
|
97 | assert_not_nil token | |||
|
98 | ||||
|
99 | attachment = Attachment.first(:order => 'id DESC') | |||
|
100 | assert_equal token, attachment.token | |||
|
101 | assert_nil attachment.container | |||
|
102 | assert_equal 2, attachment.author_id | |||
|
103 | assert_equal 'File content'.size, attachment.filesize | |||
|
104 | assert attachment.content_type.blank? | |||
|
105 | assert attachment.filename.present? | |||
|
106 | assert_match /\d+_[0-9a-z]+/, attachment.diskfile | |||
|
107 | assert File.exist?(attachment.diskfile) | |||
|
108 | assert_equal 'File content', File.read(attachment.diskfile) | |||
|
109 | end | |||
|
110 | end | |||
|
111 | ||||
|
112 | should "not accept other content types" do | |||
|
113 | set_tmp_attachments_directory | |||
|
114 | assert_no_difference 'Attachment.count' do | |||
|
115 | post '/uploads.xml', 'PNG DATA', {'Content-Type' => 'image/png'}.merge(credentials('jsmith')) | |||
|
116 | assert_response 406 | |||
|
117 | end | |||
|
118 | end | |||
|
119 | end | |||
85 | end |
|
120 | end |
@@ -707,4 +707,72 class ApiTest::IssuesTest < ActionController::IntegrationTest | |||||
707 | assert_nil Issue.find_by_id(6) |
|
707 | assert_nil Issue.find_by_id(6) | |
708 | end |
|
708 | end | |
709 | end |
|
709 | end | |
|
710 | ||||
|
711 | def test_create_issue_with_uploaded_file | |||
|
712 | set_tmp_attachments_directory | |||
|
713 | ||||
|
714 | # upload the file | |||
|
715 | assert_difference 'Attachment.count' do | |||
|
716 | post '/uploads.xml', 'test_create_with_upload', {'Content-Type' => 'application/octet-stream'}.merge(credentials('jsmith')) | |||
|
717 | assert_response :created | |||
|
718 | end | |||
|
719 | xml = Hash.from_xml(response.body) | |||
|
720 | token = xml['upload']['token'] | |||
|
721 | attachment = Attachment.first(:order => 'id DESC') | |||
|
722 | ||||
|
723 | # create the issue with the upload's token | |||
|
724 | assert_difference 'Issue.count' do | |||
|
725 | post '/issues.xml', | |||
|
726 | {:issue => {:project_id => 1, :subject => 'Uploaded file', :uploads => [{:token => token, :filename => 'test.txt', :content_type => 'text/plain'}]}}, | |||
|
727 | credentials('jsmith') | |||
|
728 | assert_response :created | |||
|
729 | end | |||
|
730 | issue = Issue.first(:order => 'id DESC') | |||
|
731 | assert_equal 1, issue.attachments.count | |||
|
732 | assert_equal attachment, issue.attachments.first | |||
|
733 | ||||
|
734 | attachment.reload | |||
|
735 | assert_equal 'test.txt', attachment.filename | |||
|
736 | assert_equal 'text/plain', attachment.content_type | |||
|
737 | assert_equal 'test_create_with_upload'.size, attachment.filesize | |||
|
738 | assert_equal 2, attachment.author_id | |||
|
739 | ||||
|
740 | # get the issue with its attachments | |||
|
741 | get "/issues/#{issue.id}.xml", :include => 'attachments' | |||
|
742 | assert_response :success | |||
|
743 | xml = Hash.from_xml(response.body) | |||
|
744 | attachments = xml['issue']['attachments'] | |||
|
745 | assert_kind_of Array, attachments | |||
|
746 | assert_equal 1, attachments.size | |||
|
747 | url = attachments.first['content_url'] | |||
|
748 | assert_not_nil url | |||
|
749 | ||||
|
750 | # download the attachment | |||
|
751 | get url | |||
|
752 | assert_response :success | |||
|
753 | end | |||
|
754 | ||||
|
755 | def test_update_issue_with_uploaded_file | |||
|
756 | set_tmp_attachments_directory | |||
|
757 | ||||
|
758 | # upload the file | |||
|
759 | assert_difference 'Attachment.count' do | |||
|
760 | post '/uploads.xml', 'test_upload_with_upload', {'Content-Type' => 'application/octet-stream'}.merge(credentials('jsmith')) | |||
|
761 | assert_response :created | |||
|
762 | end | |||
|
763 | xml = Hash.from_xml(response.body) | |||
|
764 | token = xml['upload']['token'] | |||
|
765 | attachment = Attachment.first(:order => 'id DESC') | |||
|
766 | ||||
|
767 | # update the issue with the upload's token | |||
|
768 | assert_difference 'Journal.count' do | |||
|
769 | put '/issues/1.xml', | |||
|
770 | {:issue => {:notes => 'Attachment added', :uploads => [{:token => token, :filename => 'test.txt', :content_type => 'text/plain'}]}}, | |||
|
771 | credentials('jsmith') | |||
|
772 | assert_response :ok | |||
|
773 | end | |||
|
774 | ||||
|
775 | issue = Issue.find(1) | |||
|
776 | assert_include attachment, issue.attachments | |||
|
777 | end | |||
710 | end |
|
778 | end |
@@ -49,5 +49,13 class RoutingAttachmentsTest < ActionController::IntegrationTest | |||||
49 | { :method => 'delete', :path => "/attachments/1" }, |
|
49 | { :method => 'delete', :path => "/attachments/1" }, | |
50 | { :controller => 'attachments', :action => 'destroy', :id => '1' } |
|
50 | { :controller => 'attachments', :action => 'destroy', :id => '1' } | |
51 | ) |
|
51 | ) | |
|
52 | assert_routing( | |||
|
53 | { :method => 'post', :path => '/uploads.xml' }, | |||
|
54 | { :controller => 'attachments', :action => 'upload', :format => 'xml' } | |||
|
55 | ) | |||
|
56 | assert_routing( | |||
|
57 | { :method => 'post', :path => '/uploads.json' }, | |||
|
58 | { :controller => 'attachments', :action => 'upload', :format => 'json' } | |||
|
59 | ) | |||
52 | end |
|
60 | end | |
53 | end |
|
61 | end |
@@ -61,18 +61,23 module Redmine | |||||
61 | end |
|
61 | end | |
62 |
|
62 | |||
63 | def save_attachments(attachments, author=User.current) |
|
63 | def save_attachments(attachments, author=User.current) | |
64 |
if |
|
64 | if attachments.is_a?(Hash) | |
65 |
attachments. |
|
65 | attachments = attachments.values | |
|
66 | end | |||
|
67 | if attachments.is_a?(Array) | |||
|
68 | attachments.each do |attachment| | |||
66 | a = nil |
|
69 | a = nil | |
67 | if file = attachment['file'] |
|
70 | if file = attachment['file'] | |
68 |
next unless |
|
71 | next unless file.size > 0 | |
69 | a = Attachment.create(:file => file, |
|
72 | a = Attachment.create(:file => file, :author => author) | |
70 | :description => attachment['description'].to_s.strip, |
|
|||
71 | :author => author) |
|
|||
72 | elsif token = attachment['token'] |
|
73 | elsif token = attachment['token'] | |
73 | a = Attachment.find_by_token(token) |
|
74 | a = Attachment.find_by_token(token) | |
|
75 | next unless a | |||
|
76 | a.filename = attachment['filename'] unless attachment['filename'].blank? | |||
|
77 | a.content_type = attachment['content_type'] | |||
74 | end |
|
78 | end | |
75 | next unless a |
|
79 | next unless a | |
|
80 | a.description = attachment['description'].to_s.strip | |||
76 | if a.new_record? |
|
81 | if a.new_record? | |
77 | unsaved_attachments << a |
|
82 | unsaved_attachments << a | |
78 | else |
|
83 | else |
General Comments 0
You need to be logged in to leave comments.
Login now