@@ -16,11 +16,12 | |||
|
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | 18 | class AttachmentsController < ApplicationController |
|
19 | before_filter :find_project | |
|
20 |
before_filter :file_readable, :read_authorize, : |
|
|
19 | before_filter :find_project, :except => :upload | |
|
20 | before_filter :file_readable, :read_authorize, :only => [:show, :download] | |
|
21 | 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 | 26 | def show |
|
26 | 27 | respond_to do |format| |
@@ -58,6 +59,29 class AttachmentsController < ApplicationController | |||
|
58 | 59 | |
|
59 | 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 | 85 | verify :method => :delete, :only => :destroy |
|
62 | 86 | def destroy |
|
63 | 87 | # Make sure association callbacks are called |
@@ -149,7 +149,7 class IssuesController < ApplicationController | |||
|
149 | 149 | |
|
150 | 150 | def create |
|
151 | 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 | 153 | if @issue.save |
|
154 | 154 | call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue}) |
|
155 | 155 | respond_to do |format| |
@@ -181,7 +181,7 class IssuesController < ApplicationController | |||
|
181 | 181 | |
|
182 | 182 | def update |
|
183 | 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 | 185 | saved = false |
|
186 | 186 | begin |
|
187 | 187 | saved = @issue.save_issue_with_child_records(params, @time_entry) |
@@ -76,21 +76,32 class Attachment < ActiveRecord::Base | |||
|
76 | 76 | unless incoming_file.nil? |
|
77 | 77 | @temp_file = incoming_file |
|
78 | 78 | if @temp_file.size > 0 |
|
79 |
|
|
|
80 | self.disk_filename = Attachment.disk_filename(filename) | |
|
81 | self.content_type = @temp_file.content_type.to_s.chomp | |
|
82 | if content_type.blank? | |
|
79 | if @temp_file.respond_to?(:original_filename) | |
|
80 | self.filename = @temp_file.original_filename | |
|
81 | end | |
|
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 | 86 | self.content_type = Redmine::MimeType.of(filename) |
|
84 | 87 | end |
|
85 | 88 | self.filesize = @temp_file.size |
|
86 | 89 | end |
|
87 | 90 | end |
|
88 | 91 | end |
|
89 | ||
|
92 | ||
|
90 | 93 | def file |
|
91 | 94 | nil |
|
92 | 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 | 105 | # Copies the temporary file to its final location |
|
95 | 106 | # and computes its MD5 hash |
|
96 | 107 | def files_to_final_location |
@@ -409,6 +409,8 ActionController::Routing::Routes.draw do |map| | |||
|
409 | 409 | :conditions => {:method => :get} |
|
410 | 410 | end |
|
411 | 411 | |
|
412 | map.connect 'uploads.:format', :controller => 'attachments', :action => 'upload', :conditions => {:method => :post} | |
|
413 | ||
|
412 | 414 | map.connect 'robots.txt', :controller => 'welcome', |
|
413 | 415 | :action => 'robots', :conditions => {:method => :get} |
|
414 | 416 |
@@ -67,13 +67,13 Redmine::AccessControl.map do |map| | |||
|
67 | 67 | :journals => [:index, :diff], |
|
68 | 68 | :queries => :index, |
|
69 | 69 | :reports => [:issue_report, :issue_report_details]} |
|
70 | map.permission :add_issues, {:issues => [:new, :create, :update_form]} | |
|
71 | map.permission :edit_issues, {:issues => [:edit, :update, :bulk_edit, :bulk_update, :update_form], :journals => [:new]} | |
|
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], :attachments => :upload} | |
|
72 | 72 | map.permission :manage_issue_relations, {:issue_relations => [:index, :show, :create, :destroy]} |
|
73 | 73 | map.permission :manage_subtasks, {} |
|
74 | 74 | map.permission :set_issues_private, {} |
|
75 | 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 | 77 | map.permission :edit_issue_notes, {:journals => :edit}, :require => :loggedin |
|
78 | 78 | map.permission :edit_own_issue_notes, {:journals => :edit}, :require => :loggedin |
|
79 | 79 | map.permission :move_issues, {:issues => [:bulk_edit, :bulk_update]}, :require => :loggedin |
@@ -82,4 +82,39 class ApiTest::AttachmentsTest < ActionController::IntegrationTest | |||
|
82 | 82 | end |
|
83 | 83 | end |
|
84 | 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 | 120 | end |
@@ -707,4 +707,72 class ApiTest::IssuesTest < ActionController::IntegrationTest | |||
|
707 | 707 | assert_nil Issue.find_by_id(6) |
|
708 | 708 | end |
|
709 | 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 | 778 | end |
@@ -49,5 +49,13 class RoutingAttachmentsTest < ActionController::IntegrationTest | |||
|
49 | 49 | { :method => 'delete', :path => "/attachments/1" }, |
|
50 | 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 | 60 | end |
|
53 | 61 | end |
@@ -61,18 +61,23 module Redmine | |||
|
61 | 61 | end |
|
62 | 62 | |
|
63 | 63 | def save_attachments(attachments, author=User.current) |
|
64 |
if |
|
|
65 |
attachments. |
|
|
64 | if attachments.is_a?(Hash) | |
|
65 | attachments = attachments.values | |
|
66 | end | |
|
67 | if attachments.is_a?(Array) | |
|
68 | attachments.each do |attachment| | |
|
66 | 69 | a = nil |
|
67 | 70 | if file = attachment['file'] |
|
68 |
next unless |
|
|
69 | a = Attachment.create(:file => file, | |
|
70 | :description => attachment['description'].to_s.strip, | |
|
71 | :author => author) | |
|
71 | next unless file.size > 0 | |
|
72 | a = Attachment.create(:file => file, :author => author) | |
|
72 | 73 | elsif token = attachment['token'] |
|
73 | 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 | 78 | end |
|
75 | 79 | next unless a |
|
80 | a.description = attachment['description'].to_s.strip | |
|
76 | 81 | if a.new_record? |
|
77 | 82 | unsaved_attachments << a |
|
78 | 83 | else |
General Comments 0
You need to be logged in to leave comments.
Login now