##// END OF EJS Templates
Adds a macro for inserting thumbnails in formatted text (#3510)....
Jean-Philippe Lang -
r9830:537be80be26c
parent child
Show More
@@ -1,139 +1,139
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 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_filter :find_project, :except => :upload
20 20 before_filter :file_readable, :read_authorize, :only => [:show, :download, :thumbnail]
21 21 before_filter :delete_authorize, :only => :destroy
22 22 before_filter :authorize_global, :only => :upload
23 23
24 24 accept_api_auth :show, :download, :upload
25 25
26 26 def show
27 27 respond_to do |format|
28 28 format.html {
29 29 if @attachment.is_diff?
30 30 @diff = File.new(@attachment.diskfile, "rb").read
31 31 @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
32 32 @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
33 33 # Save diff type as user preference
34 34 if User.current.logged? && @diff_type != User.current.pref[:diff_type]
35 35 User.current.pref[:diff_type] = @diff_type
36 36 User.current.preference.save
37 37 end
38 38 render :action => 'diff'
39 39 elsif @attachment.is_text? && @attachment.filesize <= Setting.file_max_size_displayed.to_i.kilobyte
40 40 @content = File.new(@attachment.diskfile, "rb").read
41 41 render :action => 'file'
42 42 else
43 43 download
44 44 end
45 45 }
46 46 format.api
47 47 end
48 48 end
49 49
50 50 def download
51 51 if @attachment.container.is_a?(Version) || @attachment.container.is_a?(Project)
52 52 @attachment.increment_download
53 53 end
54 54
55 55 if stale?(:etag => @attachment.digest)
56 56 # images are sent inline
57 57 send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename),
58 58 :type => detect_content_type(@attachment),
59 59 :disposition => (@attachment.image? ? 'inline' : 'attachment')
60 60 end
61 61 end
62 62
63 63 def thumbnail
64 if @attachment.thumbnailable? && Setting.thumbnails_enabled? && thumbnail = @attachment.thumbnail
64 if @attachment.thumbnailable? && thumbnail = @attachment.thumbnail(:size => params[:size])
65 65 if stale?(:etag => thumbnail)
66 66 send_file thumbnail,
67 67 :filename => filename_for_content_disposition(@attachment.filename),
68 68 :type => detect_content_type(@attachment),
69 69 :disposition => 'inline'
70 70 end
71 71 else
72 72 # No thumbnail for the attachment or thumbnail could not be created
73 73 render :nothing => true, :status => 404
74 74 end
75 75 end
76 76
77 77 def upload
78 78 # Make sure that API users get used to set this content type
79 79 # as it won't trigger Rails' automatic parsing of the request body for parameters
80 80 unless request.content_type == 'application/octet-stream'
81 81 render :nothing => true, :status => 406
82 82 return
83 83 end
84 84
85 85 @attachment = Attachment.new(:file => request.raw_post)
86 86 @attachment.author = User.current
87 87 @attachment.filename = Redmine::Utils.random_hex(16)
88 88
89 89 if @attachment.save
90 90 respond_to do |format|
91 91 format.api { render :action => 'upload', :status => :created }
92 92 end
93 93 else
94 94 respond_to do |format|
95 95 format.api { render_validation_errors(@attachment) }
96 96 end
97 97 end
98 98 end
99 99
100 100 def destroy
101 101 if @attachment.container.respond_to?(:init_journal)
102 102 @attachment.container.init_journal(User.current)
103 103 end
104 104 # Make sure association callbacks are called
105 105 @attachment.container.attachments.delete(@attachment)
106 106 redirect_to_referer_or project_path(@project)
107 107 end
108 108
109 109 private
110 110 def find_project
111 111 @attachment = Attachment.find(params[:id])
112 112 # Show 404 if the filename in the url is wrong
113 113 raise ActiveRecord::RecordNotFound if params[:filename] && params[:filename] != @attachment.filename
114 114 @project = @attachment.project
115 115 rescue ActiveRecord::RecordNotFound
116 116 render_404
117 117 end
118 118
119 119 # Checks that the file exists and is readable
120 120 def file_readable
121 121 @attachment.readable? ? true : render_404
122 122 end
123 123
124 124 def read_authorize
125 125 @attachment.visible? ? true : deny_access
126 126 end
127 127
128 128 def delete_authorize
129 129 @attachment.deletable? ? true : deny_access
130 130 end
131 131
132 132 def detect_content_type(attachment)
133 133 content_type = attachment.content_type
134 134 if content_type.blank?
135 135 content_type = Redmine::MimeType.of(attachment.filename)
136 136 end
137 137 content_type.to_s
138 138 end
139 139 end
@@ -1,279 +1,287
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 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 "digest/md5"
19 19
20 20 class Attachment < ActiveRecord::Base
21 21 belongs_to :container, :polymorphic => true
22 22 belongs_to :author, :class_name => "User", :foreign_key => "author_id"
23 23
24 24 validates_presence_of :filename, :author
25 25 validates_length_of :filename, :maximum => 255
26 26 validates_length_of :disk_filename, :maximum => 255
27 27 validates_length_of :description, :maximum => 255
28 28 validate :validate_max_file_size
29 29
30 30 acts_as_event :title => :filename,
31 31 :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}}
32 32
33 33 acts_as_activity_provider :type => 'files',
34 34 :permission => :view_files,
35 35 :author_key => :author_id,
36 36 :find_options => {:select => "#{Attachment.table_name}.*",
37 37 :joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " +
38 38 "LEFT JOIN #{Project.table_name} ON #{Version.table_name}.project_id = #{Project.table_name}.id OR ( #{Attachment.table_name}.container_type='Project' AND #{Attachment.table_name}.container_id = #{Project.table_name}.id )"}
39 39
40 40 acts_as_activity_provider :type => 'documents',
41 41 :permission => :view_documents,
42 42 :author_key => :author_id,
43 43 :find_options => {:select => "#{Attachment.table_name}.*",
44 44 :joins => "LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " +
45 45 "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"}
46 46
47 47 cattr_accessor :storage_path
48 48 @@storage_path = Redmine::Configuration['attachments_storage_path'] || File.join(Rails.root, "files")
49 49
50 50 cattr_accessor :thumbnails_storage_path
51 51 @@thumbnails_storage_path = File.join(Rails.root, "tmp", "thumbnails")
52 52
53 53 before_save :files_to_final_location
54 54 after_destroy :delete_from_disk
55 55
56 56 # Returns an unsaved copy of the attachment
57 57 def copy(attributes=nil)
58 58 copy = self.class.new
59 59 copy.attributes = self.attributes.dup.except("id", "downloads")
60 60 copy.attributes = attributes if attributes
61 61 copy
62 62 end
63 63
64 64 def validate_max_file_size
65 65 if @temp_file && self.filesize > Setting.attachment_max_size.to_i.kilobytes
66 66 errors.add(:base, l(:error_attachment_too_big, :max_size => Setting.attachment_max_size.to_i.kilobytes))
67 67 end
68 68 end
69 69
70 70 def file=(incoming_file)
71 71 unless incoming_file.nil?
72 72 @temp_file = incoming_file
73 73 if @temp_file.size > 0
74 74 if @temp_file.respond_to?(:original_filename)
75 75 self.filename = @temp_file.original_filename
76 76 self.filename.force_encoding("UTF-8") if filename.respond_to?(:force_encoding)
77 77 end
78 78 if @temp_file.respond_to?(:content_type)
79 79 self.content_type = @temp_file.content_type.to_s.chomp
80 80 end
81 81 if content_type.blank? && filename.present?
82 82 self.content_type = Redmine::MimeType.of(filename)
83 83 end
84 84 self.filesize = @temp_file.size
85 85 end
86 86 end
87 87 end
88 88
89 89 def file
90 90 nil
91 91 end
92 92
93 93 def filename=(arg)
94 94 write_attribute :filename, sanitize_filename(arg.to_s)
95 95 if new_record? && disk_filename.blank?
96 96 self.disk_filename = Attachment.disk_filename(filename)
97 97 end
98 98 filename
99 99 end
100 100
101 101 # Copies the temporary file to its final location
102 102 # and computes its MD5 hash
103 103 def files_to_final_location
104 104 if @temp_file && (@temp_file.size > 0)
105 105 logger.info("Saving attachment '#{self.diskfile}' (#{@temp_file.size} bytes)")
106 106 md5 = Digest::MD5.new
107 107 File.open(diskfile, "wb") do |f|
108 108 if @temp_file.respond_to?(:read)
109 109 buffer = ""
110 110 while (buffer = @temp_file.read(8192))
111 111 f.write(buffer)
112 112 md5.update(buffer)
113 113 end
114 114 else
115 115 f.write(@temp_file)
116 116 md5.update(@temp_file)
117 117 end
118 118 end
119 119 self.digest = md5.hexdigest
120 120 end
121 121 @temp_file = nil
122 122 # Don't save the content type if it's longer than the authorized length
123 123 if self.content_type && self.content_type.length > 255
124 124 self.content_type = nil
125 125 end
126 126 end
127 127
128 128 # Deletes the file from the file system if it's not referenced by other attachments
129 129 def delete_from_disk
130 130 if Attachment.where("disk_filename = ? AND id <> ?", disk_filename, id).empty?
131 131 delete_from_disk!
132 132 end
133 133 end
134 134
135 135 # Returns file's location on disk
136 136 def diskfile
137 137 File.join(self.class.storage_path, disk_filename.to_s)
138 138 end
139 139
140 140 def title
141 141 title = filename.to_s
142 142 if description.present?
143 143 title << " (#{description})"
144 144 end
145 145 title
146 146 end
147 147
148 148 def increment_download
149 149 increment!(:downloads)
150 150 end
151 151
152 152 def project
153 153 container.try(:project)
154 154 end
155 155
156 156 def visible?(user=User.current)
157 157 container && container.attachments_visible?(user)
158 158 end
159 159
160 160 def deletable?(user=User.current)
161 161 container && container.attachments_deletable?(user)
162 162 end
163 163
164 164 def image?
165 165 !!(self.filename =~ /\.(bmp|gif|jpg|jpe|jpeg|png)$/i)
166 166 end
167 167
168 168 def thumbnailable?
169 169 image?
170 170 end
171 171
172 172 # Returns the full path the attachment thumbnail, or nil
173 173 # if the thumbnail cannot be generated.
174 def thumbnail
174 def thumbnail(options={})
175 175 if thumbnailable? && readable?
176 size = Setting.thumbnails_size.to_i
176 size = options[:size].to_i
177 if size > 0
178 # Limit the number of thumbnails per image
179 size = (size / 50) * 50
180 # Maximum thumbnail size
181 size = 800 if size > 800
182 else
183 size = Setting.thumbnails_size.to_i
184 end
177 185 size = 100 unless size > 0
178 186 target = File.join(self.class.thumbnails_storage_path, "#{id}_#{digest}_#{size}.thumb")
179 187
180 188 begin
181 189 Redmine::Thumbnail.generate(self.diskfile, target, size)
182 190 rescue => e
183 191 logger.error "An error occured while generating thumbnail for #{disk_filename} to #{target}\nException was: #{e.message}" if logger
184 192 return nil
185 193 end
186 194 end
187 195 end
188 196
189 197 # Deletes all thumbnails
190 198 def self.clear_thumbnails
191 199 Dir.glob(File.join(thumbnails_storage_path, "*.thumb")).each do |file|
192 200 File.delete file
193 201 end
194 202 end
195 203
196 204 def is_text?
197 205 Redmine::MimeType.is_type?('text', filename)
198 206 end
199 207
200 208 def is_diff?
201 209 self.filename =~ /\.(patch|diff)$/i
202 210 end
203 211
204 212 # Returns true if the file is readable
205 213 def readable?
206 214 File.readable?(diskfile)
207 215 end
208 216
209 217 # Returns the attachment token
210 218 def token
211 219 "#{id}.#{digest}"
212 220 end
213 221
214 222 # Finds an attachment that matches the given token and that has no container
215 223 def self.find_by_token(token)
216 224 if token.to_s =~ /^(\d+)\.([0-9a-f]+)$/
217 225 attachment_id, attachment_digest = $1, $2
218 226 attachment = Attachment.where(:id => attachment_id, :digest => attachment_digest).first
219 227 if attachment && attachment.container.nil?
220 228 attachment
221 229 end
222 230 end
223 231 end
224 232
225 233 # Bulk attaches a set of files to an object
226 234 #
227 235 # Returns a Hash of the results:
228 236 # :files => array of the attached files
229 237 # :unsaved => array of the files that could not be attached
230 238 def self.attach_files(obj, attachments)
231 239 result = obj.save_attachments(attachments, User.current)
232 240 obj.attach_saved_attachments
233 241 result
234 242 end
235 243
236 244 def self.latest_attach(attachments, filename)
237 245 attachments.sort_by(&:created_on).reverse.detect {
238 246 |att| att.filename.downcase == filename.downcase
239 247 }
240 248 end
241 249
242 250 def self.prune(age=1.day)
243 251 Attachment.where("created_on < ? AND (container_type IS NULL OR container_type = '')", Time.now - age).destroy_all
244 252 end
245 253
246 254 private
247 255
248 256 # Physically deletes the file from the file system
249 257 def delete_from_disk!
250 258 if disk_filename.present? && File.exist?(diskfile)
251 259 File.delete(diskfile)
252 260 end
253 261 end
254 262
255 263 def sanitize_filename(value)
256 264 # get only the filename, not the whole path
257 265 just_filename = value.gsub(/^.*(\\|\/)/, '')
258 266
259 267 # Finally, replace invalid characters with underscore
260 268 @filename = just_filename.gsub(/[\/\?\%\*\:\|\"\'<>]+/, '_')
261 269 end
262 270
263 271 # Returns an ASCII or hashed filename
264 272 def self.disk_filename(filename)
265 273 timestamp = DateTime.now.strftime("%y%m%d%H%M%S")
266 274 ascii = ''
267 275 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
268 276 ascii = filename
269 277 else
270 278 ascii = Digest::MD5.hexdigest(filename)
271 279 # keep the extension if any
272 280 ascii << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
273 281 end
274 282 while File.exist?(File.join(@@storage_path, "#{timestamp}_#{ascii}"))
275 283 timestamp.succ!
276 284 end
277 285 "#{timestamp}_#{ascii}"
278 286 end
279 287 end
@@ -1,340 +1,340
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 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 RedmineApp::Application.routes.draw do
19 19 root :to => 'welcome#index', :as => 'home'
20 20
21 21 match 'login', :to => 'account#login', :as => 'signin'
22 22 match 'logout', :to => 'account#logout', :as => 'signout'
23 23 match 'account/register', :to => 'account#register', :via => [:get, :post], :as => 'register'
24 24 match 'account/lost_password', :to => 'account#lost_password', :via => [:get, :post], :as => 'lost_password'
25 25 match 'account/activate', :to => 'account#activate', :via => :get
26 26
27 27 match '/news/preview', :controller => 'previews', :action => 'news', :as => 'preview_news'
28 28 match '/issues/preview/new/:project_id', :to => 'previews#issue', :as => 'preview_new_issue'
29 29 match '/issues/preview/edit/:id', :to => 'previews#issue', :as => 'preview_edit_issue'
30 30 match '/issues/preview', :to => 'previews#issue', :as => 'preview_issue'
31 31
32 32 match 'projects/:id/wiki', :to => 'wikis#edit', :via => :post
33 33 match 'projects/:id/wiki/destroy', :to => 'wikis#destroy', :via => [:get, :post]
34 34
35 35 match 'boards/:board_id/topics/new', :to => 'messages#new', :via => [:get, :post]
36 36 get 'boards/:board_id/topics/:id', :to => 'messages#show', :as => 'board_message'
37 37 match 'boards/:board_id/topics/quote/:id', :to => 'messages#quote', :via => [:get, :post]
38 38 get 'boards/:board_id/topics/:id/edit', :to => 'messages#edit'
39 39
40 40 post 'boards/:board_id/topics/preview', :to => 'messages#preview'
41 41 post 'boards/:board_id/topics/:id/replies', :to => 'messages#reply'
42 42 post 'boards/:board_id/topics/:id/edit', :to => 'messages#edit'
43 43 post 'boards/:board_id/topics/:id/destroy', :to => 'messages#destroy'
44 44
45 45 # Misc issue routes. TODO: move into resources
46 46 match '/issues/auto_complete', :to => 'auto_completes#issues', :via => :get, :as => 'auto_complete_issues'
47 47 match '/issues/context_menu', :to => 'context_menus#issues', :as => 'issues_context_menu'
48 48 match '/issues/changes', :to => 'journals#index', :as => 'issue_changes'
49 49 match '/issues/:id/quoted', :to => 'journals#new', :id => /\d+/, :via => :post, :as => 'quoted_issue'
50 50
51 51 match '/journals/diff/:id', :to => 'journals#diff', :id => /\d+/, :via => :get
52 52 match '/journals/edit/:id', :to => 'journals#edit', :id => /\d+/, :via => [:get, :post]
53 53
54 54 match '/projects/:project_id/issues/gantt', :to => 'gantts#show'
55 55 match '/issues/gantt', :to => 'gantts#show'
56 56
57 57 match '/projects/:project_id/issues/calendar', :to => 'calendars#show'
58 58 match '/issues/calendar', :to => 'calendars#show'
59 59
60 60 match 'projects/:id/issues/report', :to => 'reports#issue_report', :via => :get
61 61 match 'projects/:id/issues/report/:detail', :to => 'reports#issue_report_details', :via => :get
62 62
63 63 match 'my/account', :controller => 'my', :action => 'account', :via => [:get, :post]
64 64 match 'my/account/destroy', :controller => 'my', :action => 'destroy', :via => [:get, :post]
65 65 match 'my/page', :controller => 'my', :action => 'page', :via => :get
66 66 match 'my', :controller => 'my', :action => 'index', :via => :get # Redirects to my/page
67 67 match 'my/reset_rss_key', :controller => 'my', :action => 'reset_rss_key', :via => :post
68 68 match 'my/reset_api_key', :controller => 'my', :action => 'reset_api_key', :via => :post
69 69 match 'my/password', :controller => 'my', :action => 'password', :via => [:get, :post]
70 70 match 'my/page_layout', :controller => 'my', :action => 'page_layout', :via => :get
71 71 match 'my/add_block', :controller => 'my', :action => 'add_block', :via => :post
72 72 match 'my/remove_block', :controller => 'my', :action => 'remove_block', :via => :post
73 73 match 'my/order_blocks', :controller => 'my', :action => 'order_blocks', :via => :post
74 74
75 75 resources :users
76 76 match 'users/:id/memberships/:membership_id', :to => 'users#edit_membership', :via => :put, :as => 'user_membership'
77 77 match 'users/:id/memberships/:membership_id', :to => 'users#destroy_membership', :via => :delete
78 78 match 'users/:id/memberships', :to => 'users#edit_membership', :via => :post, :as => 'user_memberships'
79 79
80 80 match 'watchers/new', :controller=> 'watchers', :action => 'new', :via => :get
81 81 match 'watchers', :controller=> 'watchers', :action => 'create', :via => :post
82 82 match 'watchers/append', :controller=> 'watchers', :action => 'append', :via => :post
83 83 match 'watchers/destroy', :controller=> 'watchers', :action => 'destroy', :via => :post
84 84 match 'watchers/watch', :controller=> 'watchers', :action => 'watch', :via => :post
85 85 match 'watchers/unwatch', :controller=> 'watchers', :action => 'unwatch', :via => :post
86 86 match 'watchers/autocomplete_for_user', :controller=> 'watchers', :action => 'autocomplete_for_user', :via => :get
87 87
88 88 match 'projects/:id/settings/:tab', :to => "projects#settings"
89 89
90 90 resources :projects do
91 91 member do
92 92 get 'settings'
93 93 post 'modules'
94 94 post 'archive'
95 95 post 'unarchive'
96 96 post 'close'
97 97 post 'reopen'
98 98 match 'copy', :via => [:get, :post]
99 99 end
100 100
101 101 resources :memberships, :shallow => true, :controller => 'members', :only => [:index, :show, :create, :update, :destroy] do
102 102 collection do
103 103 get 'autocomplete'
104 104 end
105 105 end
106 106
107 107 resource :enumerations, :controller => 'project_enumerations', :only => [:update, :destroy]
108 108
109 109 match 'issues/:copy_from/copy', :to => 'issues#new'
110 110 resources :issues, :only => [:index, :new, :create] do
111 111 resources :time_entries, :controller => 'timelog' do
112 112 collection do
113 113 get 'report'
114 114 end
115 115 end
116 116 end
117 117 # issue form update
118 118 match 'issues/new', :controller => 'issues', :action => 'new', :via => [:put, :post], :as => 'issue_form'
119 119
120 120 resources :files, :only => [:index, :new, :create]
121 121
122 122 resources :versions, :except => [:index, :show, :edit, :update, :destroy] do
123 123 collection do
124 124 put 'close_completed'
125 125 end
126 126 end
127 127 match 'versions.:format', :to => 'versions#index'
128 128 match 'roadmap', :to => 'versions#index', :format => false
129 129 match 'versions', :to => 'versions#index'
130 130
131 131 resources :news, :except => [:show, :edit, :update, :destroy]
132 132 resources :time_entries, :controller => 'timelog' do
133 133 get 'report', :on => :collection
134 134 end
135 135 resources :queries, :only => [:new, :create]
136 136 resources :issue_categories, :shallow => true
137 137 resources :documents, :except => [:show, :edit, :update, :destroy]
138 138 resources :boards
139 139 resources :repositories, :shallow => true, :except => [:index, :show] do
140 140 member do
141 141 match 'committers', :via => [:get, :post]
142 142 end
143 143 end
144 144
145 145 match 'wiki/index', :controller => 'wiki', :action => 'index', :via => :get
146 146 match 'wiki/:id/diff/:version/vs/:version_from', :controller => 'wiki', :action => 'diff'
147 147 match 'wiki/:id/diff/:version', :controller => 'wiki', :action => 'diff'
148 148 resources :wiki, :except => [:index, :new, :create] do
149 149 member do
150 150 get 'rename'
151 151 post 'rename'
152 152 get 'history'
153 153 get 'diff'
154 154 match 'preview', :via => [:post, :put]
155 155 post 'protect'
156 156 post 'add_attachment'
157 157 end
158 158 collection do
159 159 get 'export'
160 160 get 'date_index'
161 161 end
162 162 end
163 163 match 'wiki', :controller => 'wiki', :action => 'show', :via => :get
164 164 match 'wiki/:id/annotate/:version', :controller => 'wiki', :action => 'annotate'
165 165 end
166 166
167 167 resources :issues do
168 168 collection do
169 169 match 'bulk_edit', :via => [:get, :post]
170 170 post 'bulk_update'
171 171 end
172 172 resources :time_entries, :controller => 'timelog' do
173 173 collection do
174 174 get 'report'
175 175 end
176 176 end
177 177 resources :relations, :shallow => true, :controller => 'issue_relations', :only => [:index, :show, :create, :destroy]
178 178 end
179 179 match '/issues', :controller => 'issues', :action => 'destroy', :via => :delete
180 180
181 181 resources :queries, :except => [:show]
182 182
183 183 resources :news, :only => [:index, :show, :edit, :update, :destroy]
184 184 match '/news/:id/comments', :to => 'comments#create', :via => :post
185 185 match '/news/:id/comments/:comment_id', :to => 'comments#destroy', :via => :delete
186 186
187 187 resources :versions, :only => [:show, :edit, :update, :destroy] do
188 188 post 'status_by', :on => :member
189 189 end
190 190
191 191 resources :documents, :only => [:show, :edit, :update, :destroy] do
192 192 post 'add_attachment', :on => :member
193 193 end
194 194
195 195 match '/time_entries/context_menu', :to => 'context_menus#time_entries', :as => :time_entries_context_menu
196 196
197 197 resources :time_entries, :controller => 'timelog', :except => :destroy do
198 198 collection do
199 199 get 'report'
200 200 get 'bulk_edit'
201 201 post 'bulk_update'
202 202 end
203 203 end
204 204 match '/time_entries/:id', :to => 'timelog#destroy', :via => :delete, :id => /\d+/
205 205 # TODO: delete /time_entries for bulk deletion
206 206 match '/time_entries/destroy', :to => 'timelog#destroy', :via => :delete
207 207
208 208 # TODO: port to be part of the resources route(s)
209 209 match 'projects/:id/settings/:tab', :to => 'projects#settings', :via => :get
210 210
211 211 get 'projects/:id/activity', :to => 'activities#index'
212 212 get 'projects/:id/activity.:format', :to => 'activities#index'
213 213 get 'activity', :to => 'activities#index'
214 214
215 215 # repositories routes
216 216 get 'projects/:id/repository/:repository_id/statistics', :to => 'repositories#stats'
217 217 get 'projects/:id/repository/:repository_id/graph', :to => 'repositories#graph'
218 218
219 219 get 'projects/:id/repository/:repository_id/changes(/*path(.:ext))',
220 220 :to => 'repositories#changes'
221 221
222 222 get 'projects/:id/repository/:repository_id/revisions/:rev', :to => 'repositories#revision'
223 223 get 'projects/:id/repository/:repository_id/revision', :to => 'repositories#revision'
224 224 post 'projects/:id/repository/:repository_id/revisions/:rev/issues', :to => 'repositories#add_related_issue'
225 225 delete 'projects/:id/repository/:repository_id/revisions/:rev/issues/:issue_id', :to => 'repositories#remove_related_issue'
226 226 get 'projects/:id/repository/:repository_id/revisions', :to => 'repositories#revisions'
227 227 get 'projects/:id/repository/:repository_id/revisions/:rev/:action(/*path(.:ext))',
228 228 :controller => 'repositories',
229 229 :format => false,
230 230 :constraints => {
231 231 :action => /(browse|show|entry|raw|annotate|diff)/,
232 232 :rev => /[a-z0-9\.\-_]+/
233 233 }
234 234
235 235 get 'projects/:id/repository/statistics', :to => 'repositories#stats'
236 236 get 'projects/:id/repository/graph', :to => 'repositories#graph'
237 237
238 238 get 'projects/:id/repository/changes(/*path(.:ext))',
239 239 :to => 'repositories#changes'
240 240
241 241 get 'projects/:id/repository/revisions', :to => 'repositories#revisions'
242 242 get 'projects/:id/repository/revisions/:rev', :to => 'repositories#revision'
243 243 get 'projects/:id/repository/revision', :to => 'repositories#revision'
244 244 post 'projects/:id/repository/revisions/:rev/issues', :to => 'repositories#add_related_issue'
245 245 delete 'projects/:id/repository/revisions/:rev/issues/:issue_id', :to => 'repositories#remove_related_issue'
246 246 get 'projects/:id/repository/revisions/:rev/:action(/*path(.:ext))',
247 247 :controller => 'repositories',
248 248 :format => false,
249 249 :constraints => {
250 250 :action => /(browse|show|entry|raw|annotate|diff)/,
251 251 :rev => /[a-z0-9\.\-_]+/
252 252 }
253 253 get 'projects/:id/repository/:repository_id/:action(/*path(.:ext))',
254 254 :controller => 'repositories',
255 255 :action => /(browse|show|entry|raw|changes|annotate|diff)/
256 256 get 'projects/:id/repository/:action(/*path(.:ext))',
257 257 :controller => 'repositories',
258 258 :action => /(browse|show|entry|raw|changes|annotate|diff)/
259 259
260 260 get 'projects/:id/repository/:repository_id', :to => 'repositories#show', :path => nil
261 261 get 'projects/:id/repository', :to => 'repositories#show', :path => nil
262 262
263 263 # additional routes for having the file name at the end of url
264 264 match 'attachments/:id/:filename', :controller => 'attachments', :action => 'show', :id => /\d+/, :filename => /.*/, :via => :get
265 265 match 'attachments/download/:id/:filename', :controller => 'attachments', :action => 'download', :id => /\d+/, :filename => /.*/, :via => :get
266 266 match 'attachments/download/:id', :controller => 'attachments', :action => 'download', :id => /\d+/, :via => :get
267 match 'attachments/thumbnail/:id', :controller => 'attachments', :action => 'thumbnail', :id => /\d+/, :via => :get
267 match 'attachments/thumbnail/:id(/:size)', :controller => 'attachments', :action => 'thumbnail', :id => /\d+/, :via => :get, :size => /\d+/
268 268 resources :attachments, :only => [:show, :destroy]
269 269
270 270 resources :groups do
271 271 member do
272 272 get 'autocomplete_for_user'
273 273 end
274 274 end
275 275
276 276 match 'groups/:id/users', :controller => 'groups', :action => 'add_users', :id => /\d+/, :via => :post, :as => 'group_users'
277 277 match 'groups/:id/users/:user_id', :controller => 'groups', :action => 'remove_user', :id => /\d+/, :via => :delete, :as => 'group_user'
278 278 match 'groups/destroy_membership/:id', :controller => 'groups', :action => 'destroy_membership', :id => /\d+/, :via => :post
279 279 match 'groups/edit_membership/:id', :controller => 'groups', :action => 'edit_membership', :id => /\d+/, :via => :post
280 280
281 281 resources :trackers, :except => :show
282 282 resources :issue_statuses, :except => :show do
283 283 collection do
284 284 post 'update_issue_done_ratio'
285 285 end
286 286 end
287 287 resources :custom_fields, :except => :show
288 288 resources :roles, :except => :show do
289 289 collection do
290 290 match 'permissions', :via => [:get, :post]
291 291 end
292 292 end
293 293 resources :enumerations, :except => :show
294 294
295 295 get 'projects/:id/search', :controller => 'search', :action => 'index'
296 296 get 'search', :controller => 'search', :action => 'index'
297 297
298 298 match 'mail_handler', :controller => 'mail_handler', :action => 'index', :via => :post
299 299
300 300 match 'admin', :controller => 'admin', :action => 'index', :via => :get
301 301 match 'admin/projects', :controller => 'admin', :action => 'projects', :via => :get
302 302 match 'admin/plugins', :controller => 'admin', :action => 'plugins', :via => :get
303 303 match 'admin/info', :controller => 'admin', :action => 'info', :via => :get
304 304 match 'admin/test_email', :controller => 'admin', :action => 'test_email', :via => :get
305 305 match 'admin/default_configuration', :controller => 'admin', :action => 'default_configuration', :via => :post
306 306
307 307 resources :auth_sources do
308 308 member do
309 309 get 'test_connection'
310 310 end
311 311 end
312 312
313 313 match 'workflows', :controller => 'workflows', :action => 'index', :via => :get
314 314 match 'workflows/edit', :controller => 'workflows', :action => 'edit', :via => [:get, :post]
315 315 match 'workflows/permissions', :controller => 'workflows', :action => 'permissions', :via => [:get, :post]
316 316 match 'workflows/copy', :controller => 'workflows', :action => 'copy', :via => [:get, :post]
317 317 match 'settings', :controller => 'settings', :action => 'index', :via => :get
318 318 match 'settings/edit', :controller => 'settings', :action => 'edit', :via => [:get, :post]
319 319 match 'settings/plugin/:id', :controller => 'settings', :action => 'plugin', :via => [:get, :post]
320 320
321 321 match 'sys/projects', :to => 'sys#projects', :via => :get
322 322 match 'sys/projects/:id/repository', :to => 'sys#create_project_repository', :via => :post
323 323 match 'sys/fetch_changesets', :to => 'sys#fetch_changesets', :via => :get
324 324
325 325 match 'uploads', :to => 'attachments#upload', :via => :post
326 326
327 327 get 'robots.txt', :to => 'welcome#robots'
328 328
329 329 Dir.glob File.expand_path("plugins/*", Rails.root) do |plugin_dir|
330 330 file = File.join(plugin_dir, "config/routes.rb")
331 331 if File.exists?(file)
332 332 begin
333 333 instance_eval File.read(file)
334 334 rescue Exception => e
335 335 puts "An error occurred while loading the routes definition of #{File.basename(plugin_dir)} plugin (#{file}): #{e.message}."
336 336 exit 1
337 337 end
338 338 end
339 339 end
340 340 end
@@ -1,121 +1,139
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 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 module Redmine
19 19 module WikiFormatting
20 20 module Macros
21 21 module Definitions
22 22 def exec_macro(name, obj, args)
23 23 method_name = "macro_#{name}"
24 24 send(method_name, obj, args) if respond_to?(method_name)
25 25 end
26 26
27 27 def extract_macro_options(args, *keys)
28 28 options = {}
29 29 while args.last.to_s.strip =~ %r{^(.+)\=(.+)$} && keys.include?($1.downcase.to_sym)
30 30 options[$1.downcase.to_sym] = $2
31 31 args.pop
32 32 end
33 33 return [args, options]
34 34 end
35 35 end
36 36
37 37 @@available_macros = {}
38 38
39 39 class << self
40 40 # Called with a block to define additional macros.
41 41 # Macro blocks accept 2 arguments:
42 42 # * obj: the object that is rendered
43 43 # * args: macro arguments
44 44 #
45 45 # Plugins can use this method to define new macros:
46 46 #
47 47 # Redmine::WikiFormatting::Macros.register do
48 48 # desc "This is my macro"
49 49 # macro :my_macro do |obj, args|
50 50 # "My macro output"
51 51 # end
52 52 # end
53 53 def register(&block)
54 54 class_eval(&block) if block_given?
55 55 end
56 56
57 57 private
58 58 # Defines a new macro with the given name and block.
59 59 def macro(name, &block)
60 60 name = name.to_sym if name.is_a?(String)
61 61 @@available_macros[name] = @@desc || ''
62 62 @@desc = nil
63 63 raise "Can not create a macro without a block!" unless block_given?
64 64 Definitions.send :define_method, "macro_#{name}".downcase, &block
65 65 end
66 66
67 67 # Sets description for the next macro to be defined
68 68 def desc(txt)
69 69 @@desc = txt
70 70 end
71 71 end
72 72
73 73 # Builtin macros
74 74 desc "Sample macro."
75 75 macro :hello_world do |obj, args|
76 76 "Hello world! Object: #{obj.class.name}, " + (args.empty? ? "Called with no argument." : "Arguments: #{args.join(', ')}")
77 77 end
78 78
79 79 desc "Displays a list of all available macros, including description if available."
80 80 macro :macro_list do |obj, args|
81 81 out = ''.html_safe
82 82 @@available_macros.keys.collect(&:to_s).sort.each do |macro|
83 83 out << content_tag('dt', content_tag('code', macro))
84 84 out << content_tag('dd', textilizable(@@available_macros[macro.to_sym]))
85 85 end
86 86 content_tag('dl', out)
87 87 end
88 88
89 89 desc "Displays a list of child pages. With no argument, it displays the child pages of the current wiki page. Examples:\n\n" +
90 90 " !{{child_pages}} -- can be used from a wiki page only\n" +
91 91 " !{{child_pages(Foo)}} -- lists all children of page Foo\n" +
92 92 " !{{child_pages(Foo, parent=1)}} -- same as above with a link to page Foo"
93 93 macro :child_pages do |obj, args|
94 94 args, options = extract_macro_options(args, :parent)
95 95 page = nil
96 96 if args.size > 0
97 97 page = Wiki.find_page(args.first.to_s, :project => @project)
98 98 elsif obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)
99 99 page = obj.page
100 100 else
101 101 raise 'With no argument, this macro can be called from wiki pages only.'
102 102 end
103 103 raise 'Page not found' if page.nil? || !User.current.allowed_to?(:view_wiki_pages, page.wiki.project)
104 104 pages = ([page] + page.descendants).group_by(&:parent_id)
105 105 render_page_hierarchy(pages, options[:parent] ? page.parent_id : page.id)
106 106 end
107 107
108 108 desc "Include a wiki page. Example:\n\n !{{include(Foo)}}\n\nor to include a page of a specific project wiki:\n\n !{{include(projectname:Foo)}}"
109 109 macro :include do |obj, args|
110 110 page = Wiki.find_page(args.first.to_s, :project => @project)
111 111 raise 'Page not found' if page.nil? || !User.current.allowed_to?(:view_wiki_pages, page.wiki.project)
112 112 @included_wiki_pages ||= []
113 113 raise 'Circular inclusion detected' if @included_wiki_pages.include?(page.title)
114 114 @included_wiki_pages << page.title
115 115 out = textilizable(page.content, :text, :attachments => page.attachments, :headings => false)
116 116 @included_wiki_pages.pop
117 117 out
118 118 end
119
120 desc "Displays a clickable thumbnail of an attached image. Examples:\n\n<pre>{{thumbnail(image.png)}}\n{{thumbnail(image.png, size=300, title=Thumbnail)}}</pre>"
121 macro :thumbnail do |obj, args|
122 args, options = extract_macro_options(args, :size, :title)
123 filename = args.first
124 raise 'Filename required' unless filename.present?
125 size = options[:size]
126 raise 'Invalid size parameter' unless size.nil? || size.match(/^\d+$/)
127 size = size.to_i
128 size = nil unless size > 0
129 if obj && obj.respond_to?(:attachments) && attachment = Attachment.latest_attach(obj.attachments, filename)
130 title = options[:title] || attachment.title
131 img = image_tag(url_for(:controller => 'attachments', :action => 'thumbnail', :id => attachment, :size => size), :alt => attachment.filename)
132 link_to(img, url_for(:controller => 'attachments', :action => 'show', :id => attachment), :class => 'thumbnail', :title => title)
133 else
134 raise "Attachment #{filename} not found"
135 end
136 end
119 137 end
120 138 end
121 139 end
@@ -1,362 +1,363
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2012 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 require File.expand_path('../../test_helper', __FILE__)
21 21 require 'attachments_controller'
22 22
23 23 # Re-raise errors caught by the controller.
24 24 class AttachmentsController; def rescue_action(e) raise e end; end
25 25
26 26 class AttachmentsControllerTest < ActionController::TestCase
27 27 fixtures :users, :projects, :roles, :members, :member_roles,
28 28 :enabled_modules, :issues, :trackers, :attachments,
29 29 :versions, :wiki_pages, :wikis, :documents
30 30
31 31 def setup
32 32 @controller = AttachmentsController.new
33 33 @request = ActionController::TestRequest.new
34 34 @response = ActionController::TestResponse.new
35 35 User.current = nil
36 36 set_fixtures_attachments_directory
37 37 end
38 38
39 39 def teardown
40 40 set_tmp_attachments_directory
41 41 end
42 42
43 43 def test_show_diff
44 44 ['inline', 'sbs'].each do |dt|
45 45 # 060719210727_changeset_utf8.diff
46 46 get :show, :id => 14, :type => dt
47 47 assert_response :success
48 48 assert_template 'diff'
49 49 assert_equal 'text/html', @response.content_type
50 50 assert_tag 'th',
51 51 :attributes => {:class => /filename/},
52 52 :content => /issues_controller.rb\t\(rΓ©vision 1484\)/
53 53 assert_tag 'td',
54 54 :attributes => {:class => /line-code/},
55 55 :content => /Demande créée avec succès/
56 56 end
57 57 set_tmp_attachments_directory
58 58 end
59 59
60 60 def test_show_diff_replcace_cannot_convert_content
61 61 with_settings :repositories_encodings => 'UTF-8' do
62 62 ['inline', 'sbs'].each do |dt|
63 63 # 060719210727_changeset_iso8859-1.diff
64 64 get :show, :id => 5, :type => dt
65 65 assert_response :success
66 66 assert_template 'diff'
67 67 assert_equal 'text/html', @response.content_type
68 68 assert_tag 'th',
69 69 :attributes => {:class => "filename"},
70 70 :content => /issues_controller.rb\t\(r\?vision 1484\)/
71 71 assert_tag 'td',
72 72 :attributes => {:class => /line-code/},
73 73 :content => /Demande cr\?\?e avec succ\?s/
74 74 end
75 75 end
76 76 set_tmp_attachments_directory
77 77 end
78 78
79 79 def test_show_diff_latin_1
80 80 with_settings :repositories_encodings => 'UTF-8,ISO-8859-1' do
81 81 ['inline', 'sbs'].each do |dt|
82 82 # 060719210727_changeset_iso8859-1.diff
83 83 get :show, :id => 5, :type => dt
84 84 assert_response :success
85 85 assert_template 'diff'
86 86 assert_equal 'text/html', @response.content_type
87 87 assert_tag 'th',
88 88 :attributes => {:class => "filename"},
89 89 :content => /issues_controller.rb\t\(rΓ©vision 1484\)/
90 90 assert_tag 'td',
91 91 :attributes => {:class => /line-code/},
92 92 :content => /Demande créée avec succès/
93 93 end
94 94 end
95 95 set_tmp_attachments_directory
96 96 end
97 97
98 98 def test_save_diff_type
99 99 @request.session[:user_id] = 1 # admin
100 100 user = User.find(1)
101 101 get :show, :id => 5
102 102 assert_response :success
103 103 assert_template 'diff'
104 104 user.reload
105 105 assert_equal "inline", user.pref[:diff_type]
106 106 get :show, :id => 5, :type => 'sbs'
107 107 assert_response :success
108 108 assert_template 'diff'
109 109 user.reload
110 110 assert_equal "sbs", user.pref[:diff_type]
111 111 end
112 112
113 113 def test_show_text_file
114 114 get :show, :id => 4
115 115 assert_response :success
116 116 assert_template 'file'
117 117 assert_equal 'text/html', @response.content_type
118 118 set_tmp_attachments_directory
119 119 end
120 120
121 121 def test_show_text_file_utf_8
122 122 set_tmp_attachments_directory
123 123 a = Attachment.new(:container => Issue.find(1),
124 124 :file => uploaded_test_file("japanese-utf-8.txt", "text/plain"),
125 125 :author => User.find(1))
126 126 assert a.save
127 127 assert_equal 'japanese-utf-8.txt', a.filename
128 128
129 129 str_japanese = "\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e"
130 130 str_japanese.force_encoding('UTF-8') if str_japanese.respond_to?(:force_encoding)
131 131
132 132 get :show, :id => a.id
133 133 assert_response :success
134 134 assert_template 'file'
135 135 assert_equal 'text/html', @response.content_type
136 136 assert_tag :tag => 'th',
137 137 :content => '1',
138 138 :attributes => { :class => 'line-num' },
139 139 :sibling => { :tag => 'td', :content => /#{str_japanese}/ }
140 140 end
141 141
142 142 def test_show_text_file_replcace_cannot_convert_content
143 143 set_tmp_attachments_directory
144 144 with_settings :repositories_encodings => 'UTF-8' do
145 145 a = Attachment.new(:container => Issue.find(1),
146 146 :file => uploaded_test_file("iso8859-1.txt", "text/plain"),
147 147 :author => User.find(1))
148 148 assert a.save
149 149 assert_equal 'iso8859-1.txt', a.filename
150 150
151 151 get :show, :id => a.id
152 152 assert_response :success
153 153 assert_template 'file'
154 154 assert_equal 'text/html', @response.content_type
155 155 assert_tag :tag => 'th',
156 156 :content => '7',
157 157 :attributes => { :class => 'line-num' },
158 158 :sibling => { :tag => 'td', :content => /Demande cr\?\?e avec succ\?s/ }
159 159 end
160 160 end
161 161
162 162 def test_show_text_file_latin_1
163 163 set_tmp_attachments_directory
164 164 with_settings :repositories_encodings => 'UTF-8,ISO-8859-1' do
165 165 a = Attachment.new(:container => Issue.find(1),
166 166 :file => uploaded_test_file("iso8859-1.txt", "text/plain"),
167 167 :author => User.find(1))
168 168 assert a.save
169 169 assert_equal 'iso8859-1.txt', a.filename
170 170
171 171 get :show, :id => a.id
172 172 assert_response :success
173 173 assert_template 'file'
174 174 assert_equal 'text/html', @response.content_type
175 175 assert_tag :tag => 'th',
176 176 :content => '7',
177 177 :attributes => { :class => 'line-num' },
178 178 :sibling => { :tag => 'td', :content => /Demande créée avec succès/ }
179 179 end
180 180 end
181 181
182 182 def test_show_text_file_should_send_if_too_big
183 183 Setting.file_max_size_displayed = 512
184 184 Attachment.find(4).update_attribute :filesize, 754.kilobyte
185 185
186 186 get :show, :id => 4
187 187 assert_response :success
188 188 assert_equal 'application/x-ruby', @response.content_type
189 189 set_tmp_attachments_directory
190 190 end
191 191
192 192 def test_show_other
193 193 get :show, :id => 6
194 194 assert_response :success
195 195 assert_equal 'application/octet-stream', @response.content_type
196 196 set_tmp_attachments_directory
197 197 end
198 198
199 199 def test_show_file_from_private_issue_without_permission
200 200 get :show, :id => 15
201 201 assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Fattachments%2F15'
202 202 set_tmp_attachments_directory
203 203 end
204 204
205 205 def test_show_file_from_private_issue_with_permission
206 206 @request.session[:user_id] = 2
207 207 get :show, :id => 15
208 208 assert_response :success
209 209 assert_tag 'h2', :content => /private.diff/
210 210 set_tmp_attachments_directory
211 211 end
212 212
213 213 def test_show_file_without_container_should_be_denied
214 214 set_tmp_attachments_directory
215 215 attachment = Attachment.create!(:file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 2)
216 216
217 217 @request.session[:user_id] = 2
218 218 get :show, :id => attachment.id
219 219 assert_response 403
220 220 end
221 221
222 222 def test_show_invalid_should_respond_with_404
223 223 get :show, :id => 999
224 224 assert_response 404
225 225 end
226 226
227 227 def test_download_text_file
228 228 get :download, :id => 4
229 229 assert_response :success
230 230 assert_equal 'application/x-ruby', @response.content_type
231 231 set_tmp_attachments_directory
232 232 end
233 233
234 234 def test_download_version_file_with_issue_tracking_disabled
235 235 Project.find(1).disable_module! :issue_tracking
236 236 get :download, :id => 9
237 237 assert_response :success
238 238 end
239 239
240 240 def test_download_should_assign_content_type_if_blank
241 241 Attachment.find(4).update_attribute(:content_type, '')
242 242
243 243 get :download, :id => 4
244 244 assert_response :success
245 245 assert_equal 'text/x-ruby', @response.content_type
246 246 set_tmp_attachments_directory
247 247 end
248 248
249 249 def test_download_missing_file
250 250 get :download, :id => 2
251 251 assert_response 404
252 252 set_tmp_attachments_directory
253 253 end
254 254
255 255 def test_download_should_be_denied_without_permission
256 256 get :download, :id => 7
257 257 assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Fattachments%2Fdownload%2F7'
258 258 set_tmp_attachments_directory
259 259 end
260 260
261 261 if convert_installed?
262 262 def test_thumbnail
263 263 Attachment.clear_thumbnails
264 264 @request.session[:user_id] = 2
265 with_settings :thumbnails_enabled => '1' do
266 get :thumbnail, :id => 16
267 assert_response :success
268 assert_equal 'image/png', response.content_type
269 end
265
266 get :thumbnail, :id => 16
267 assert_response :success
268 assert_equal 'image/png', response.content_type
270 269 end
271 270
272 def test_thumbnail_should_return_404_for_non_image_attachment
271 def test_thumbnail_should_not_exceed_maximum_size
272 Redmine::Thumbnail.expects(:generate).with {|source, target, size| size == 800}
273
273 274 @request.session[:user_id] = 2
274 with_settings :thumbnails_enabled => '1' do
275 get :thumbnail, :id => 15
276 assert_response 404
277 end
275 get :thumbnail, :id => 16, :size => 2000
278 276 end
279 277
280 def test_thumbnail_should_return_404_if_thumbnails_not_enabled
278 def test_thumbnail_should_round_size
279 Redmine::Thumbnail.expects(:generate).with {|source, target, size| size == 250}
280
281 281 @request.session[:user_id] = 2
282 with_settings :thumbnails_enabled => '0' do
283 get :thumbnail, :id => 16
284 assert_response 404
285 end
282 get :thumbnail, :id => 16, :size => 260
283 end
284
285 def test_thumbnail_should_return_404_for_non_image_attachment
286 @request.session[:user_id] = 2
287
288 get :thumbnail, :id => 15
289 assert_response 404
286 290 end
287 291
288 292 def test_thumbnail_should_return_404_if_thumbnail_generation_failed
289 293 Attachment.any_instance.stubs(:thumbnail).returns(nil)
290 294 @request.session[:user_id] = 2
291 with_settings :thumbnails_enabled => '1' do
292 get :thumbnail, :id => 16
293 assert_response 404
294 end
295
296 get :thumbnail, :id => 16
297 assert_response 404
295 298 end
296 299
297 300 def test_thumbnail_should_be_denied_without_permission
298 with_settings :thumbnails_enabled => '1' do
299 get :thumbnail, :id => 16
300 assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Fattachments%2Fthumbnail%2F16'
301 end
301 get :thumbnail, :id => 16
302 assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Fattachments%2Fthumbnail%2F16'
302 303 end
303 304 else
304 305 puts '(ImageMagick convert not available)'
305 306 end
306 307
307 308 def test_destroy_issue_attachment
308 309 set_tmp_attachments_directory
309 310 issue = Issue.find(3)
310 311 @request.session[:user_id] = 2
311 312
312 313 assert_difference 'issue.attachments.count', -1 do
313 314 assert_difference 'Journal.count' do
314 315 delete :destroy, :id => 1
315 316 assert_redirected_to '/projects/ecookbook'
316 317 end
317 318 end
318 319 assert_nil Attachment.find_by_id(1)
319 320 j = Journal.first(:order => 'id DESC')
320 321 assert_equal issue, j.journalized
321 322 assert_equal 'attachment', j.details.first.property
322 323 assert_equal '1', j.details.first.prop_key
323 324 assert_equal 'error281.txt', j.details.first.old_value
324 325 assert_equal User.find(2), j.user
325 326 end
326 327
327 328 def test_destroy_wiki_page_attachment
328 329 set_tmp_attachments_directory
329 330 @request.session[:user_id] = 2
330 331 assert_difference 'Attachment.count', -1 do
331 332 delete :destroy, :id => 3
332 333 assert_response 302
333 334 end
334 335 end
335 336
336 337 def test_destroy_project_attachment
337 338 set_tmp_attachments_directory
338 339 @request.session[:user_id] = 2
339 340 assert_difference 'Attachment.count', -1 do
340 341 delete :destroy, :id => 8
341 342 assert_response 302
342 343 end
343 344 end
344 345
345 346 def test_destroy_version_attachment
346 347 set_tmp_attachments_directory
347 348 @request.session[:user_id] = 2
348 349 assert_difference 'Attachment.count', -1 do
349 350 delete :destroy, :id => 9
350 351 assert_response 302
351 352 end
352 353 end
353 354
354 355 def test_destroy_without_permission
355 356 set_tmp_attachments_directory
356 357 assert_no_difference 'Attachment.count' do
357 358 delete :destroy, :id => 3
358 359 end
359 360 assert_response 302
360 361 assert Attachment.find_by_id(3)
361 362 end
362 363 end
@@ -1,65 +1,69
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 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 File.expand_path('../../../test_helper', __FILE__)
19 19
20 20 class RoutingAttachmentsTest < ActionController::IntegrationTest
21 21 def test_attachments
22 22 assert_routing(
23 23 { :method => 'get', :path => "/attachments/1" },
24 24 { :controller => 'attachments', :action => 'show', :id => '1' }
25 25 )
26 26 assert_routing(
27 27 { :method => 'get', :path => "/attachments/1.xml" },
28 28 { :controller => 'attachments', :action => 'show', :id => '1', :format => 'xml' }
29 29 )
30 30 assert_routing(
31 31 { :method => 'get', :path => "/attachments/1.json" },
32 32 { :controller => 'attachments', :action => 'show', :id => '1', :format => 'json' }
33 33 )
34 34 assert_routing(
35 35 { :method => 'get', :path => "/attachments/1/filename.ext" },
36 36 { :controller => 'attachments', :action => 'show', :id => '1',
37 37 :filename => 'filename.ext' }
38 38 )
39 39 assert_routing(
40 40 { :method => 'get', :path => "/attachments/download/1" },
41 41 { :controller => 'attachments', :action => 'download', :id => '1' }
42 42 )
43 43 assert_routing(
44 44 { :method => 'get', :path => "/attachments/download/1/filename.ext" },
45 45 { :controller => 'attachments', :action => 'download', :id => '1',
46 46 :filename => 'filename.ext' }
47 47 )
48 48 assert_routing(
49 49 { :method => 'get', :path => "/attachments/thumbnail/1" },
50 50 { :controller => 'attachments', :action => 'thumbnail', :id => '1' }
51 51 )
52 52 assert_routing(
53 { :method => 'get', :path => "/attachments/thumbnail/1/200" },
54 { :controller => 'attachments', :action => 'thumbnail', :id => '1', :size => '200' }
55 )
56 assert_routing(
53 57 { :method => 'delete', :path => "/attachments/1" },
54 58 { :controller => 'attachments', :action => 'destroy', :id => '1' }
55 59 )
56 60 assert_routing(
57 61 { :method => 'post', :path => '/uploads.xml' },
58 62 { :controller => 'attachments', :action => 'upload', :format => 'xml' }
59 63 )
60 64 assert_routing(
61 65 { :method => 'post', :path => '/uploads.json' },
62 66 { :controller => 'attachments', :action => 'upload', :format => 'json' }
63 67 )
64 68 end
65 69 end
@@ -1,122 +1,142
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 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 File.expand_path('../../../../../test_helper', __FILE__)
19 19
20 20 class Redmine::WikiFormatting::MacrosTest < ActionView::TestCase
21 21 include ApplicationHelper
22 22 include ActionView::Helpers::TextHelper
23 23 include ActionView::Helpers::SanitizeHelper
24 24 include ERB::Util
25 25 extend ActionView::Helpers::SanitizeHelper::ClassMethods
26 26
27 27 fixtures :projects, :roles, :enabled_modules, :users,
28 28 :repositories, :changesets,
29 29 :trackers, :issue_statuses, :issues,
30 30 :versions, :documents,
31 31 :wikis, :wiki_pages, :wiki_contents,
32 32 :boards, :messages,
33 33 :attachments
34 34
35 35 def setup
36 36 super
37 37 @project = nil
38 38 end
39 39
40 40 def teardown
41 41 end
42 42
43 43 def test_macro_registration
44 44 Redmine::WikiFormatting::Macros.register do
45 45 macro :foo do |obj, args|
46 46 "Foo macro output"
47 47 end
48 48 end
49 49
50 50 text = "{{foo}}"
51 51 assert_equal '<p>Foo macro output</p>', textilizable(text)
52 52 end
53 53
54 54 def test_macro_hello_world
55 55 text = "{{hello_world}}"
56 56 assert textilizable(text).match(/Hello world!/)
57 57 # escaping
58 58 text = "!{{hello_world}}"
59 59 assert_equal '<p>{{hello_world}}</p>', textilizable(text)
60 60 end
61 61
62 62 def test_macro_macro_list
63 63 text = "{{macro_list}}"
64 64 assert_match %r{<code>hello_world</code>}, textilizable(text)
65 65 end
66 66
67 67 def test_macro_include
68 68 @project = Project.find(1)
69 69 # include a page of the current project wiki
70 70 text = "{{include(Another page)}}"
71 71 assert textilizable(text).match(/This is a link to a ticket/)
72 72
73 73 @project = nil
74 74 # include a page of a specific project wiki
75 75 text = "{{include(ecookbook:Another page)}}"
76 76 assert textilizable(text).match(/This is a link to a ticket/)
77 77
78 78 text = "{{include(ecookbook:)}}"
79 79 assert textilizable(text).match(/CookBook documentation/)
80 80
81 81 text = "{{include(unknowidentifier:somepage)}}"
82 82 assert textilizable(text).match(/Page not found/)
83 83 end
84 84
85 85 def test_macro_child_pages
86 86 expected = "<p><ul class=\"pages-hierarchy\">\n" +
87 87 "<li><a href=\"/projects/ecookbook/wiki/Child_1\">Child 1</a></li>\n" +
88 88 "<li><a href=\"/projects/ecookbook/wiki/Child_2\">Child 2</a></li>\n" +
89 89 "</ul>\n</p>"
90 90
91 91 @project = Project.find(1)
92 92 # child pages of the current wiki page
93 93 assert_equal expected, textilizable("{{child_pages}}", :object => WikiPage.find(2).content)
94 94 # child pages of another page
95 95 assert_equal expected, textilizable("{{child_pages(Another_page)}}", :object => WikiPage.find(1).content)
96 96
97 97 @project = Project.find(2)
98 98 assert_equal expected, textilizable("{{child_pages(ecookbook:Another_page)}}", :object => WikiPage.find(1).content)
99 99 end
100 100
101 101 def test_macro_child_pages_with_option
102 102 expected = "<p><ul class=\"pages-hierarchy\">\n" +
103 103 "<li><a href=\"/projects/ecookbook/wiki/Another_page\">Another page</a>\n" +
104 104 "<ul class=\"pages-hierarchy\">\n" +
105 105 "<li><a href=\"/projects/ecookbook/wiki/Child_1\">Child 1</a></li>\n" +
106 106 "<li><a href=\"/projects/ecookbook/wiki/Child_2\">Child 2</a></li>\n" +
107 107 "</ul>\n</li>\n</ul>\n</p>"
108 108
109 109 @project = Project.find(1)
110 110 # child pages of the current wiki page
111 111 assert_equal expected, textilizable("{{child_pages(parent=1)}}", :object => WikiPage.find(2).content)
112 112 # child pages of another page
113 113 assert_equal expected, textilizable("{{child_pages(Another_page, parent=1)}}", :object => WikiPage.find(1).content)
114 114
115 115 @project = Project.find(2)
116 116 assert_equal expected, textilizable("{{child_pages(ecookbook:Another_page, parent=1)}}", :object => WikiPage.find(1).content)
117 117 end
118 118
119 119 def test_macro_child_pages_without_wiki_page_should_fail
120 120 assert_match /can be called from wiki pages only/, textilizable("{{child_pages}}")
121 121 end
122
123 def test_macro_thumbnail
124 assert_equal '<p><a href="/attachments/17" class="thumbnail" title="testfile.PNG"><img alt="testfile.PNG" src="/attachments/thumbnail/17" /></a></p>',
125 textilizable("{{thumbnail(testfile.png)}}", :object => Issue.find(14))
126 end
127
128 def test_macro_thumbnail_with_size
129 assert_equal '<p><a href="/attachments/17" class="thumbnail" title="testfile.PNG"><img alt="testfile.PNG" src="/attachments/thumbnail/17/200" /></a></p>',
130 textilizable("{{thumbnail(testfile.png, size=200)}}", :object => Issue.find(14))
131 end
132
133 def test_macro_thumbnail_with_title
134 assert_equal '<p><a href="/attachments/17" class="thumbnail" title="Cool image"><img alt="testfile.PNG" src="/attachments/thumbnail/17" /></a></p>',
135 textilizable("{{thumbnail(testfile.png, title=Cool image)}}", :object => Issue.find(14))
136 end
137
138 def test_macro_thumbnail_with_invalid_filename_should_fail
139 assert_include 'test.png not found',
140 textilizable("{{thumbnail(test.png)}}", :object => Issue.find(14))
141 end
122 142 end
General Comments 0
You need to be logged in to leave comments. Login now