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