##// END OF EJS Templates
Always send images user-uploaded images with Content-Disposition: attachment (#24199)....
Jean-Philippe Lang -
r15903:34c5b51cf095
parent child
Show More
@@ -1,228 +1,228
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 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_action :find_attachment, :only => [:show, :download, :thumbnail, :update, :destroy]
19 before_action :find_attachment, :only => [:show, :download, :thumbnail, :update, :destroy]
20 before_action :find_editable_attachments, :only => [:edit_all, :update_all]
20 before_action :find_editable_attachments, :only => [:edit_all, :update_all]
21 before_action :file_readable, :read_authorize, :only => [:show, :download, :thumbnail]
21 before_action :file_readable, :read_authorize, :only => [:show, :download, :thumbnail]
22 before_action :update_authorize, :only => :update
22 before_action :update_authorize, :only => :update
23 before_action :delete_authorize, :only => :destroy
23 before_action :delete_authorize, :only => :destroy
24 before_action :authorize_global, :only => :upload
24 before_action :authorize_global, :only => :upload
25
25
26 # Disable check for same origin requests for JS files, i.e. attachments with
26 # Disable check for same origin requests for JS files, i.e. attachments with
27 # MIME type text/javascript.
27 # MIME type text/javascript.
28 skip_after_filter :verify_same_origin_request, :only => :download
28 skip_after_filter :verify_same_origin_request, :only => :download
29
29
30 accept_api_auth :show, :download, :thumbnail, :upload, :update, :destroy
30 accept_api_auth :show, :download, :thumbnail, :upload, :update, :destroy
31
31
32 def show
32 def show
33 respond_to do |format|
33 respond_to do |format|
34 format.html {
34 format.html {
35 if @attachment.is_diff?
35 if @attachment.is_diff?
36 @diff = File.read(@attachment.diskfile, :mode => "rb")
36 @diff = File.read(@attachment.diskfile, :mode => "rb")
37 @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
37 @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
38 @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
38 @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
39 # Save diff type as user preference
39 # Save diff type as user preference
40 if User.current.logged? && @diff_type != User.current.pref[:diff_type]
40 if User.current.logged? && @diff_type != User.current.pref[:diff_type]
41 User.current.pref[:diff_type] = @diff_type
41 User.current.pref[:diff_type] = @diff_type
42 User.current.preference.save
42 User.current.preference.save
43 end
43 end
44 render :action => 'diff'
44 render :action => 'diff'
45 elsif @attachment.is_text? && @attachment.filesize <= Setting.file_max_size_displayed.to_i.kilobyte
45 elsif @attachment.is_text? && @attachment.filesize <= Setting.file_max_size_displayed.to_i.kilobyte
46 @content = File.read(@attachment.diskfile, :mode => "rb")
46 @content = File.read(@attachment.diskfile, :mode => "rb")
47 render :action => 'file'
47 render :action => 'file'
48 elsif @attachment.is_image?
48 elsif @attachment.is_image?
49 render :action => 'image'
49 render :action => 'image'
50 else
50 else
51 render :action => 'other'
51 render :action => 'other'
52 end
52 end
53 }
53 }
54 format.api
54 format.api
55 end
55 end
56 end
56 end
57
57
58 def download
58 def download
59 if @attachment.container.is_a?(Version) || @attachment.container.is_a?(Project)
59 if @attachment.container.is_a?(Version) || @attachment.container.is_a?(Project)
60 @attachment.increment_download
60 @attachment.increment_download
61 end
61 end
62
62
63 if stale?(:etag => @attachment.digest)
63 if stale?(:etag => @attachment.digest)
64 # images are sent inline
64 # images are sent inline
65 send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename),
65 send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename),
66 :type => detect_content_type(@attachment),
66 :type => detect_content_type(@attachment),
67 :disposition => disposition(@attachment)
67 :disposition => disposition(@attachment)
68 end
68 end
69 end
69 end
70
70
71 def thumbnail
71 def thumbnail
72 if @attachment.thumbnailable? && tbnail = @attachment.thumbnail(:size => params[:size])
72 if @attachment.thumbnailable? && tbnail = @attachment.thumbnail(:size => params[:size])
73 if stale?(:etag => tbnail)
73 if stale?(:etag => tbnail)
74 send_file tbnail,
74 send_file tbnail,
75 :filename => filename_for_content_disposition(@attachment.filename),
75 :filename => filename_for_content_disposition(@attachment.filename),
76 :type => detect_content_type(@attachment),
76 :type => detect_content_type(@attachment),
77 :disposition => 'inline'
77 :disposition => 'inline'
78 end
78 end
79 else
79 else
80 # No thumbnail for the attachment or thumbnail could not be created
80 # No thumbnail for the attachment or thumbnail could not be created
81 head 404
81 head 404
82 end
82 end
83 end
83 end
84
84
85 def upload
85 def upload
86 # Make sure that API users get used to set this content type
86 # Make sure that API users get used to set this content type
87 # as it won't trigger Rails' automatic parsing of the request body for parameters
87 # as it won't trigger Rails' automatic parsing of the request body for parameters
88 unless request.content_type == 'application/octet-stream'
88 unless request.content_type == 'application/octet-stream'
89 head 406
89 head 406
90 return
90 return
91 end
91 end
92
92
93 @attachment = Attachment.new(:file => request.raw_post)
93 @attachment = Attachment.new(:file => request.raw_post)
94 @attachment.author = User.current
94 @attachment.author = User.current
95 @attachment.filename = params[:filename].presence || Redmine::Utils.random_hex(16)
95 @attachment.filename = params[:filename].presence || Redmine::Utils.random_hex(16)
96 @attachment.content_type = params[:content_type].presence
96 @attachment.content_type = params[:content_type].presence
97 saved = @attachment.save
97 saved = @attachment.save
98
98
99 respond_to do |format|
99 respond_to do |format|
100 format.js
100 format.js
101 format.api {
101 format.api {
102 if saved
102 if saved
103 render :action => 'upload', :status => :created
103 render :action => 'upload', :status => :created
104 else
104 else
105 render_validation_errors(@attachment)
105 render_validation_errors(@attachment)
106 end
106 end
107 }
107 }
108 end
108 end
109 end
109 end
110
110
111 # Edit all the attachments of a container
111 # Edit all the attachments of a container
112 def edit_all
112 def edit_all
113 end
113 end
114
114
115 # Update all the attachments of a container
115 # Update all the attachments of a container
116 def update_all
116 def update_all
117 if params[:attachments].is_a?(Hash)
117 if params[:attachments].is_a?(Hash)
118 if Attachment.update_attachments(@attachments, params[:attachments])
118 if Attachment.update_attachments(@attachments, params[:attachments])
119 redirect_back_or_default home_path
119 redirect_back_or_default home_path
120 return
120 return
121 end
121 end
122 end
122 end
123 render :action => 'edit_all'
123 render :action => 'edit_all'
124 end
124 end
125
125
126 def update
126 def update
127 @attachment.safe_attributes = params[:attachment]
127 @attachment.safe_attributes = params[:attachment]
128 saved = @attachment.save
128 saved = @attachment.save
129
129
130 respond_to do |format|
130 respond_to do |format|
131 format.api {
131 format.api {
132 if saved
132 if saved
133 render_api_ok
133 render_api_ok
134 else
134 else
135 render_validation_errors(@attachment)
135 render_validation_errors(@attachment)
136 end
136 end
137 }
137 }
138 end
138 end
139 end
139 end
140
140
141 def destroy
141 def destroy
142 if @attachment.container.respond_to?(:init_journal)
142 if @attachment.container.respond_to?(:init_journal)
143 @attachment.container.init_journal(User.current)
143 @attachment.container.init_journal(User.current)
144 end
144 end
145 if @attachment.container
145 if @attachment.container
146 # Make sure association callbacks are called
146 # Make sure association callbacks are called
147 @attachment.container.attachments.delete(@attachment)
147 @attachment.container.attachments.delete(@attachment)
148 else
148 else
149 @attachment.destroy
149 @attachment.destroy
150 end
150 end
151
151
152 respond_to do |format|
152 respond_to do |format|
153 format.html { redirect_to_referer_or project_path(@project) }
153 format.html { redirect_to_referer_or project_path(@project) }
154 format.js
154 format.js
155 format.api { render_api_ok }
155 format.api { render_api_ok }
156 end
156 end
157 end
157 end
158
158
159 private
159 private
160
160
161 def find_attachment
161 def find_attachment
162 @attachment = Attachment.find(params[:id])
162 @attachment = Attachment.find(params[:id])
163 # Show 404 if the filename in the url is wrong
163 # Show 404 if the filename in the url is wrong
164 raise ActiveRecord::RecordNotFound if params[:filename] && params[:filename] != @attachment.filename
164 raise ActiveRecord::RecordNotFound if params[:filename] && params[:filename] != @attachment.filename
165 @project = @attachment.project
165 @project = @attachment.project
166 rescue ActiveRecord::RecordNotFound
166 rescue ActiveRecord::RecordNotFound
167 render_404
167 render_404
168 end
168 end
169
169
170 def find_editable_attachments
170 def find_editable_attachments
171 klass = params[:object_type].to_s.singularize.classify.constantize rescue nil
171 klass = params[:object_type].to_s.singularize.classify.constantize rescue nil
172 unless klass && klass.reflect_on_association(:attachments)
172 unless klass && klass.reflect_on_association(:attachments)
173 render_404
173 render_404
174 return
174 return
175 end
175 end
176
176
177 @container = klass.find(params[:object_id])
177 @container = klass.find(params[:object_id])
178 if @container.respond_to?(:visible?) && !@container.visible?
178 if @container.respond_to?(:visible?) && !@container.visible?
179 render_403
179 render_403
180 return
180 return
181 end
181 end
182 @attachments = @container.attachments.select(&:editable?)
182 @attachments = @container.attachments.select(&:editable?)
183 if @container.respond_to?(:project)
183 if @container.respond_to?(:project)
184 @project = @container.project
184 @project = @container.project
185 end
185 end
186 render_404 if @attachments.empty?
186 render_404 if @attachments.empty?
187 rescue ActiveRecord::RecordNotFound
187 rescue ActiveRecord::RecordNotFound
188 render_404
188 render_404
189 end
189 end
190
190
191 # Checks that the file exists and is readable
191 # Checks that the file exists and is readable
192 def file_readable
192 def file_readable
193 if @attachment.readable?
193 if @attachment.readable?
194 true
194 true
195 else
195 else
196 logger.error "Cannot send attachment, #{@attachment.diskfile} does not exist or is unreadable."
196 logger.error "Cannot send attachment, #{@attachment.diskfile} does not exist or is unreadable."
197 render_404
197 render_404
198 end
198 end
199 end
199 end
200
200
201 def read_authorize
201 def read_authorize
202 @attachment.visible? ? true : deny_access
202 @attachment.visible? ? true : deny_access
203 end
203 end
204
204
205 def update_authorize
205 def update_authorize
206 @attachment.editable? ? true : deny_access
206 @attachment.editable? ? true : deny_access
207 end
207 end
208
208
209 def delete_authorize
209 def delete_authorize
210 @attachment.deletable? ? true : deny_access
210 @attachment.deletable? ? true : deny_access
211 end
211 end
212
212
213 def detect_content_type(attachment)
213 def detect_content_type(attachment)
214 content_type = attachment.content_type
214 content_type = attachment.content_type
215 if content_type.blank? || content_type == "application/octet-stream"
215 if content_type.blank? || content_type == "application/octet-stream"
216 content_type = Redmine::MimeType.of(attachment.filename)
216 content_type = Redmine::MimeType.of(attachment.filename)
217 end
217 end
218 content_type.to_s
218 content_type.to_s
219 end
219 end
220
220
221 def disposition(attachment)
221 def disposition(attachment)
222 if attachment.is_image? || attachment.is_pdf?
222 if attachment.is_pdf?
223 'inline'
223 'inline'
224 else
224 else
225 'attachment'
225 'attachment'
226 end
226 end
227 end
227 end
228 end
228 end
@@ -1,439 +1,439
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 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 'SVG/Graph/Bar'
18 require 'SVG/Graph/Bar'
19 require 'SVG/Graph/BarHorizontal'
19 require 'SVG/Graph/BarHorizontal'
20 require 'digest/sha1'
20 require 'digest/sha1'
21 require 'redmine/scm/adapters'
21 require 'redmine/scm/adapters'
22
22
23 class ChangesetNotFound < Exception; end
23 class ChangesetNotFound < Exception; end
24 class InvalidRevisionParam < Exception; end
24 class InvalidRevisionParam < Exception; end
25
25
26 class RepositoriesController < ApplicationController
26 class RepositoriesController < ApplicationController
27 menu_item :repository
27 menu_item :repository
28 menu_item :settings, :only => [:new, :create, :edit, :update, :destroy, :committers]
28 menu_item :settings, :only => [:new, :create, :edit, :update, :destroy, :committers]
29 default_search_scope :changesets
29 default_search_scope :changesets
30
30
31 before_action :find_project_by_project_id, :only => [:new, :create]
31 before_action :find_project_by_project_id, :only => [:new, :create]
32 before_action :build_new_repository_from_params, :only => [:new, :create]
32 before_action :build_new_repository_from_params, :only => [:new, :create]
33 before_action :find_repository, :only => [:edit, :update, :destroy, :committers]
33 before_action :find_repository, :only => [:edit, :update, :destroy, :committers]
34 before_action :find_project_repository, :except => [:new, :create, :edit, :update, :destroy, :committers]
34 before_action :find_project_repository, :except => [:new, :create, :edit, :update, :destroy, :committers]
35 before_action :find_changeset, :only => [:revision, :add_related_issue, :remove_related_issue]
35 before_action :find_changeset, :only => [:revision, :add_related_issue, :remove_related_issue]
36 before_action :authorize
36 before_action :authorize
37 accept_rss_auth :revisions
37 accept_rss_auth :revisions
38
38
39 rescue_from Redmine::Scm::Adapters::CommandFailed, :with => :show_error_command_failed
39 rescue_from Redmine::Scm::Adapters::CommandFailed, :with => :show_error_command_failed
40
40
41 def new
41 def new
42 @repository.is_default = @project.repository.nil?
42 @repository.is_default = @project.repository.nil?
43 end
43 end
44
44
45 def create
45 def create
46 if @repository.save
46 if @repository.save
47 redirect_to settings_project_path(@project, :tab => 'repositories')
47 redirect_to settings_project_path(@project, :tab => 'repositories')
48 else
48 else
49 render :action => 'new'
49 render :action => 'new'
50 end
50 end
51 end
51 end
52
52
53 def edit
53 def edit
54 end
54 end
55
55
56 def update
56 def update
57 @repository.safe_attributes = params[:repository]
57 @repository.safe_attributes = params[:repository]
58 if @repository.save
58 if @repository.save
59 redirect_to settings_project_path(@project, :tab => 'repositories')
59 redirect_to settings_project_path(@project, :tab => 'repositories')
60 else
60 else
61 render :action => 'edit'
61 render :action => 'edit'
62 end
62 end
63 end
63 end
64
64
65 def committers
65 def committers
66 @committers = @repository.committers
66 @committers = @repository.committers
67 @users = @project.users.to_a
67 @users = @project.users.to_a
68 additional_user_ids = @committers.collect(&:last).collect(&:to_i) - @users.collect(&:id)
68 additional_user_ids = @committers.collect(&:last).collect(&:to_i) - @users.collect(&:id)
69 @users += User.where(:id => additional_user_ids).to_a unless additional_user_ids.empty?
69 @users += User.where(:id => additional_user_ids).to_a unless additional_user_ids.empty?
70 @users.compact!
70 @users.compact!
71 @users.sort!
71 @users.sort!
72 if request.post? && params[:committers].is_a?(Hash)
72 if request.post? && params[:committers].is_a?(Hash)
73 # Build a hash with repository usernames as keys and corresponding user ids as values
73 # Build a hash with repository usernames as keys and corresponding user ids as values
74 @repository.committer_ids = params[:committers].values.inject({}) {|h, c| h[c.first] = c.last; h}
74 @repository.committer_ids = params[:committers].values.inject({}) {|h, c| h[c.first] = c.last; h}
75 flash[:notice] = l(:notice_successful_update)
75 flash[:notice] = l(:notice_successful_update)
76 redirect_to settings_project_path(@project, :tab => 'repositories')
76 redirect_to settings_project_path(@project, :tab => 'repositories')
77 end
77 end
78 end
78 end
79
79
80 def destroy
80 def destroy
81 @repository.destroy if request.delete?
81 @repository.destroy if request.delete?
82 redirect_to settings_project_path(@project, :tab => 'repositories')
82 redirect_to settings_project_path(@project, :tab => 'repositories')
83 end
83 end
84
84
85 def show
85 def show
86 @repository.fetch_changesets if @project.active? && Setting.autofetch_changesets? && @path.empty?
86 @repository.fetch_changesets if @project.active? && Setting.autofetch_changesets? && @path.empty?
87
87
88 @entries = @repository.entries(@path, @rev)
88 @entries = @repository.entries(@path, @rev)
89 @changeset = @repository.find_changeset_by_name(@rev)
89 @changeset = @repository.find_changeset_by_name(@rev)
90 if request.xhr?
90 if request.xhr?
91 @entries ? render(:partial => 'dir_list_content') : head(200)
91 @entries ? render(:partial => 'dir_list_content') : head(200)
92 else
92 else
93 (show_error_not_found; return) unless @entries
93 (show_error_not_found; return) unless @entries
94 @changesets = @repository.latest_changesets(@path, @rev)
94 @changesets = @repository.latest_changesets(@path, @rev)
95 @properties = @repository.properties(@path, @rev)
95 @properties = @repository.properties(@path, @rev)
96 @repositories = @project.repositories
96 @repositories = @project.repositories
97 render :action => 'show'
97 render :action => 'show'
98 end
98 end
99 end
99 end
100
100
101 alias_method :browse, :show
101 alias_method :browse, :show
102
102
103 def changes
103 def changes
104 @entry = @repository.entry(@path, @rev)
104 @entry = @repository.entry(@path, @rev)
105 (show_error_not_found; return) unless @entry
105 (show_error_not_found; return) unless @entry
106 @changesets = @repository.latest_changesets(@path, @rev, Setting.repository_log_display_limit.to_i)
106 @changesets = @repository.latest_changesets(@path, @rev, Setting.repository_log_display_limit.to_i)
107 @properties = @repository.properties(@path, @rev)
107 @properties = @repository.properties(@path, @rev)
108 @changeset = @repository.find_changeset_by_name(@rev)
108 @changeset = @repository.find_changeset_by_name(@rev)
109 end
109 end
110
110
111 def revisions
111 def revisions
112 @changeset_count = @repository.changesets.count
112 @changeset_count = @repository.changesets.count
113 @changeset_pages = Paginator.new @changeset_count,
113 @changeset_pages = Paginator.new @changeset_count,
114 per_page_option,
114 per_page_option,
115 params['page']
115 params['page']
116 @changesets = @repository.changesets.
116 @changesets = @repository.changesets.
117 limit(@changeset_pages.per_page).
117 limit(@changeset_pages.per_page).
118 offset(@changeset_pages.offset).
118 offset(@changeset_pages.offset).
119 includes(:user, :repository, :parents).
119 includes(:user, :repository, :parents).
120 to_a
120 to_a
121
121
122 respond_to do |format|
122 respond_to do |format|
123 format.html { render :layout => false if request.xhr? }
123 format.html { render :layout => false if request.xhr? }
124 format.atom { render_feed(@changesets, :title => "#{@project.name}: #{l(:label_revision_plural)}") }
124 format.atom { render_feed(@changesets, :title => "#{@project.name}: #{l(:label_revision_plural)}") }
125 end
125 end
126 end
126 end
127
127
128 def raw
128 def raw
129 entry_and_raw(true)
129 entry_and_raw(true)
130 end
130 end
131
131
132 def entry
132 def entry
133 entry_and_raw(false)
133 entry_and_raw(false)
134 end
134 end
135
135
136 def entry_and_raw(is_raw)
136 def entry_and_raw(is_raw)
137 @entry = @repository.entry(@path, @rev)
137 @entry = @repository.entry(@path, @rev)
138 (show_error_not_found; return) unless @entry
138 (show_error_not_found; return) unless @entry
139
139
140 # If the entry is a dir, show the browser
140 # If the entry is a dir, show the browser
141 (show; return) if @entry.is_dir?
141 (show; return) if @entry.is_dir?
142
142
143 if is_raw
143 if is_raw
144 # Force the download
144 # Force the download
145 send_opt = { :filename => filename_for_content_disposition(@path.split('/').last) }
145 send_opt = { :filename => filename_for_content_disposition(@path.split('/').last) }
146 send_type = Redmine::MimeType.of(@path)
146 send_type = Redmine::MimeType.of(@path)
147 send_opt[:type] = send_type.to_s if send_type
147 send_opt[:type] = send_type.to_s if send_type
148 send_opt[:disposition] = disposition(@path)
148 send_opt[:disposition] = disposition(@path)
149 send_data @repository.cat(@path, @rev), send_opt
149 send_data @repository.cat(@path, @rev), send_opt
150 else
150 else
151 if !@entry.size || @entry.size <= Setting.file_max_size_displayed.to_i.kilobyte
151 if !@entry.size || @entry.size <= Setting.file_max_size_displayed.to_i.kilobyte
152 content = @repository.cat(@path, @rev)
152 content = @repository.cat(@path, @rev)
153 (show_error_not_found; return) unless content
153 (show_error_not_found; return) unless content
154
154
155 if content.size <= Setting.file_max_size_displayed.to_i.kilobyte &&
155 if content.size <= Setting.file_max_size_displayed.to_i.kilobyte &&
156 is_entry_text_data?(content, @path)
156 is_entry_text_data?(content, @path)
157 # TODO: UTF-16
157 # TODO: UTF-16
158 # Prevent empty lines when displaying a file with Windows style eol
158 # Prevent empty lines when displaying a file with Windows style eol
159 # Is this needed? AttachmentsController simply reads file.
159 # Is this needed? AttachmentsController simply reads file.
160 @content = content.gsub("\r\n", "\n")
160 @content = content.gsub("\r\n", "\n")
161 end
161 end
162 end
162 end
163 @changeset = @repository.find_changeset_by_name(@rev)
163 @changeset = @repository.find_changeset_by_name(@rev)
164 end
164 end
165 end
165 end
166 private :entry_and_raw
166 private :entry_and_raw
167
167
168 def is_entry_text_data?(ent, path)
168 def is_entry_text_data?(ent, path)
169 # UTF-16 contains "\x00".
169 # UTF-16 contains "\x00".
170 # It is very strict that file contains less than 30% of ascii symbols
170 # It is very strict that file contains less than 30% of ascii symbols
171 # in non Western Europe.
171 # in non Western Europe.
172 return true if Redmine::MimeType.is_type?('text', path)
172 return true if Redmine::MimeType.is_type?('text', path)
173 # Ruby 1.8.6 has a bug of integer divisions.
173 # Ruby 1.8.6 has a bug of integer divisions.
174 # http://apidock.com/ruby/v1_8_6_287/String/is_binary_data%3F
174 # http://apidock.com/ruby/v1_8_6_287/String/is_binary_data%3F
175 return false if ent.is_binary_data?
175 return false if ent.is_binary_data?
176 true
176 true
177 end
177 end
178 private :is_entry_text_data?
178 private :is_entry_text_data?
179
179
180 def annotate
180 def annotate
181 @entry = @repository.entry(@path, @rev)
181 @entry = @repository.entry(@path, @rev)
182 (show_error_not_found; return) unless @entry
182 (show_error_not_found; return) unless @entry
183
183
184 @annotate = @repository.scm.annotate(@path, @rev)
184 @annotate = @repository.scm.annotate(@path, @rev)
185 if @annotate.nil? || @annotate.empty?
185 if @annotate.nil? || @annotate.empty?
186 @annotate = nil
186 @annotate = nil
187 @error_message = l(:error_scm_annotate)
187 @error_message = l(:error_scm_annotate)
188 else
188 else
189 ann_buf_size = 0
189 ann_buf_size = 0
190 @annotate.lines.each do |buf|
190 @annotate.lines.each do |buf|
191 ann_buf_size += buf.size
191 ann_buf_size += buf.size
192 end
192 end
193 if ann_buf_size > Setting.file_max_size_displayed.to_i.kilobyte
193 if ann_buf_size > Setting.file_max_size_displayed.to_i.kilobyte
194 @annotate = nil
194 @annotate = nil
195 @error_message = l(:error_scm_annotate_big_text_file)
195 @error_message = l(:error_scm_annotate_big_text_file)
196 end
196 end
197 end
197 end
198 @changeset = @repository.find_changeset_by_name(@rev)
198 @changeset = @repository.find_changeset_by_name(@rev)
199 end
199 end
200
200
201 def revision
201 def revision
202 respond_to do |format|
202 respond_to do |format|
203 format.html
203 format.html
204 format.js {render :layout => false}
204 format.js {render :layout => false}
205 end
205 end
206 end
206 end
207
207
208 # Adds a related issue to a changeset
208 # Adds a related issue to a changeset
209 # POST /projects/:project_id/repository/(:repository_id/)revisions/:rev/issues
209 # POST /projects/:project_id/repository/(:repository_id/)revisions/:rev/issues
210 def add_related_issue
210 def add_related_issue
211 issue_id = params[:issue_id].to_s.sub(/^#/,'')
211 issue_id = params[:issue_id].to_s.sub(/^#/,'')
212 @issue = @changeset.find_referenced_issue_by_id(issue_id)
212 @issue = @changeset.find_referenced_issue_by_id(issue_id)
213 if @issue && (!@issue.visible? || @changeset.issues.include?(@issue))
213 if @issue && (!@issue.visible? || @changeset.issues.include?(@issue))
214 @issue = nil
214 @issue = nil
215 end
215 end
216
216
217 if @issue
217 if @issue
218 @changeset.issues << @issue
218 @changeset.issues << @issue
219 end
219 end
220 end
220 end
221
221
222 # Removes a related issue from a changeset
222 # Removes a related issue from a changeset
223 # DELETE /projects/:project_id/repository/(:repository_id/)revisions/:rev/issues/:issue_id
223 # DELETE /projects/:project_id/repository/(:repository_id/)revisions/:rev/issues/:issue_id
224 def remove_related_issue
224 def remove_related_issue
225 @issue = Issue.visible.find_by_id(params[:issue_id])
225 @issue = Issue.visible.find_by_id(params[:issue_id])
226 if @issue
226 if @issue
227 @changeset.issues.delete(@issue)
227 @changeset.issues.delete(@issue)
228 end
228 end
229 end
229 end
230
230
231 def diff
231 def diff
232 if params[:format] == 'diff'
232 if params[:format] == 'diff'
233 @diff = @repository.diff(@path, @rev, @rev_to)
233 @diff = @repository.diff(@path, @rev, @rev_to)
234 (show_error_not_found; return) unless @diff
234 (show_error_not_found; return) unless @diff
235 filename = "changeset_r#{@rev}"
235 filename = "changeset_r#{@rev}"
236 filename << "_r#{@rev_to}" if @rev_to
236 filename << "_r#{@rev_to}" if @rev_to
237 send_data @diff.join, :filename => "#{filename}.diff",
237 send_data @diff.join, :filename => "#{filename}.diff",
238 :type => 'text/x-patch',
238 :type => 'text/x-patch',
239 :disposition => 'attachment'
239 :disposition => 'attachment'
240 else
240 else
241 @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
241 @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
242 @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
242 @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
243
243
244 # Save diff type as user preference
244 # Save diff type as user preference
245 if User.current.logged? && @diff_type != User.current.pref[:diff_type]
245 if User.current.logged? && @diff_type != User.current.pref[:diff_type]
246 User.current.pref[:diff_type] = @diff_type
246 User.current.pref[:diff_type] = @diff_type
247 User.current.preference.save
247 User.current.preference.save
248 end
248 end
249 @cache_key = "repositories/diff/#{@repository.id}/" +
249 @cache_key = "repositories/diff/#{@repository.id}/" +
250 Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}-#{current_language}")
250 Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}-#{current_language}")
251 unless read_fragment(@cache_key)
251 unless read_fragment(@cache_key)
252 @diff = @repository.diff(@path, @rev, @rev_to)
252 @diff = @repository.diff(@path, @rev, @rev_to)
253 show_error_not_found unless @diff
253 show_error_not_found unless @diff
254 end
254 end
255
255
256 @changeset = @repository.find_changeset_by_name(@rev)
256 @changeset = @repository.find_changeset_by_name(@rev)
257 @changeset_to = @rev_to ? @repository.find_changeset_by_name(@rev_to) : nil
257 @changeset_to = @rev_to ? @repository.find_changeset_by_name(@rev_to) : nil
258 @diff_format_revisions = @repository.diff_format_revisions(@changeset, @changeset_to)
258 @diff_format_revisions = @repository.diff_format_revisions(@changeset, @changeset_to)
259 end
259 end
260 end
260 end
261
261
262 def stats
262 def stats
263 end
263 end
264
264
265 def graph
265 def graph
266 data = nil
266 data = nil
267 case params[:graph]
267 case params[:graph]
268 when "commits_per_month"
268 when "commits_per_month"
269 data = graph_commits_per_month(@repository)
269 data = graph_commits_per_month(@repository)
270 when "commits_per_author"
270 when "commits_per_author"
271 data = graph_commits_per_author(@repository)
271 data = graph_commits_per_author(@repository)
272 end
272 end
273 if data
273 if data
274 headers["Content-Type"] = "image/svg+xml"
274 headers["Content-Type"] = "image/svg+xml"
275 send_data(data, :type => "image/svg+xml", :disposition => "inline")
275 send_data(data, :type => "image/svg+xml", :disposition => "inline")
276 else
276 else
277 render_404
277 render_404
278 end
278 end
279 end
279 end
280
280
281 private
281 private
282
282
283 def build_new_repository_from_params
283 def build_new_repository_from_params
284 scm = params[:repository_scm] || (Redmine::Scm::Base.all & Setting.enabled_scm).first
284 scm = params[:repository_scm] || (Redmine::Scm::Base.all & Setting.enabled_scm).first
285 unless @repository = Repository.factory(scm)
285 unless @repository = Repository.factory(scm)
286 render_404
286 render_404
287 return
287 return
288 end
288 end
289
289
290 @repository.project = @project
290 @repository.project = @project
291 @repository.safe_attributes = params[:repository]
291 @repository.safe_attributes = params[:repository]
292 @repository
292 @repository
293 end
293 end
294
294
295 def find_repository
295 def find_repository
296 @repository = Repository.find(params[:id])
296 @repository = Repository.find(params[:id])
297 @project = @repository.project
297 @project = @repository.project
298 rescue ActiveRecord::RecordNotFound
298 rescue ActiveRecord::RecordNotFound
299 render_404
299 render_404
300 end
300 end
301
301
302 REV_PARAM_RE = %r{\A[a-f0-9]*\Z}i
302 REV_PARAM_RE = %r{\A[a-f0-9]*\Z}i
303
303
304 def find_project_repository
304 def find_project_repository
305 @project = Project.find(params[:id])
305 @project = Project.find(params[:id])
306 if params[:repository_id].present?
306 if params[:repository_id].present?
307 @repository = @project.repositories.find_by_identifier_param(params[:repository_id])
307 @repository = @project.repositories.find_by_identifier_param(params[:repository_id])
308 else
308 else
309 @repository = @project.repository
309 @repository = @project.repository
310 end
310 end
311 (render_404; return false) unless @repository
311 (render_404; return false) unless @repository
312 @path = params[:path].is_a?(Array) ? params[:path].join('/') : params[:path].to_s
312 @path = params[:path].is_a?(Array) ? params[:path].join('/') : params[:path].to_s
313 @rev = params[:rev].blank? ? @repository.default_branch : params[:rev].to_s.strip
313 @rev = params[:rev].blank? ? @repository.default_branch : params[:rev].to_s.strip
314 @rev_to = params[:rev_to]
314 @rev_to = params[:rev_to]
315
315
316 unless @rev.to_s.match(REV_PARAM_RE) && @rev_to.to_s.match(REV_PARAM_RE)
316 unless @rev.to_s.match(REV_PARAM_RE) && @rev_to.to_s.match(REV_PARAM_RE)
317 if @repository.branches.blank?
317 if @repository.branches.blank?
318 raise InvalidRevisionParam
318 raise InvalidRevisionParam
319 end
319 end
320 end
320 end
321 rescue ActiveRecord::RecordNotFound
321 rescue ActiveRecord::RecordNotFound
322 render_404
322 render_404
323 rescue InvalidRevisionParam
323 rescue InvalidRevisionParam
324 show_error_not_found
324 show_error_not_found
325 end
325 end
326
326
327 def find_changeset
327 def find_changeset
328 if @rev.present?
328 if @rev.present?
329 @changeset = @repository.find_changeset_by_name(@rev)
329 @changeset = @repository.find_changeset_by_name(@rev)
330 end
330 end
331 show_error_not_found unless @changeset
331 show_error_not_found unless @changeset
332 end
332 end
333
333
334 def show_error_not_found
334 def show_error_not_found
335 render_error :message => l(:error_scm_not_found), :status => 404
335 render_error :message => l(:error_scm_not_found), :status => 404
336 end
336 end
337
337
338 # Handler for Redmine::Scm::Adapters::CommandFailed exception
338 # Handler for Redmine::Scm::Adapters::CommandFailed exception
339 def show_error_command_failed(exception)
339 def show_error_command_failed(exception)
340 render_error l(:error_scm_command_failed, exception.message)
340 render_error l(:error_scm_command_failed, exception.message)
341 end
341 end
342
342
343 def graph_commits_per_month(repository)
343 def graph_commits_per_month(repository)
344 @date_to = User.current.today
344 @date_to = User.current.today
345 @date_from = @date_to << 11
345 @date_from = @date_to << 11
346 @date_from = Date.civil(@date_from.year, @date_from.month, 1)
346 @date_from = Date.civil(@date_from.year, @date_from.month, 1)
347 commits_by_day = Changeset.
347 commits_by_day = Changeset.
348 where("repository_id = ? AND commit_date BETWEEN ? AND ?", repository.id, @date_from, @date_to).
348 where("repository_id = ? AND commit_date BETWEEN ? AND ?", repository.id, @date_from, @date_to).
349 group(:commit_date).
349 group(:commit_date).
350 count
350 count
351 commits_by_month = [0] * 12
351 commits_by_month = [0] * 12
352 commits_by_day.each {|c| commits_by_month[(@date_to.month - c.first.to_date.month) % 12] += c.last }
352 commits_by_day.each {|c| commits_by_month[(@date_to.month - c.first.to_date.month) % 12] += c.last }
353
353
354 changes_by_day = Change.
354 changes_by_day = Change.
355 joins(:changeset).
355 joins(:changeset).
356 where("#{Changeset.table_name}.repository_id = ? AND #{Changeset.table_name}.commit_date BETWEEN ? AND ?", repository.id, @date_from, @date_to).
356 where("#{Changeset.table_name}.repository_id = ? AND #{Changeset.table_name}.commit_date BETWEEN ? AND ?", repository.id, @date_from, @date_to).
357 group(:commit_date).
357 group(:commit_date).
358 count
358 count
359 changes_by_month = [0] * 12
359 changes_by_month = [0] * 12
360 changes_by_day.each {|c| changes_by_month[(@date_to.month - c.first.to_date.month) % 12] += c.last }
360 changes_by_day.each {|c| changes_by_month[(@date_to.month - c.first.to_date.month) % 12] += c.last }
361
361
362 fields = []
362 fields = []
363 today = User.current.today
363 today = User.current.today
364 12.times {|m| fields << month_name(((today.month - 1 - m) % 12) + 1)}
364 12.times {|m| fields << month_name(((today.month - 1 - m) % 12) + 1)}
365
365
366 graph = SVG::Graph::Bar.new(
366 graph = SVG::Graph::Bar.new(
367 :height => 300,
367 :height => 300,
368 :width => 800,
368 :width => 800,
369 :fields => fields.reverse,
369 :fields => fields.reverse,
370 :stack => :side,
370 :stack => :side,
371 :scale_integers => true,
371 :scale_integers => true,
372 :step_x_labels => 2,
372 :step_x_labels => 2,
373 :show_data_values => false,
373 :show_data_values => false,
374 :graph_title => l(:label_commits_per_month),
374 :graph_title => l(:label_commits_per_month),
375 :show_graph_title => true
375 :show_graph_title => true
376 )
376 )
377
377
378 graph.add_data(
378 graph.add_data(
379 :data => commits_by_month[0..11].reverse,
379 :data => commits_by_month[0..11].reverse,
380 :title => l(:label_revision_plural)
380 :title => l(:label_revision_plural)
381 )
381 )
382
382
383 graph.add_data(
383 graph.add_data(
384 :data => changes_by_month[0..11].reverse,
384 :data => changes_by_month[0..11].reverse,
385 :title => l(:label_change_plural)
385 :title => l(:label_change_plural)
386 )
386 )
387
387
388 graph.burn
388 graph.burn
389 end
389 end
390
390
391 def graph_commits_per_author(repository)
391 def graph_commits_per_author(repository)
392 #data
392 #data
393 stats = repository.stats_by_author
393 stats = repository.stats_by_author
394 fields, commits_data, changes_data = [], [], []
394 fields, commits_data, changes_data = [], [], []
395 stats.each do |name, hsh|
395 stats.each do |name, hsh|
396 fields << name
396 fields << name
397 commits_data << hsh[:commits_count]
397 commits_data << hsh[:commits_count]
398 changes_data << hsh[:changes_count]
398 changes_data << hsh[:changes_count]
399 end
399 end
400
400
401 #expand to 10 values if needed
401 #expand to 10 values if needed
402 fields = fields + [""]*(10 - fields.length) if fields.length<10
402 fields = fields + [""]*(10 - fields.length) if fields.length<10
403 commits_data = commits_data + [0]*(10 - commits_data.length) if commits_data.length<10
403 commits_data = commits_data + [0]*(10 - commits_data.length) if commits_data.length<10
404 changes_data = changes_data + [0]*(10 - changes_data.length) if changes_data.length<10
404 changes_data = changes_data + [0]*(10 - changes_data.length) if changes_data.length<10
405
405
406 # Remove email address in usernames
406 # Remove email address in usernames
407 fields = fields.collect {|c| c.gsub(%r{<.+@.+>}, '') }
407 fields = fields.collect {|c| c.gsub(%r{<.+@.+>}, '') }
408
408
409 #prepare graph
409 #prepare graph
410 graph = SVG::Graph::BarHorizontal.new(
410 graph = SVG::Graph::BarHorizontal.new(
411 :height => 30 * commits_data.length,
411 :height => 30 * commits_data.length,
412 :width => 800,
412 :width => 800,
413 :fields => fields,
413 :fields => fields,
414 :stack => :side,
414 :stack => :side,
415 :scale_integers => true,
415 :scale_integers => true,
416 :show_data_values => false,
416 :show_data_values => false,
417 :rotate_y_labels => false,
417 :rotate_y_labels => false,
418 :graph_title => l(:label_commits_per_author),
418 :graph_title => l(:label_commits_per_author),
419 :show_graph_title => true
419 :show_graph_title => true
420 )
420 )
421 graph.add_data(
421 graph.add_data(
422 :data => commits_data,
422 :data => commits_data,
423 :title => l(:label_revision_plural)
423 :title => l(:label_revision_plural)
424 )
424 )
425 graph.add_data(
425 graph.add_data(
426 :data => changes_data,
426 :data => changes_data,
427 :title => l(:label_change_plural)
427 :title => l(:label_change_plural)
428 )
428 )
429 graph.burn
429 graph.burn
430 end
430 end
431
431
432 def disposition(path)
432 def disposition(path)
433 if Redmine::MimeType.is_type?('image', @path) || Redmine::MimeType.of(@path) == "application/pdf"
433 if Redmine::MimeType.of(@path) == "application/pdf"
434 'inline'
434 'inline'
435 else
435 else
436 'attachment'
436 'attachment'
437 end
437 end
438 end
438 end
439 end
439 end
General Comments 0
You need to be logged in to leave comments. Login now