##// END OF EJS Templates
Include helper instead of patching (#20508)....
Jean-Philippe Lang -
r14311:7356e18d36a6
parent child
Show More
@@ -1,680 +1,681
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 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 'uri'
18 require 'uri'
19 require 'cgi'
19 require 'cgi'
20
20
21 class Unauthorized < Exception; end
21 class Unauthorized < Exception; end
22
22
23 class ApplicationController < ActionController::Base
23 class ApplicationController < ActionController::Base
24 include Redmine::I18n
24 include Redmine::I18n
25 include Redmine::Pagination
25 include Redmine::Pagination
26 include Redmine::Hook::Helper
26 include RoutesHelper
27 include RoutesHelper
27 helper :routes
28 helper :routes
28
29
29 class_attribute :accept_api_auth_actions
30 class_attribute :accept_api_auth_actions
30 class_attribute :accept_rss_auth_actions
31 class_attribute :accept_rss_auth_actions
31 class_attribute :model_object
32 class_attribute :model_object
32
33
33 layout 'base'
34 layout 'base'
34
35
35 protect_from_forgery
36 protect_from_forgery
36
37
37 def verify_authenticity_token
38 def verify_authenticity_token
38 unless api_request?
39 unless api_request?
39 super
40 super
40 end
41 end
41 end
42 end
42
43
43 def handle_unverified_request
44 def handle_unverified_request
44 unless api_request?
45 unless api_request?
45 super
46 super
46 cookies.delete(autologin_cookie_name)
47 cookies.delete(autologin_cookie_name)
47 self.logged_user = nil
48 self.logged_user = nil
48 set_localization
49 set_localization
49 render_error :status => 422, :message => "Invalid form authenticity token."
50 render_error :status => 422, :message => "Invalid form authenticity token."
50 end
51 end
51 end
52 end
52
53
53 before_filter :session_expiration, :user_setup, :force_logout_if_password_changed, :check_if_login_required, :check_password_change, :set_localization
54 before_filter :session_expiration, :user_setup, :force_logout_if_password_changed, :check_if_login_required, :check_password_change, :set_localization
54
55
55 rescue_from ::Unauthorized, :with => :deny_access
56 rescue_from ::Unauthorized, :with => :deny_access
56 rescue_from ::ActionView::MissingTemplate, :with => :missing_template
57 rescue_from ::ActionView::MissingTemplate, :with => :missing_template
57
58
58 include Redmine::Search::Controller
59 include Redmine::Search::Controller
59 include Redmine::MenuManager::MenuController
60 include Redmine::MenuManager::MenuController
60 helper Redmine::MenuManager::MenuHelper
61 helper Redmine::MenuManager::MenuHelper
61
62
62 include Redmine::SudoMode::Controller
63 include Redmine::SudoMode::Controller
63
64
64 def session_expiration
65 def session_expiration
65 if session[:user_id]
66 if session[:user_id]
66 if session_expired? && !try_to_autologin
67 if session_expired? && !try_to_autologin
67 set_localization(User.active.find_by_id(session[:user_id]))
68 set_localization(User.active.find_by_id(session[:user_id]))
68 self.logged_user = nil
69 self.logged_user = nil
69 flash[:error] = l(:error_session_expired)
70 flash[:error] = l(:error_session_expired)
70 require_login
71 require_login
71 else
72 else
72 session[:atime] = Time.now.utc.to_i
73 session[:atime] = Time.now.utc.to_i
73 end
74 end
74 end
75 end
75 end
76 end
76
77
77 def session_expired?
78 def session_expired?
78 if Setting.session_lifetime?
79 if Setting.session_lifetime?
79 unless session[:ctime] && (Time.now.utc.to_i - session[:ctime].to_i <= Setting.session_lifetime.to_i * 60)
80 unless session[:ctime] && (Time.now.utc.to_i - session[:ctime].to_i <= Setting.session_lifetime.to_i * 60)
80 return true
81 return true
81 end
82 end
82 end
83 end
83 if Setting.session_timeout?
84 if Setting.session_timeout?
84 unless session[:atime] && (Time.now.utc.to_i - session[:atime].to_i <= Setting.session_timeout.to_i * 60)
85 unless session[:atime] && (Time.now.utc.to_i - session[:atime].to_i <= Setting.session_timeout.to_i * 60)
85 return true
86 return true
86 end
87 end
87 end
88 end
88 false
89 false
89 end
90 end
90
91
91 def start_user_session(user)
92 def start_user_session(user)
92 session[:user_id] = user.id
93 session[:user_id] = user.id
93 session[:ctime] = Time.now.utc.to_i
94 session[:ctime] = Time.now.utc.to_i
94 session[:atime] = Time.now.utc.to_i
95 session[:atime] = Time.now.utc.to_i
95 if user.must_change_password?
96 if user.must_change_password?
96 session[:pwd] = '1'
97 session[:pwd] = '1'
97 end
98 end
98 end
99 end
99
100
100 def user_setup
101 def user_setup
101 # Check the settings cache for each request
102 # Check the settings cache for each request
102 Setting.check_cache
103 Setting.check_cache
103 # Find the current user
104 # Find the current user
104 User.current = find_current_user
105 User.current = find_current_user
105 logger.info(" Current user: " + (User.current.logged? ? "#{User.current.login} (id=#{User.current.id})" : "anonymous")) if logger
106 logger.info(" Current user: " + (User.current.logged? ? "#{User.current.login} (id=#{User.current.id})" : "anonymous")) if logger
106 end
107 end
107
108
108 # Returns the current user or nil if no user is logged in
109 # Returns the current user or nil if no user is logged in
109 # and starts a session if needed
110 # and starts a session if needed
110 def find_current_user
111 def find_current_user
111 user = nil
112 user = nil
112 unless api_request?
113 unless api_request?
113 if session[:user_id]
114 if session[:user_id]
114 # existing session
115 # existing session
115 user = (User.active.find(session[:user_id]) rescue nil)
116 user = (User.active.find(session[:user_id]) rescue nil)
116 elsif autologin_user = try_to_autologin
117 elsif autologin_user = try_to_autologin
117 user = autologin_user
118 user = autologin_user
118 elsif params[:format] == 'atom' && params[:key] && request.get? && accept_rss_auth?
119 elsif params[:format] == 'atom' && params[:key] && request.get? && accept_rss_auth?
119 # RSS key authentication does not start a session
120 # RSS key authentication does not start a session
120 user = User.find_by_rss_key(params[:key])
121 user = User.find_by_rss_key(params[:key])
121 end
122 end
122 end
123 end
123 if user.nil? && Setting.rest_api_enabled? && accept_api_auth?
124 if user.nil? && Setting.rest_api_enabled? && accept_api_auth?
124 if (key = api_key_from_request)
125 if (key = api_key_from_request)
125 # Use API key
126 # Use API key
126 user = User.find_by_api_key(key)
127 user = User.find_by_api_key(key)
127 elsif request.authorization.to_s =~ /\ABasic /i
128 elsif request.authorization.to_s =~ /\ABasic /i
128 # HTTP Basic, either username/password or API key/random
129 # HTTP Basic, either username/password or API key/random
129 authenticate_with_http_basic do |username, password|
130 authenticate_with_http_basic do |username, password|
130 user = User.try_to_login(username, password) || User.find_by_api_key(username)
131 user = User.try_to_login(username, password) || User.find_by_api_key(username)
131 end
132 end
132 if user && user.must_change_password?
133 if user && user.must_change_password?
133 render_error :message => 'You must change your password', :status => 403
134 render_error :message => 'You must change your password', :status => 403
134 return
135 return
135 end
136 end
136 end
137 end
137 # Switch user if requested by an admin user
138 # Switch user if requested by an admin user
138 if user && user.admin? && (username = api_switch_user_from_request)
139 if user && user.admin? && (username = api_switch_user_from_request)
139 su = User.find_by_login(username)
140 su = User.find_by_login(username)
140 if su && su.active?
141 if su && su.active?
141 logger.info(" User switched by: #{user.login} (id=#{user.id})") if logger
142 logger.info(" User switched by: #{user.login} (id=#{user.id})") if logger
142 user = su
143 user = su
143 else
144 else
144 render_error :message => 'Invalid X-Redmine-Switch-User header', :status => 412
145 render_error :message => 'Invalid X-Redmine-Switch-User header', :status => 412
145 end
146 end
146 end
147 end
147 end
148 end
148 user
149 user
149 end
150 end
150
151
151 def force_logout_if_password_changed
152 def force_logout_if_password_changed
152 passwd_changed_on = User.current.passwd_changed_on || Time.at(0)
153 passwd_changed_on = User.current.passwd_changed_on || Time.at(0)
153 # Make sure we force logout only for web browser sessions, not API calls
154 # Make sure we force logout only for web browser sessions, not API calls
154 # if the password was changed after the session creation.
155 # if the password was changed after the session creation.
155 if session[:user_id] && passwd_changed_on.utc.to_i > session[:ctime].to_i
156 if session[:user_id] && passwd_changed_on.utc.to_i > session[:ctime].to_i
156 reset_session
157 reset_session
157 set_localization
158 set_localization
158 flash[:error] = l(:error_session_expired)
159 flash[:error] = l(:error_session_expired)
159 redirect_to signin_url
160 redirect_to signin_url
160 end
161 end
161 end
162 end
162
163
163 def autologin_cookie_name
164 def autologin_cookie_name
164 Redmine::Configuration['autologin_cookie_name'].presence || 'autologin'
165 Redmine::Configuration['autologin_cookie_name'].presence || 'autologin'
165 end
166 end
166
167
167 def try_to_autologin
168 def try_to_autologin
168 if cookies[autologin_cookie_name] && Setting.autologin?
169 if cookies[autologin_cookie_name] && Setting.autologin?
169 # auto-login feature starts a new session
170 # auto-login feature starts a new session
170 user = User.try_to_autologin(cookies[autologin_cookie_name])
171 user = User.try_to_autologin(cookies[autologin_cookie_name])
171 if user
172 if user
172 reset_session
173 reset_session
173 start_user_session(user)
174 start_user_session(user)
174 end
175 end
175 user
176 user
176 end
177 end
177 end
178 end
178
179
179 # Sets the logged in user
180 # Sets the logged in user
180 def logged_user=(user)
181 def logged_user=(user)
181 reset_session
182 reset_session
182 if user && user.is_a?(User)
183 if user && user.is_a?(User)
183 User.current = user
184 User.current = user
184 start_user_session(user)
185 start_user_session(user)
185 else
186 else
186 User.current = User.anonymous
187 User.current = User.anonymous
187 end
188 end
188 end
189 end
189
190
190 # Logs out current user
191 # Logs out current user
191 def logout_user
192 def logout_user
192 if User.current.logged?
193 if User.current.logged?
193 cookies.delete(autologin_cookie_name)
194 cookies.delete(autologin_cookie_name)
194 Token.delete_all(["user_id = ? AND action = ?", User.current.id, 'autologin'])
195 Token.delete_all(["user_id = ? AND action = ?", User.current.id, 'autologin'])
195 self.logged_user = nil
196 self.logged_user = nil
196 end
197 end
197 end
198 end
198
199
199 # check if login is globally required to access the application
200 # check if login is globally required to access the application
200 def check_if_login_required
201 def check_if_login_required
201 # no check needed if user is already logged in
202 # no check needed if user is already logged in
202 return true if User.current.logged?
203 return true if User.current.logged?
203 require_login if Setting.login_required?
204 require_login if Setting.login_required?
204 end
205 end
205
206
206 def check_password_change
207 def check_password_change
207 if session[:pwd]
208 if session[:pwd]
208 if User.current.must_change_password?
209 if User.current.must_change_password?
209 flash[:error] = l(:error_password_expired)
210 flash[:error] = l(:error_password_expired)
210 redirect_to my_password_path
211 redirect_to my_password_path
211 else
212 else
212 session.delete(:pwd)
213 session.delete(:pwd)
213 end
214 end
214 end
215 end
215 end
216 end
216
217
217 def set_localization(user=User.current)
218 def set_localization(user=User.current)
218 lang = nil
219 lang = nil
219 if user && user.logged?
220 if user && user.logged?
220 lang = find_language(user.language)
221 lang = find_language(user.language)
221 end
222 end
222 if lang.nil? && !Setting.force_default_language_for_anonymous? && request.env['HTTP_ACCEPT_LANGUAGE']
223 if lang.nil? && !Setting.force_default_language_for_anonymous? && request.env['HTTP_ACCEPT_LANGUAGE']
223 accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first
224 accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first
224 if !accept_lang.blank?
225 if !accept_lang.blank?
225 accept_lang = accept_lang.downcase
226 accept_lang = accept_lang.downcase
226 lang = find_language(accept_lang) || find_language(accept_lang.split('-').first)
227 lang = find_language(accept_lang) || find_language(accept_lang.split('-').first)
227 end
228 end
228 end
229 end
229 lang ||= Setting.default_language
230 lang ||= Setting.default_language
230 set_language_if_valid(lang)
231 set_language_if_valid(lang)
231 end
232 end
232
233
233 def require_login
234 def require_login
234 if !User.current.logged?
235 if !User.current.logged?
235 # Extract only the basic url parameters on non-GET requests
236 # Extract only the basic url parameters on non-GET requests
236 if request.get?
237 if request.get?
237 url = url_for(params)
238 url = url_for(params)
238 else
239 else
239 url = url_for(:controller => params[:controller], :action => params[:action], :id => params[:id], :project_id => params[:project_id])
240 url = url_for(:controller => params[:controller], :action => params[:action], :id => params[:id], :project_id => params[:project_id])
240 end
241 end
241 respond_to do |format|
242 respond_to do |format|
242 format.html {
243 format.html {
243 if request.xhr?
244 if request.xhr?
244 head :unauthorized
245 head :unauthorized
245 else
246 else
246 redirect_to signin_path(:back_url => url)
247 redirect_to signin_path(:back_url => url)
247 end
248 end
248 }
249 }
249 format.any(:atom, :pdf, :csv) {
250 format.any(:atom, :pdf, :csv) {
250 redirect_to signin_path(:back_url => url)
251 redirect_to signin_path(:back_url => url)
251 }
252 }
252 format.xml { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
253 format.xml { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
253 format.js { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
254 format.js { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
254 format.json { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
255 format.json { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
255 format.any { head :unauthorized }
256 format.any { head :unauthorized }
256 end
257 end
257 return false
258 return false
258 end
259 end
259 true
260 true
260 end
261 end
261
262
262 def require_admin
263 def require_admin
263 return unless require_login
264 return unless require_login
264 if !User.current.admin?
265 if !User.current.admin?
265 render_403
266 render_403
266 return false
267 return false
267 end
268 end
268 true
269 true
269 end
270 end
270
271
271 def deny_access
272 def deny_access
272 User.current.logged? ? render_403 : require_login
273 User.current.logged? ? render_403 : require_login
273 end
274 end
274
275
275 # Authorize the user for the requested action
276 # Authorize the user for the requested action
276 def authorize(ctrl = params[:controller], action = params[:action], global = false)
277 def authorize(ctrl = params[:controller], action = params[:action], global = false)
277 allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project || @projects, :global => global)
278 allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project || @projects, :global => global)
278 if allowed
279 if allowed
279 true
280 true
280 else
281 else
281 if @project && @project.archived?
282 if @project && @project.archived?
282 render_403 :message => :notice_not_authorized_archived_project
283 render_403 :message => :notice_not_authorized_archived_project
283 else
284 else
284 deny_access
285 deny_access
285 end
286 end
286 end
287 end
287 end
288 end
288
289
289 # Authorize the user for the requested action outside a project
290 # Authorize the user for the requested action outside a project
290 def authorize_global(ctrl = params[:controller], action = params[:action], global = true)
291 def authorize_global(ctrl = params[:controller], action = params[:action], global = true)
291 authorize(ctrl, action, global)
292 authorize(ctrl, action, global)
292 end
293 end
293
294
294 # Find project of id params[:id]
295 # Find project of id params[:id]
295 def find_project
296 def find_project
296 @project = Project.find(params[:id])
297 @project = Project.find(params[:id])
297 rescue ActiveRecord::RecordNotFound
298 rescue ActiveRecord::RecordNotFound
298 render_404
299 render_404
299 end
300 end
300
301
301 # Find project of id params[:project_id]
302 # Find project of id params[:project_id]
302 def find_project_by_project_id
303 def find_project_by_project_id
303 @project = Project.find(params[:project_id])
304 @project = Project.find(params[:project_id])
304 rescue ActiveRecord::RecordNotFound
305 rescue ActiveRecord::RecordNotFound
305 render_404
306 render_404
306 end
307 end
307
308
308 # Find a project based on params[:project_id]
309 # Find a project based on params[:project_id]
309 # TODO: some subclasses override this, see about merging their logic
310 # TODO: some subclasses override this, see about merging their logic
310 def find_optional_project
311 def find_optional_project
311 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
312 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
312 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
313 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
313 allowed ? true : deny_access
314 allowed ? true : deny_access
314 rescue ActiveRecord::RecordNotFound
315 rescue ActiveRecord::RecordNotFound
315 render_404
316 render_404
316 end
317 end
317
318
318 # Finds and sets @project based on @object.project
319 # Finds and sets @project based on @object.project
319 def find_project_from_association
320 def find_project_from_association
320 render_404 unless @object.present?
321 render_404 unless @object.present?
321
322
322 @project = @object.project
323 @project = @object.project
323 end
324 end
324
325
325 def find_model_object
326 def find_model_object
326 model = self.class.model_object
327 model = self.class.model_object
327 if model
328 if model
328 @object = model.find(params[:id])
329 @object = model.find(params[:id])
329 self.instance_variable_set('@' + controller_name.singularize, @object) if @object
330 self.instance_variable_set('@' + controller_name.singularize, @object) if @object
330 end
331 end
331 rescue ActiveRecord::RecordNotFound
332 rescue ActiveRecord::RecordNotFound
332 render_404
333 render_404
333 end
334 end
334
335
335 def self.model_object(model)
336 def self.model_object(model)
336 self.model_object = model
337 self.model_object = model
337 end
338 end
338
339
339 # Find the issue whose id is the :id parameter
340 # Find the issue whose id is the :id parameter
340 # Raises a Unauthorized exception if the issue is not visible
341 # Raises a Unauthorized exception if the issue is not visible
341 def find_issue
342 def find_issue
342 # Issue.visible.find(...) can not be used to redirect user to the login form
343 # Issue.visible.find(...) can not be used to redirect user to the login form
343 # if the issue actually exists but requires authentication
344 # if the issue actually exists but requires authentication
344 @issue = Issue.find(params[:id])
345 @issue = Issue.find(params[:id])
345 raise Unauthorized unless @issue.visible?
346 raise Unauthorized unless @issue.visible?
346 @project = @issue.project
347 @project = @issue.project
347 rescue ActiveRecord::RecordNotFound
348 rescue ActiveRecord::RecordNotFound
348 render_404
349 render_404
349 end
350 end
350
351
351 # Find issues with a single :id param or :ids array param
352 # Find issues with a single :id param or :ids array param
352 # Raises a Unauthorized exception if one of the issues is not visible
353 # Raises a Unauthorized exception if one of the issues is not visible
353 def find_issues
354 def find_issues
354 @issues = Issue.where(:id => (params[:id] || params[:ids])).preload(:project, :status, :tracker, :priority, :author, :assigned_to, :relations_to).to_a
355 @issues = Issue.where(:id => (params[:id] || params[:ids])).preload(:project, :status, :tracker, :priority, :author, :assigned_to, :relations_to).to_a
355 raise ActiveRecord::RecordNotFound if @issues.empty?
356 raise ActiveRecord::RecordNotFound if @issues.empty?
356 raise Unauthorized unless @issues.all?(&:visible?)
357 raise Unauthorized unless @issues.all?(&:visible?)
357 @projects = @issues.collect(&:project).compact.uniq
358 @projects = @issues.collect(&:project).compact.uniq
358 @project = @projects.first if @projects.size == 1
359 @project = @projects.first if @projects.size == 1
359 rescue ActiveRecord::RecordNotFound
360 rescue ActiveRecord::RecordNotFound
360 render_404
361 render_404
361 end
362 end
362
363
363 def find_attachments
364 def find_attachments
364 if (attachments = params[:attachments]).present?
365 if (attachments = params[:attachments]).present?
365 att = attachments.values.collect do |attachment|
366 att = attachments.values.collect do |attachment|
366 Attachment.find_by_token( attachment[:token] ) if attachment[:token].present?
367 Attachment.find_by_token( attachment[:token] ) if attachment[:token].present?
367 end
368 end
368 att.compact!
369 att.compact!
369 end
370 end
370 @attachments = att || []
371 @attachments = att || []
371 end
372 end
372
373
373 # make sure that the user is a member of the project (or admin) if project is private
374 # make sure that the user is a member of the project (or admin) if project is private
374 # used as a before_filter for actions that do not require any particular permission on the project
375 # used as a before_filter for actions that do not require any particular permission on the project
375 def check_project_privacy
376 def check_project_privacy
376 if @project && !@project.archived?
377 if @project && !@project.archived?
377 if @project.visible?
378 if @project.visible?
378 true
379 true
379 else
380 else
380 deny_access
381 deny_access
381 end
382 end
382 else
383 else
383 @project = nil
384 @project = nil
384 render_404
385 render_404
385 false
386 false
386 end
387 end
387 end
388 end
388
389
389 def back_url
390 def back_url
390 url = params[:back_url]
391 url = params[:back_url]
391 if url.nil? && referer = request.env['HTTP_REFERER']
392 if url.nil? && referer = request.env['HTTP_REFERER']
392 url = CGI.unescape(referer.to_s)
393 url = CGI.unescape(referer.to_s)
393 end
394 end
394 url
395 url
395 end
396 end
396
397
397 def redirect_back_or_default(default, options={})
398 def redirect_back_or_default(default, options={})
398 back_url = params[:back_url].to_s
399 back_url = params[:back_url].to_s
399 if back_url.present? && valid_url = validate_back_url(back_url)
400 if back_url.present? && valid_url = validate_back_url(back_url)
400 redirect_to(valid_url)
401 redirect_to(valid_url)
401 return
402 return
402 elsif options[:referer]
403 elsif options[:referer]
403 redirect_to_referer_or default
404 redirect_to_referer_or default
404 return
405 return
405 end
406 end
406 redirect_to default
407 redirect_to default
407 false
408 false
408 end
409 end
409
410
410 # Returns a validated URL string if back_url is a valid url for redirection,
411 # Returns a validated URL string if back_url is a valid url for redirection,
411 # otherwise false
412 # otherwise false
412 def validate_back_url(back_url)
413 def validate_back_url(back_url)
413 if CGI.unescape(back_url).include?('..')
414 if CGI.unescape(back_url).include?('..')
414 return false
415 return false
415 end
416 end
416
417
417 begin
418 begin
418 uri = URI.parse(back_url)
419 uri = URI.parse(back_url)
419 rescue URI::InvalidURIError
420 rescue URI::InvalidURIError
420 return false
421 return false
421 end
422 end
422
423
423 [:scheme, :host, :port].each do |component|
424 [:scheme, :host, :port].each do |component|
424 if uri.send(component).present? && uri.send(component) != request.send(component)
425 if uri.send(component).present? && uri.send(component) != request.send(component)
425 return false
426 return false
426 end
427 end
427 uri.send(:"#{component}=", nil)
428 uri.send(:"#{component}=", nil)
428 end
429 end
429 # Always ignore basic user:password in the URL
430 # Always ignore basic user:password in the URL
430 uri.userinfo = nil
431 uri.userinfo = nil
431
432
432 path = uri.to_s
433 path = uri.to_s
433 # Ensure that the remaining URL starts with a slash, followed by a
434 # Ensure that the remaining URL starts with a slash, followed by a
434 # non-slash character or the end
435 # non-slash character or the end
435 if path !~ %r{\A/([^/]|\z)}
436 if path !~ %r{\A/([^/]|\z)}
436 return false
437 return false
437 end
438 end
438
439
439 if path.match(%r{/(login|account/register)})
440 if path.match(%r{/(login|account/register)})
440 return false
441 return false
441 end
442 end
442
443
443 if relative_url_root.present? && !path.starts_with?(relative_url_root)
444 if relative_url_root.present? && !path.starts_with?(relative_url_root)
444 return false
445 return false
445 end
446 end
446
447
447 return path
448 return path
448 end
449 end
449 private :validate_back_url
450 private :validate_back_url
450
451
451 def valid_back_url?(back_url)
452 def valid_back_url?(back_url)
452 !!validate_back_url(back_url)
453 !!validate_back_url(back_url)
453 end
454 end
454 private :valid_back_url?
455 private :valid_back_url?
455
456
456 # Redirects to the request referer if present, redirects to args or call block otherwise.
457 # Redirects to the request referer if present, redirects to args or call block otherwise.
457 def redirect_to_referer_or(*args, &block)
458 def redirect_to_referer_or(*args, &block)
458 redirect_to :back
459 redirect_to :back
459 rescue ::ActionController::RedirectBackError
460 rescue ::ActionController::RedirectBackError
460 if args.any?
461 if args.any?
461 redirect_to *args
462 redirect_to *args
462 elsif block_given?
463 elsif block_given?
463 block.call
464 block.call
464 else
465 else
465 raise "#redirect_to_referer_or takes arguments or a block"
466 raise "#redirect_to_referer_or takes arguments or a block"
466 end
467 end
467 end
468 end
468
469
469 def render_403(options={})
470 def render_403(options={})
470 @project = nil
471 @project = nil
471 render_error({:message => :notice_not_authorized, :status => 403}.merge(options))
472 render_error({:message => :notice_not_authorized, :status => 403}.merge(options))
472 return false
473 return false
473 end
474 end
474
475
475 def render_404(options={})
476 def render_404(options={})
476 render_error({:message => :notice_file_not_found, :status => 404}.merge(options))
477 render_error({:message => :notice_file_not_found, :status => 404}.merge(options))
477 return false
478 return false
478 end
479 end
479
480
480 # Renders an error response
481 # Renders an error response
481 def render_error(arg)
482 def render_error(arg)
482 arg = {:message => arg} unless arg.is_a?(Hash)
483 arg = {:message => arg} unless arg.is_a?(Hash)
483
484
484 @message = arg[:message]
485 @message = arg[:message]
485 @message = l(@message) if @message.is_a?(Symbol)
486 @message = l(@message) if @message.is_a?(Symbol)
486 @status = arg[:status] || 500
487 @status = arg[:status] || 500
487
488
488 respond_to do |format|
489 respond_to do |format|
489 format.html {
490 format.html {
490 render :template => 'common/error', :layout => use_layout, :status => @status
491 render :template => 'common/error', :layout => use_layout, :status => @status
491 }
492 }
492 format.any { head @status }
493 format.any { head @status }
493 end
494 end
494 end
495 end
495
496
496 # Handler for ActionView::MissingTemplate exception
497 # Handler for ActionView::MissingTemplate exception
497 def missing_template
498 def missing_template
498 logger.warn "Missing template, responding with 404"
499 logger.warn "Missing template, responding with 404"
499 @project = nil
500 @project = nil
500 render_404
501 render_404
501 end
502 end
502
503
503 # Filter for actions that provide an API response
504 # Filter for actions that provide an API response
504 # but have no HTML representation for non admin users
505 # but have no HTML representation for non admin users
505 def require_admin_or_api_request
506 def require_admin_or_api_request
506 return true if api_request?
507 return true if api_request?
507 if User.current.admin?
508 if User.current.admin?
508 true
509 true
509 elsif User.current.logged?
510 elsif User.current.logged?
510 render_error(:status => 406)
511 render_error(:status => 406)
511 else
512 else
512 deny_access
513 deny_access
513 end
514 end
514 end
515 end
515
516
516 # Picks which layout to use based on the request
517 # Picks which layout to use based on the request
517 #
518 #
518 # @return [boolean, string] name of the layout to use or false for no layout
519 # @return [boolean, string] name of the layout to use or false for no layout
519 def use_layout
520 def use_layout
520 request.xhr? ? false : 'base'
521 request.xhr? ? false : 'base'
521 end
522 end
522
523
523 def render_feed(items, options={})
524 def render_feed(items, options={})
524 @items = (items || []).to_a
525 @items = (items || []).to_a
525 @items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
526 @items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
526 @items = @items.slice(0, Setting.feeds_limit.to_i)
527 @items = @items.slice(0, Setting.feeds_limit.to_i)
527 @title = options[:title] || Setting.app_title
528 @title = options[:title] || Setting.app_title
528 render :template => "common/feed", :formats => [:atom], :layout => false,
529 render :template => "common/feed", :formats => [:atom], :layout => false,
529 :content_type => 'application/atom+xml'
530 :content_type => 'application/atom+xml'
530 end
531 end
531
532
532 def self.accept_rss_auth(*actions)
533 def self.accept_rss_auth(*actions)
533 if actions.any?
534 if actions.any?
534 self.accept_rss_auth_actions = actions
535 self.accept_rss_auth_actions = actions
535 else
536 else
536 self.accept_rss_auth_actions || []
537 self.accept_rss_auth_actions || []
537 end
538 end
538 end
539 end
539
540
540 def accept_rss_auth?(action=action_name)
541 def accept_rss_auth?(action=action_name)
541 self.class.accept_rss_auth.include?(action.to_sym)
542 self.class.accept_rss_auth.include?(action.to_sym)
542 end
543 end
543
544
544 def self.accept_api_auth(*actions)
545 def self.accept_api_auth(*actions)
545 if actions.any?
546 if actions.any?
546 self.accept_api_auth_actions = actions
547 self.accept_api_auth_actions = actions
547 else
548 else
548 self.accept_api_auth_actions || []
549 self.accept_api_auth_actions || []
549 end
550 end
550 end
551 end
551
552
552 def accept_api_auth?(action=action_name)
553 def accept_api_auth?(action=action_name)
553 self.class.accept_api_auth.include?(action.to_sym)
554 self.class.accept_api_auth.include?(action.to_sym)
554 end
555 end
555
556
556 # Returns the number of objects that should be displayed
557 # Returns the number of objects that should be displayed
557 # on the paginated list
558 # on the paginated list
558 def per_page_option
559 def per_page_option
559 per_page = nil
560 per_page = nil
560 if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i)
561 if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i)
561 per_page = params[:per_page].to_s.to_i
562 per_page = params[:per_page].to_s.to_i
562 session[:per_page] = per_page
563 session[:per_page] = per_page
563 elsif session[:per_page]
564 elsif session[:per_page]
564 per_page = session[:per_page]
565 per_page = session[:per_page]
565 else
566 else
566 per_page = Setting.per_page_options_array.first || 25
567 per_page = Setting.per_page_options_array.first || 25
567 end
568 end
568 per_page
569 per_page
569 end
570 end
570
571
571 # Returns offset and limit used to retrieve objects
572 # Returns offset and limit used to retrieve objects
572 # for an API response based on offset, limit and page parameters
573 # for an API response based on offset, limit and page parameters
573 def api_offset_and_limit(options=params)
574 def api_offset_and_limit(options=params)
574 if options[:offset].present?
575 if options[:offset].present?
575 offset = options[:offset].to_i
576 offset = options[:offset].to_i
576 if offset < 0
577 if offset < 0
577 offset = 0
578 offset = 0
578 end
579 end
579 end
580 end
580 limit = options[:limit].to_i
581 limit = options[:limit].to_i
581 if limit < 1
582 if limit < 1
582 limit = 25
583 limit = 25
583 elsif limit > 100
584 elsif limit > 100
584 limit = 100
585 limit = 100
585 end
586 end
586 if offset.nil? && options[:page].present?
587 if offset.nil? && options[:page].present?
587 offset = (options[:page].to_i - 1) * limit
588 offset = (options[:page].to_i - 1) * limit
588 offset = 0 if offset < 0
589 offset = 0 if offset < 0
589 end
590 end
590 offset ||= 0
591 offset ||= 0
591
592
592 [offset, limit]
593 [offset, limit]
593 end
594 end
594
595
595 # qvalues http header parser
596 # qvalues http header parser
596 # code taken from webrick
597 # code taken from webrick
597 def parse_qvalues(value)
598 def parse_qvalues(value)
598 tmp = []
599 tmp = []
599 if value
600 if value
600 parts = value.split(/,\s*/)
601 parts = value.split(/,\s*/)
601 parts.each {|part|
602 parts.each {|part|
602 if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part)
603 if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part)
603 val = m[1]
604 val = m[1]
604 q = (m[2] or 1).to_f
605 q = (m[2] or 1).to_f
605 tmp.push([val, q])
606 tmp.push([val, q])
606 end
607 end
607 }
608 }
608 tmp = tmp.sort_by{|val, q| -q}
609 tmp = tmp.sort_by{|val, q| -q}
609 tmp.collect!{|val, q| val}
610 tmp.collect!{|val, q| val}
610 end
611 end
611 return tmp
612 return tmp
612 rescue
613 rescue
613 nil
614 nil
614 end
615 end
615
616
616 # Returns a string that can be used as filename value in Content-Disposition header
617 # Returns a string that can be used as filename value in Content-Disposition header
617 def filename_for_content_disposition(name)
618 def filename_for_content_disposition(name)
618 request.env['HTTP_USER_AGENT'] =~ %r{(MSIE|Trident)} ? ERB::Util.url_encode(name) : name
619 request.env['HTTP_USER_AGENT'] =~ %r{(MSIE|Trident)} ? ERB::Util.url_encode(name) : name
619 end
620 end
620
621
621 def api_request?
622 def api_request?
622 %w(xml json).include? params[:format]
623 %w(xml json).include? params[:format]
623 end
624 end
624
625
625 # Returns the API key present in the request
626 # Returns the API key present in the request
626 def api_key_from_request
627 def api_key_from_request
627 if params[:key].present?
628 if params[:key].present?
628 params[:key].to_s
629 params[:key].to_s
629 elsif request.headers["X-Redmine-API-Key"].present?
630 elsif request.headers["X-Redmine-API-Key"].present?
630 request.headers["X-Redmine-API-Key"].to_s
631 request.headers["X-Redmine-API-Key"].to_s
631 end
632 end
632 end
633 end
633
634
634 # Returns the API 'switch user' value if present
635 # Returns the API 'switch user' value if present
635 def api_switch_user_from_request
636 def api_switch_user_from_request
636 request.headers["X-Redmine-Switch-User"].to_s.presence
637 request.headers["X-Redmine-Switch-User"].to_s.presence
637 end
638 end
638
639
639 # Renders a warning flash if obj has unsaved attachments
640 # Renders a warning flash if obj has unsaved attachments
640 def render_attachment_warning_if_needed(obj)
641 def render_attachment_warning_if_needed(obj)
641 flash[:warning] = l(:warning_attachments_not_saved, obj.unsaved_attachments.size) if obj.unsaved_attachments.present?
642 flash[:warning] = l(:warning_attachments_not_saved, obj.unsaved_attachments.size) if obj.unsaved_attachments.present?
642 end
643 end
643
644
644 # Rescues an invalid query statement. Just in case...
645 # Rescues an invalid query statement. Just in case...
645 def query_statement_invalid(exception)
646 def query_statement_invalid(exception)
646 logger.error "Query::StatementInvalid: #{exception.message}" if logger
647 logger.error "Query::StatementInvalid: #{exception.message}" if logger
647 session.delete(:query)
648 session.delete(:query)
648 sort_clear if respond_to?(:sort_clear)
649 sort_clear if respond_to?(:sort_clear)
649 render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
650 render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
650 end
651 end
651
652
652 # Renders a 200 response for successfull updates or deletions via the API
653 # Renders a 200 response for successfull updates or deletions via the API
653 def render_api_ok
654 def render_api_ok
654 render_api_head :ok
655 render_api_head :ok
655 end
656 end
656
657
657 # Renders a head API response
658 # Renders a head API response
658 def render_api_head(status)
659 def render_api_head(status)
659 # #head would return a response body with one space
660 # #head would return a response body with one space
660 render :text => '', :status => status, :layout => nil
661 render :text => '', :status => status, :layout => nil
661 end
662 end
662
663
663 # Renders API response on validation failure
664 # Renders API response on validation failure
664 # for an object or an array of objects
665 # for an object or an array of objects
665 def render_validation_errors(objects)
666 def render_validation_errors(objects)
666 messages = Array.wrap(objects).map {|object| object.errors.full_messages}.flatten
667 messages = Array.wrap(objects).map {|object| object.errors.full_messages}.flatten
667 render_api_errors(messages)
668 render_api_errors(messages)
668 end
669 end
669
670
670 def render_api_errors(*messages)
671 def render_api_errors(*messages)
671 @error_messages = messages.flatten
672 @error_messages = messages.flatten
672 render :template => 'common/error_messages.api', :status => :unprocessable_entity, :layout => nil
673 render :template => 'common/error_messages.api', :status => :unprocessable_entity, :layout => nil
673 end
674 end
674
675
675 # Overrides #_include_layout? so that #render with no arguments
676 # Overrides #_include_layout? so that #render with no arguments
676 # doesn't use the layout for api requests
677 # doesn't use the layout for api requests
677 def _include_layout?(*args)
678 def _include_layout?(*args)
678 api_request? ? false : super
679 api_request? ? false : super
679 end
680 end
680 end
681 end
@@ -1,1335 +1,1336
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 require 'forwardable'
20 require 'forwardable'
21 require 'cgi'
21 require 'cgi'
22
22
23 module ApplicationHelper
23 module ApplicationHelper
24 include Redmine::WikiFormatting::Macros::Definitions
24 include Redmine::WikiFormatting::Macros::Definitions
25 include Redmine::I18n
25 include Redmine::I18n
26 include GravatarHelper::PublicMethods
26 include GravatarHelper::PublicMethods
27 include Redmine::Pagination::Helper
27 include Redmine::Pagination::Helper
28 include Redmine::SudoMode::Helper
28 include Redmine::SudoMode::Helper
29 include Redmine::Hook::Helper
29
30
30 extend Forwardable
31 extend Forwardable
31 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
32 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
32
33
33 # Return true if user is authorized for controller/action, otherwise false
34 # Return true if user is authorized for controller/action, otherwise false
34 def authorize_for(controller, action)
35 def authorize_for(controller, action)
35 User.current.allowed_to?({:controller => controller, :action => action}, @project)
36 User.current.allowed_to?({:controller => controller, :action => action}, @project)
36 end
37 end
37
38
38 # Display a link if user is authorized
39 # Display a link if user is authorized
39 #
40 #
40 # @param [String] name Anchor text (passed to link_to)
41 # @param [String] name Anchor text (passed to link_to)
41 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
42 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
42 # @param [optional, Hash] html_options Options passed to link_to
43 # @param [optional, Hash] html_options Options passed to link_to
43 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
44 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
44 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
45 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
45 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
46 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
46 end
47 end
47
48
48 # Displays a link to user's account page if active
49 # Displays a link to user's account page if active
49 def link_to_user(user, options={})
50 def link_to_user(user, options={})
50 if user.is_a?(User)
51 if user.is_a?(User)
51 name = h(user.name(options[:format]))
52 name = h(user.name(options[:format]))
52 if user.active? || (User.current.admin? && user.logged?)
53 if user.active? || (User.current.admin? && user.logged?)
53 link_to name, user_path(user), :class => user.css_classes
54 link_to name, user_path(user), :class => user.css_classes
54 else
55 else
55 name
56 name
56 end
57 end
57 else
58 else
58 h(user.to_s)
59 h(user.to_s)
59 end
60 end
60 end
61 end
61
62
62 # Displays a link to +issue+ with its subject.
63 # Displays a link to +issue+ with its subject.
63 # Examples:
64 # Examples:
64 #
65 #
65 # link_to_issue(issue) # => Defect #6: This is the subject
66 # link_to_issue(issue) # => Defect #6: This is the subject
66 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
67 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
67 # link_to_issue(issue, :subject => false) # => Defect #6
68 # link_to_issue(issue, :subject => false) # => Defect #6
68 # link_to_issue(issue, :project => true) # => Foo - Defect #6
69 # link_to_issue(issue, :project => true) # => Foo - Defect #6
69 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
70 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
70 #
71 #
71 def link_to_issue(issue, options={})
72 def link_to_issue(issue, options={})
72 title = nil
73 title = nil
73 subject = nil
74 subject = nil
74 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
75 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
75 if options[:subject] == false
76 if options[:subject] == false
76 title = issue.subject.truncate(60)
77 title = issue.subject.truncate(60)
77 else
78 else
78 subject = issue.subject
79 subject = issue.subject
79 if truncate_length = options[:truncate]
80 if truncate_length = options[:truncate]
80 subject = subject.truncate(truncate_length)
81 subject = subject.truncate(truncate_length)
81 end
82 end
82 end
83 end
83 only_path = options[:only_path].nil? ? true : options[:only_path]
84 only_path = options[:only_path].nil? ? true : options[:only_path]
84 s = link_to(text, issue_url(issue, :only_path => only_path),
85 s = link_to(text, issue_url(issue, :only_path => only_path),
85 :class => issue.css_classes, :title => title)
86 :class => issue.css_classes, :title => title)
86 s << h(": #{subject}") if subject
87 s << h(": #{subject}") if subject
87 s = h("#{issue.project} - ") + s if options[:project]
88 s = h("#{issue.project} - ") + s if options[:project]
88 s
89 s
89 end
90 end
90
91
91 # Generates a link to an attachment.
92 # Generates a link to an attachment.
92 # Options:
93 # Options:
93 # * :text - Link text (default to attachment filename)
94 # * :text - Link text (default to attachment filename)
94 # * :download - Force download (default: false)
95 # * :download - Force download (default: false)
95 def link_to_attachment(attachment, options={})
96 def link_to_attachment(attachment, options={})
96 text = options.delete(:text) || attachment.filename
97 text = options.delete(:text) || attachment.filename
97 route_method = options.delete(:download) ? :download_named_attachment_url : :named_attachment_url
98 route_method = options.delete(:download) ? :download_named_attachment_url : :named_attachment_url
98 html_options = options.slice!(:only_path)
99 html_options = options.slice!(:only_path)
99 options[:only_path] = true unless options.key?(:only_path)
100 options[:only_path] = true unless options.key?(:only_path)
100 url = send(route_method, attachment, attachment.filename, options)
101 url = send(route_method, attachment, attachment.filename, options)
101 link_to text, url, html_options
102 link_to text, url, html_options
102 end
103 end
103
104
104 # Generates a link to a SCM revision
105 # Generates a link to a SCM revision
105 # Options:
106 # Options:
106 # * :text - Link text (default to the formatted revision)
107 # * :text - Link text (default to the formatted revision)
107 def link_to_revision(revision, repository, options={})
108 def link_to_revision(revision, repository, options={})
108 if repository.is_a?(Project)
109 if repository.is_a?(Project)
109 repository = repository.repository
110 repository = repository.repository
110 end
111 end
111 text = options.delete(:text) || format_revision(revision)
112 text = options.delete(:text) || format_revision(revision)
112 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
113 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
113 link_to(
114 link_to(
114 h(text),
115 h(text),
115 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
116 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
116 :title => l(:label_revision_id, format_revision(revision)),
117 :title => l(:label_revision_id, format_revision(revision)),
117 :accesskey => options[:accesskey]
118 :accesskey => options[:accesskey]
118 )
119 )
119 end
120 end
120
121
121 # Generates a link to a message
122 # Generates a link to a message
122 def link_to_message(message, options={}, html_options = nil)
123 def link_to_message(message, options={}, html_options = nil)
123 link_to(
124 link_to(
124 message.subject.truncate(60),
125 message.subject.truncate(60),
125 board_message_url(message.board_id, message.parent_id || message.id, {
126 board_message_url(message.board_id, message.parent_id || message.id, {
126 :r => (message.parent_id && message.id),
127 :r => (message.parent_id && message.id),
127 :anchor => (message.parent_id ? "message-#{message.id}" : nil),
128 :anchor => (message.parent_id ? "message-#{message.id}" : nil),
128 :only_path => true
129 :only_path => true
129 }.merge(options)),
130 }.merge(options)),
130 html_options
131 html_options
131 )
132 )
132 end
133 end
133
134
134 # Generates a link to a project if active
135 # Generates a link to a project if active
135 # Examples:
136 # Examples:
136 #
137 #
137 # link_to_project(project) # => link to the specified project overview
138 # link_to_project(project) # => link to the specified project overview
138 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
139 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
139 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
140 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
140 #
141 #
141 def link_to_project(project, options={}, html_options = nil)
142 def link_to_project(project, options={}, html_options = nil)
142 if project.archived?
143 if project.archived?
143 h(project.name)
144 h(project.name)
144 else
145 else
145 link_to project.name,
146 link_to project.name,
146 project_url(project, {:only_path => true}.merge(options)),
147 project_url(project, {:only_path => true}.merge(options)),
147 html_options
148 html_options
148 end
149 end
149 end
150 end
150
151
151 # Generates a link to a project settings if active
152 # Generates a link to a project settings if active
152 def link_to_project_settings(project, options={}, html_options=nil)
153 def link_to_project_settings(project, options={}, html_options=nil)
153 if project.active?
154 if project.active?
154 link_to project.name, settings_project_path(project, options), html_options
155 link_to project.name, settings_project_path(project, options), html_options
155 elsif project.archived?
156 elsif project.archived?
156 h(project.name)
157 h(project.name)
157 else
158 else
158 link_to project.name, project_path(project, options), html_options
159 link_to project.name, project_path(project, options), html_options
159 end
160 end
160 end
161 end
161
162
162 # Generates a link to a version
163 # Generates a link to a version
163 def link_to_version(version, options = {})
164 def link_to_version(version, options = {})
164 return '' unless version && version.is_a?(Version)
165 return '' unless version && version.is_a?(Version)
165 options = {:title => format_date(version.effective_date)}.merge(options)
166 options = {:title => format_date(version.effective_date)}.merge(options)
166 link_to_if version.visible?, format_version_name(version), version_path(version), options
167 link_to_if version.visible?, format_version_name(version), version_path(version), options
167 end
168 end
168
169
169 # Helper that formats object for html or text rendering
170 # Helper that formats object for html or text rendering
170 def format_object(object, html=true, &block)
171 def format_object(object, html=true, &block)
171 if block_given?
172 if block_given?
172 object = yield object
173 object = yield object
173 end
174 end
174 case object.class.name
175 case object.class.name
175 when 'Array'
176 when 'Array'
176 object.map {|o| format_object(o, html)}.join(', ').html_safe
177 object.map {|o| format_object(o, html)}.join(', ').html_safe
177 when 'Time'
178 when 'Time'
178 format_time(object)
179 format_time(object)
179 when 'Date'
180 when 'Date'
180 format_date(object)
181 format_date(object)
181 when 'Fixnum'
182 when 'Fixnum'
182 object.to_s
183 object.to_s
183 when 'Float'
184 when 'Float'
184 sprintf "%.2f", object
185 sprintf "%.2f", object
185 when 'User'
186 when 'User'
186 html ? link_to_user(object) : object.to_s
187 html ? link_to_user(object) : object.to_s
187 when 'Project'
188 when 'Project'
188 html ? link_to_project(object) : object.to_s
189 html ? link_to_project(object) : object.to_s
189 when 'Version'
190 when 'Version'
190 html ? link_to_version(object) : object.to_s
191 html ? link_to_version(object) : object.to_s
191 when 'TrueClass'
192 when 'TrueClass'
192 l(:general_text_Yes)
193 l(:general_text_Yes)
193 when 'FalseClass'
194 when 'FalseClass'
194 l(:general_text_No)
195 l(:general_text_No)
195 when 'Issue'
196 when 'Issue'
196 object.visible? && html ? link_to_issue(object) : "##{object.id}"
197 object.visible? && html ? link_to_issue(object) : "##{object.id}"
197 when 'CustomValue', 'CustomFieldValue'
198 when 'CustomValue', 'CustomFieldValue'
198 if object.custom_field
199 if object.custom_field
199 f = object.custom_field.format.formatted_custom_value(self, object, html)
200 f = object.custom_field.format.formatted_custom_value(self, object, html)
200 if f.nil? || f.is_a?(String)
201 if f.nil? || f.is_a?(String)
201 f
202 f
202 else
203 else
203 format_object(f, html, &block)
204 format_object(f, html, &block)
204 end
205 end
205 else
206 else
206 object.value.to_s
207 object.value.to_s
207 end
208 end
208 else
209 else
209 html ? h(object) : object.to_s
210 html ? h(object) : object.to_s
210 end
211 end
211 end
212 end
212
213
213 def wiki_page_path(page, options={})
214 def wiki_page_path(page, options={})
214 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
215 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
215 end
216 end
216
217
217 def thumbnail_tag(attachment)
218 def thumbnail_tag(attachment)
218 link_to image_tag(thumbnail_path(attachment)),
219 link_to image_tag(thumbnail_path(attachment)),
219 named_attachment_path(attachment, attachment.filename),
220 named_attachment_path(attachment, attachment.filename),
220 :title => attachment.filename
221 :title => attachment.filename
221 end
222 end
222
223
223 def toggle_link(name, id, options={})
224 def toggle_link(name, id, options={})
224 onclick = "$('##{id}').toggle(); "
225 onclick = "$('##{id}').toggle(); "
225 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
226 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
226 onclick << "return false;"
227 onclick << "return false;"
227 link_to(name, "#", :onclick => onclick)
228 link_to(name, "#", :onclick => onclick)
228 end
229 end
229
230
230 def format_activity_title(text)
231 def format_activity_title(text)
231 h(truncate_single_line_raw(text, 100))
232 h(truncate_single_line_raw(text, 100))
232 end
233 end
233
234
234 def format_activity_day(date)
235 def format_activity_day(date)
235 date == User.current.today ? l(:label_today).titleize : format_date(date)
236 date == User.current.today ? l(:label_today).titleize : format_date(date)
236 end
237 end
237
238
238 def format_activity_description(text)
239 def format_activity_description(text)
239 h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
240 h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
240 ).gsub(/[\r\n]+/, "<br />").html_safe
241 ).gsub(/[\r\n]+/, "<br />").html_safe
241 end
242 end
242
243
243 def format_version_name(version)
244 def format_version_name(version)
244 if version.project == @project
245 if version.project == @project
245 h(version)
246 h(version)
246 else
247 else
247 h("#{version.project} - #{version}")
248 h("#{version.project} - #{version}")
248 end
249 end
249 end
250 end
250
251
251 def due_date_distance_in_words(date)
252 def due_date_distance_in_words(date)
252 if date
253 if date
253 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
254 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
254 end
255 end
255 end
256 end
256
257
257 # Renders a tree of projects as a nested set of unordered lists
258 # Renders a tree of projects as a nested set of unordered lists
258 # The given collection may be a subset of the whole project tree
259 # The given collection may be a subset of the whole project tree
259 # (eg. some intermediate nodes are private and can not be seen)
260 # (eg. some intermediate nodes are private and can not be seen)
260 def render_project_nested_lists(projects, &block)
261 def render_project_nested_lists(projects, &block)
261 s = ''
262 s = ''
262 if projects.any?
263 if projects.any?
263 ancestors = []
264 ancestors = []
264 original_project = @project
265 original_project = @project
265 projects.sort_by(&:lft).each do |project|
266 projects.sort_by(&:lft).each do |project|
266 # set the project environment to please macros.
267 # set the project environment to please macros.
267 @project = project
268 @project = project
268 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
269 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
269 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
270 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
270 else
271 else
271 ancestors.pop
272 ancestors.pop
272 s << "</li>"
273 s << "</li>"
273 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
274 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
274 ancestors.pop
275 ancestors.pop
275 s << "</ul></li>\n"
276 s << "</ul></li>\n"
276 end
277 end
277 end
278 end
278 classes = (ancestors.empty? ? 'root' : 'child')
279 classes = (ancestors.empty? ? 'root' : 'child')
279 s << "<li class='#{classes}'><div class='#{classes}'>"
280 s << "<li class='#{classes}'><div class='#{classes}'>"
280 s << h(block_given? ? capture(project, &block) : project.name)
281 s << h(block_given? ? capture(project, &block) : project.name)
281 s << "</div>\n"
282 s << "</div>\n"
282 ancestors << project
283 ancestors << project
283 end
284 end
284 s << ("</li></ul>\n" * ancestors.size)
285 s << ("</li></ul>\n" * ancestors.size)
285 @project = original_project
286 @project = original_project
286 end
287 end
287 s.html_safe
288 s.html_safe
288 end
289 end
289
290
290 def render_page_hierarchy(pages, node=nil, options={})
291 def render_page_hierarchy(pages, node=nil, options={})
291 content = ''
292 content = ''
292 if pages[node]
293 if pages[node]
293 content << "<ul class=\"pages-hierarchy\">\n"
294 content << "<ul class=\"pages-hierarchy\">\n"
294 pages[node].each do |page|
295 pages[node].each do |page|
295 content << "<li>"
296 content << "<li>"
296 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
297 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
297 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
298 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
298 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
299 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
299 content << "</li>\n"
300 content << "</li>\n"
300 end
301 end
301 content << "</ul>\n"
302 content << "</ul>\n"
302 end
303 end
303 content.html_safe
304 content.html_safe
304 end
305 end
305
306
306 # Renders flash messages
307 # Renders flash messages
307 def render_flash_messages
308 def render_flash_messages
308 s = ''
309 s = ''
309 flash.each do |k,v|
310 flash.each do |k,v|
310 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
311 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
311 end
312 end
312 s.html_safe
313 s.html_safe
313 end
314 end
314
315
315 # Renders tabs and their content
316 # Renders tabs and their content
316 def render_tabs(tabs, selected=params[:tab])
317 def render_tabs(tabs, selected=params[:tab])
317 if tabs.any?
318 if tabs.any?
318 unless tabs.detect {|tab| tab[:name] == selected}
319 unless tabs.detect {|tab| tab[:name] == selected}
319 selected = nil
320 selected = nil
320 end
321 end
321 selected ||= tabs.first[:name]
322 selected ||= tabs.first[:name]
322 render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected}
323 render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected}
323 else
324 else
324 content_tag 'p', l(:label_no_data), :class => "nodata"
325 content_tag 'p', l(:label_no_data), :class => "nodata"
325 end
326 end
326 end
327 end
327
328
328 # Renders the project quick-jump box
329 # Renders the project quick-jump box
329 def render_project_jump_box
330 def render_project_jump_box
330 return unless User.current.logged?
331 return unless User.current.logged?
331 projects = User.current.projects.active.select(:id, :name, :identifier, :lft, :rgt).to_a
332 projects = User.current.projects.active.select(:id, :name, :identifier, :lft, :rgt).to_a
332 if projects.any?
333 if projects.any?
333 options =
334 options =
334 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
335 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
335 '<option value="" disabled="disabled">---</option>').html_safe
336 '<option value="" disabled="disabled">---</option>').html_safe
336
337
337 options << project_tree_options_for_select(projects, :selected => @project) do |p|
338 options << project_tree_options_for_select(projects, :selected => @project) do |p|
338 { :value => project_path(:id => p, :jump => current_menu_item) }
339 { :value => project_path(:id => p, :jump => current_menu_item) }
339 end
340 end
340
341
341 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
342 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
342 end
343 end
343 end
344 end
344
345
345 def project_tree_options_for_select(projects, options = {})
346 def project_tree_options_for_select(projects, options = {})
346 s = ''.html_safe
347 s = ''.html_safe
347 if blank_text = options[:include_blank]
348 if blank_text = options[:include_blank]
348 if blank_text == true
349 if blank_text == true
349 blank_text = '&nbsp;'.html_safe
350 blank_text = '&nbsp;'.html_safe
350 end
351 end
351 s << content_tag('option', blank_text, :value => '')
352 s << content_tag('option', blank_text, :value => '')
352 end
353 end
353 project_tree(projects) do |project, level|
354 project_tree(projects) do |project, level|
354 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
355 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
355 tag_options = {:value => project.id}
356 tag_options = {:value => project.id}
356 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
357 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
357 tag_options[:selected] = 'selected'
358 tag_options[:selected] = 'selected'
358 else
359 else
359 tag_options[:selected] = nil
360 tag_options[:selected] = nil
360 end
361 end
361 tag_options.merge!(yield(project)) if block_given?
362 tag_options.merge!(yield(project)) if block_given?
362 s << content_tag('option', name_prefix + h(project), tag_options)
363 s << content_tag('option', name_prefix + h(project), tag_options)
363 end
364 end
364 s.html_safe
365 s.html_safe
365 end
366 end
366
367
367 # Yields the given block for each project with its level in the tree
368 # Yields the given block for each project with its level in the tree
368 #
369 #
369 # Wrapper for Project#project_tree
370 # Wrapper for Project#project_tree
370 def project_tree(projects, &block)
371 def project_tree(projects, &block)
371 Project.project_tree(projects, &block)
372 Project.project_tree(projects, &block)
372 end
373 end
373
374
374 def principals_check_box_tags(name, principals)
375 def principals_check_box_tags(name, principals)
375 s = ''
376 s = ''
376 principals.each do |principal|
377 principals.each do |principal|
377 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
378 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
378 end
379 end
379 s.html_safe
380 s.html_safe
380 end
381 end
381
382
382 # Returns a string for users/groups option tags
383 # Returns a string for users/groups option tags
383 def principals_options_for_select(collection, selected=nil)
384 def principals_options_for_select(collection, selected=nil)
384 s = ''
385 s = ''
385 if collection.include?(User.current)
386 if collection.include?(User.current)
386 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
387 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
387 end
388 end
388 groups = ''
389 groups = ''
389 collection.sort.each do |element|
390 collection.sort.each do |element|
390 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
391 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
391 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
392 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
392 end
393 end
393 unless groups.empty?
394 unless groups.empty?
394 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
395 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
395 end
396 end
396 s.html_safe
397 s.html_safe
397 end
398 end
398
399
399 def option_tag(name, text, value, selected=nil, options={})
400 def option_tag(name, text, value, selected=nil, options={})
400 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
401 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
401 end
402 end
402
403
403 def truncate_single_line_raw(string, length)
404 def truncate_single_line_raw(string, length)
404 string.to_s.truncate(length).gsub(%r{[\r\n]+}m, ' ')
405 string.to_s.truncate(length).gsub(%r{[\r\n]+}m, ' ')
405 end
406 end
406
407
407 # Truncates at line break after 250 characters or options[:length]
408 # Truncates at line break after 250 characters or options[:length]
408 def truncate_lines(string, options={})
409 def truncate_lines(string, options={})
409 length = options[:length] || 250
410 length = options[:length] || 250
410 if string.to_s =~ /\A(.{#{length}}.*?)$/m
411 if string.to_s =~ /\A(.{#{length}}.*?)$/m
411 "#{$1}..."
412 "#{$1}..."
412 else
413 else
413 string
414 string
414 end
415 end
415 end
416 end
416
417
417 def anchor(text)
418 def anchor(text)
418 text.to_s.gsub(' ', '_')
419 text.to_s.gsub(' ', '_')
419 end
420 end
420
421
421 def html_hours(text)
422 def html_hours(text)
422 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
423 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
423 end
424 end
424
425
425 def authoring(created, author, options={})
426 def authoring(created, author, options={})
426 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
427 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
427 end
428 end
428
429
429 def time_tag(time)
430 def time_tag(time)
430 text = distance_of_time_in_words(Time.now, time)
431 text = distance_of_time_in_words(Time.now, time)
431 if @project
432 if @project
432 link_to(text, project_activity_path(@project, :from => User.current.time_to_date(time)), :title => format_time(time))
433 link_to(text, project_activity_path(@project, :from => User.current.time_to_date(time)), :title => format_time(time))
433 else
434 else
434 content_tag('abbr', text, :title => format_time(time))
435 content_tag('abbr', text, :title => format_time(time))
435 end
436 end
436 end
437 end
437
438
438 def syntax_highlight_lines(name, content)
439 def syntax_highlight_lines(name, content)
439 lines = []
440 lines = []
440 syntax_highlight(name, content).each_line { |line| lines << line }
441 syntax_highlight(name, content).each_line { |line| lines << line }
441 lines
442 lines
442 end
443 end
443
444
444 def syntax_highlight(name, content)
445 def syntax_highlight(name, content)
445 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
446 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
446 end
447 end
447
448
448 def to_path_param(path)
449 def to_path_param(path)
449 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
450 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
450 str.blank? ? nil : str
451 str.blank? ? nil : str
451 end
452 end
452
453
453 def reorder_links(name, url, method = :post)
454 def reorder_links(name, url, method = :post)
454 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
455 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
455 url.merge({"#{name}[move_to]" => 'highest'}),
456 url.merge({"#{name}[move_to]" => 'highest'}),
456 :method => method, :title => l(:label_sort_highest)) +
457 :method => method, :title => l(:label_sort_highest)) +
457 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
458 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
458 url.merge({"#{name}[move_to]" => 'higher'}),
459 url.merge({"#{name}[move_to]" => 'higher'}),
459 :method => method, :title => l(:label_sort_higher)) +
460 :method => method, :title => l(:label_sort_higher)) +
460 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
461 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
461 url.merge({"#{name}[move_to]" => 'lower'}),
462 url.merge({"#{name}[move_to]" => 'lower'}),
462 :method => method, :title => l(:label_sort_lower)) +
463 :method => method, :title => l(:label_sort_lower)) +
463 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
464 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
464 url.merge({"#{name}[move_to]" => 'lowest'}),
465 url.merge({"#{name}[move_to]" => 'lowest'}),
465 :method => method, :title => l(:label_sort_lowest))
466 :method => method, :title => l(:label_sort_lowest))
466 end
467 end
467
468
468 def breadcrumb(*args)
469 def breadcrumb(*args)
469 elements = args.flatten
470 elements = args.flatten
470 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
471 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
471 end
472 end
472
473
473 def other_formats_links(&block)
474 def other_formats_links(&block)
474 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
475 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
475 yield Redmine::Views::OtherFormatsBuilder.new(self)
476 yield Redmine::Views::OtherFormatsBuilder.new(self)
476 concat('</p>'.html_safe)
477 concat('</p>'.html_safe)
477 end
478 end
478
479
479 def page_header_title
480 def page_header_title
480 if @project.nil? || @project.new_record?
481 if @project.nil? || @project.new_record?
481 h(Setting.app_title)
482 h(Setting.app_title)
482 else
483 else
483 b = []
484 b = []
484 ancestors = (@project.root? ? [] : @project.ancestors.visible.to_a)
485 ancestors = (@project.root? ? [] : @project.ancestors.visible.to_a)
485 if ancestors.any?
486 if ancestors.any?
486 root = ancestors.shift
487 root = ancestors.shift
487 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
488 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
488 if ancestors.size > 2
489 if ancestors.size > 2
489 b << "\xe2\x80\xa6"
490 b << "\xe2\x80\xa6"
490 ancestors = ancestors[-2, 2]
491 ancestors = ancestors[-2, 2]
491 end
492 end
492 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
493 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
493 end
494 end
494 b << h(@project)
495 b << h(@project)
495 b.join(" \xc2\xbb ").html_safe
496 b.join(" \xc2\xbb ").html_safe
496 end
497 end
497 end
498 end
498
499
499 # Returns a h2 tag and sets the html title with the given arguments
500 # Returns a h2 tag and sets the html title with the given arguments
500 def title(*args)
501 def title(*args)
501 strings = args.map do |arg|
502 strings = args.map do |arg|
502 if arg.is_a?(Array) && arg.size >= 2
503 if arg.is_a?(Array) && arg.size >= 2
503 link_to(*arg)
504 link_to(*arg)
504 else
505 else
505 h(arg.to_s)
506 h(arg.to_s)
506 end
507 end
507 end
508 end
508 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
509 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
509 content_tag('h2', strings.join(' &#187; ').html_safe)
510 content_tag('h2', strings.join(' &#187; ').html_safe)
510 end
511 end
511
512
512 # Sets the html title
513 # Sets the html title
513 # Returns the html title when called without arguments
514 # Returns the html title when called without arguments
514 # Current project name and app_title and automatically appended
515 # Current project name and app_title and automatically appended
515 # Exemples:
516 # Exemples:
516 # html_title 'Foo', 'Bar'
517 # html_title 'Foo', 'Bar'
517 # html_title # => 'Foo - Bar - My Project - Redmine'
518 # html_title # => 'Foo - Bar - My Project - Redmine'
518 def html_title(*args)
519 def html_title(*args)
519 if args.empty?
520 if args.empty?
520 title = @html_title || []
521 title = @html_title || []
521 title << @project.name if @project
522 title << @project.name if @project
522 title << Setting.app_title unless Setting.app_title == title.last
523 title << Setting.app_title unless Setting.app_title == title.last
523 title.reject(&:blank?).join(' - ')
524 title.reject(&:blank?).join(' - ')
524 else
525 else
525 @html_title ||= []
526 @html_title ||= []
526 @html_title += args
527 @html_title += args
527 end
528 end
528 end
529 end
529
530
530 # Returns the theme, controller name, and action as css classes for the
531 # Returns the theme, controller name, and action as css classes for the
531 # HTML body.
532 # HTML body.
532 def body_css_classes
533 def body_css_classes
533 css = []
534 css = []
534 if theme = Redmine::Themes.theme(Setting.ui_theme)
535 if theme = Redmine::Themes.theme(Setting.ui_theme)
535 css << 'theme-' + theme.name
536 css << 'theme-' + theme.name
536 end
537 end
537
538
538 css << 'project-' + @project.identifier if @project && @project.identifier.present?
539 css << 'project-' + @project.identifier if @project && @project.identifier.present?
539 css << 'controller-' + controller_name
540 css << 'controller-' + controller_name
540 css << 'action-' + action_name
541 css << 'action-' + action_name
541 css.join(' ')
542 css.join(' ')
542 end
543 end
543
544
544 def accesskey(s)
545 def accesskey(s)
545 @used_accesskeys ||= []
546 @used_accesskeys ||= []
546 key = Redmine::AccessKeys.key_for(s)
547 key = Redmine::AccessKeys.key_for(s)
547 return nil if @used_accesskeys.include?(key)
548 return nil if @used_accesskeys.include?(key)
548 @used_accesskeys << key
549 @used_accesskeys << key
549 key
550 key
550 end
551 end
551
552
552 # Formats text according to system settings.
553 # Formats text according to system settings.
553 # 2 ways to call this method:
554 # 2 ways to call this method:
554 # * with a String: textilizable(text, options)
555 # * with a String: textilizable(text, options)
555 # * with an object and one of its attribute: textilizable(issue, :description, options)
556 # * with an object and one of its attribute: textilizable(issue, :description, options)
556 def textilizable(*args)
557 def textilizable(*args)
557 options = args.last.is_a?(Hash) ? args.pop : {}
558 options = args.last.is_a?(Hash) ? args.pop : {}
558 case args.size
559 case args.size
559 when 1
560 when 1
560 obj = options[:object]
561 obj = options[:object]
561 text = args.shift
562 text = args.shift
562 when 2
563 when 2
563 obj = args.shift
564 obj = args.shift
564 attr = args.shift
565 attr = args.shift
565 text = obj.send(attr).to_s
566 text = obj.send(attr).to_s
566 else
567 else
567 raise ArgumentError, 'invalid arguments to textilizable'
568 raise ArgumentError, 'invalid arguments to textilizable'
568 end
569 end
569 return '' if text.blank?
570 return '' if text.blank?
570 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
571 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
571 @only_path = only_path = options.delete(:only_path) == false ? false : true
572 @only_path = only_path = options.delete(:only_path) == false ? false : true
572
573
573 text = text.dup
574 text = text.dup
574 macros = catch_macros(text)
575 macros = catch_macros(text)
575 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
576 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
576
577
577 @parsed_headings = []
578 @parsed_headings = []
578 @heading_anchors = {}
579 @heading_anchors = {}
579 @current_section = 0 if options[:edit_section_links]
580 @current_section = 0 if options[:edit_section_links]
580
581
581 parse_sections(text, project, obj, attr, only_path, options)
582 parse_sections(text, project, obj, attr, only_path, options)
582 text = parse_non_pre_blocks(text, obj, macros) do |text|
583 text = parse_non_pre_blocks(text, obj, macros) do |text|
583 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
584 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
584 send method_name, text, project, obj, attr, only_path, options
585 send method_name, text, project, obj, attr, only_path, options
585 end
586 end
586 end
587 end
587 parse_headings(text, project, obj, attr, only_path, options)
588 parse_headings(text, project, obj, attr, only_path, options)
588
589
589 if @parsed_headings.any?
590 if @parsed_headings.any?
590 replace_toc(text, @parsed_headings)
591 replace_toc(text, @parsed_headings)
591 end
592 end
592
593
593 text.html_safe
594 text.html_safe
594 end
595 end
595
596
596 def parse_non_pre_blocks(text, obj, macros)
597 def parse_non_pre_blocks(text, obj, macros)
597 s = StringScanner.new(text)
598 s = StringScanner.new(text)
598 tags = []
599 tags = []
599 parsed = ''
600 parsed = ''
600 while !s.eos?
601 while !s.eos?
601 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
602 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
602 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
603 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
603 if tags.empty?
604 if tags.empty?
604 yield text
605 yield text
605 inject_macros(text, obj, macros) if macros.any?
606 inject_macros(text, obj, macros) if macros.any?
606 else
607 else
607 inject_macros(text, obj, macros, false) if macros.any?
608 inject_macros(text, obj, macros, false) if macros.any?
608 end
609 end
609 parsed << text
610 parsed << text
610 if tag
611 if tag
611 if closing
612 if closing
612 if tags.last && tags.last.casecmp(tag) == 0
613 if tags.last && tags.last.casecmp(tag) == 0
613 tags.pop
614 tags.pop
614 end
615 end
615 else
616 else
616 tags << tag.downcase
617 tags << tag.downcase
617 end
618 end
618 parsed << full_tag
619 parsed << full_tag
619 end
620 end
620 end
621 end
621 # Close any non closing tags
622 # Close any non closing tags
622 while tag = tags.pop
623 while tag = tags.pop
623 parsed << "</#{tag}>"
624 parsed << "</#{tag}>"
624 end
625 end
625 parsed
626 parsed
626 end
627 end
627
628
628 def parse_inline_attachments(text, project, obj, attr, only_path, options)
629 def parse_inline_attachments(text, project, obj, attr, only_path, options)
629 return if options[:inline_attachments] == false
630 return if options[:inline_attachments] == false
630
631
631 # when using an image link, try to use an attachment, if possible
632 # when using an image link, try to use an attachment, if possible
632 attachments = options[:attachments] || []
633 attachments = options[:attachments] || []
633 attachments += obj.attachments if obj.respond_to?(:attachments)
634 attachments += obj.attachments if obj.respond_to?(:attachments)
634 if attachments.present?
635 if attachments.present?
635 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
636 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
636 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
637 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
637 # search for the picture in attachments
638 # search for the picture in attachments
638 if found = Attachment.latest_attach(attachments, CGI.unescape(filename))
639 if found = Attachment.latest_attach(attachments, CGI.unescape(filename))
639 image_url = download_named_attachment_url(found, found.filename, :only_path => only_path)
640 image_url = download_named_attachment_url(found, found.filename, :only_path => only_path)
640 desc = found.description.to_s.gsub('"', '')
641 desc = found.description.to_s.gsub('"', '')
641 if !desc.blank? && alttext.blank?
642 if !desc.blank? && alttext.blank?
642 alt = " title=\"#{desc}\" alt=\"#{desc}\""
643 alt = " title=\"#{desc}\" alt=\"#{desc}\""
643 end
644 end
644 "src=\"#{image_url}\"#{alt}"
645 "src=\"#{image_url}\"#{alt}"
645 else
646 else
646 m
647 m
647 end
648 end
648 end
649 end
649 end
650 end
650 end
651 end
651
652
652 # Wiki links
653 # Wiki links
653 #
654 #
654 # Examples:
655 # Examples:
655 # [[mypage]]
656 # [[mypage]]
656 # [[mypage|mytext]]
657 # [[mypage|mytext]]
657 # wiki links can refer other project wikis, using project name or identifier:
658 # wiki links can refer other project wikis, using project name or identifier:
658 # [[project:]] -> wiki starting page
659 # [[project:]] -> wiki starting page
659 # [[project:|mytext]]
660 # [[project:|mytext]]
660 # [[project:mypage]]
661 # [[project:mypage]]
661 # [[project:mypage|mytext]]
662 # [[project:mypage|mytext]]
662 def parse_wiki_links(text, project, obj, attr, only_path, options)
663 def parse_wiki_links(text, project, obj, attr, only_path, options)
663 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
664 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
664 link_project = project
665 link_project = project
665 esc, all, page, title = $1, $2, $3, $5
666 esc, all, page, title = $1, $2, $3, $5
666 if esc.nil?
667 if esc.nil?
667 if page =~ /^([^\:]+)\:(.*)$/
668 if page =~ /^([^\:]+)\:(.*)$/
668 identifier, page = $1, $2
669 identifier, page = $1, $2
669 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
670 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
670 title ||= identifier if page.blank?
671 title ||= identifier if page.blank?
671 end
672 end
672
673
673 if link_project && link_project.wiki
674 if link_project && link_project.wiki
674 # extract anchor
675 # extract anchor
675 anchor = nil
676 anchor = nil
676 if page =~ /^(.+?)\#(.+)$/
677 if page =~ /^(.+?)\#(.+)$/
677 page, anchor = $1, $2
678 page, anchor = $1, $2
678 end
679 end
679 anchor = sanitize_anchor_name(anchor) if anchor.present?
680 anchor = sanitize_anchor_name(anchor) if anchor.present?
680 # check if page exists
681 # check if page exists
681 wiki_page = link_project.wiki.find_page(page)
682 wiki_page = link_project.wiki.find_page(page)
682 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
683 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
683 "##{anchor}"
684 "##{anchor}"
684 else
685 else
685 case options[:wiki_links]
686 case options[:wiki_links]
686 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
687 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
687 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
688 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
688 else
689 else
689 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
690 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
690 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
691 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
691 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
692 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
692 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
693 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
693 end
694 end
694 end
695 end
695 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
696 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
696 else
697 else
697 # project or wiki doesn't exist
698 # project or wiki doesn't exist
698 all
699 all
699 end
700 end
700 else
701 else
701 all
702 all
702 end
703 end
703 end
704 end
704 end
705 end
705
706
706 # Redmine links
707 # Redmine links
707 #
708 #
708 # Examples:
709 # Examples:
709 # Issues:
710 # Issues:
710 # #52 -> Link to issue #52
711 # #52 -> Link to issue #52
711 # Changesets:
712 # Changesets:
712 # r52 -> Link to revision 52
713 # r52 -> Link to revision 52
713 # commit:a85130f -> Link to scmid starting with a85130f
714 # commit:a85130f -> Link to scmid starting with a85130f
714 # Documents:
715 # Documents:
715 # document#17 -> Link to document with id 17
716 # document#17 -> Link to document with id 17
716 # document:Greetings -> Link to the document with title "Greetings"
717 # document:Greetings -> Link to the document with title "Greetings"
717 # document:"Some document" -> Link to the document with title "Some document"
718 # document:"Some document" -> Link to the document with title "Some document"
718 # Versions:
719 # Versions:
719 # version#3 -> Link to version with id 3
720 # version#3 -> Link to version with id 3
720 # version:1.0.0 -> Link to version named "1.0.0"
721 # version:1.0.0 -> Link to version named "1.0.0"
721 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
722 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
722 # Attachments:
723 # Attachments:
723 # attachment:file.zip -> Link to the attachment of the current object named file.zip
724 # attachment:file.zip -> Link to the attachment of the current object named file.zip
724 # Source files:
725 # Source files:
725 # source:some/file -> Link to the file located at /some/file in the project's repository
726 # source:some/file -> Link to the file located at /some/file in the project's repository
726 # source:some/file@52 -> Link to the file's revision 52
727 # source:some/file@52 -> Link to the file's revision 52
727 # source:some/file#L120 -> Link to line 120 of the file
728 # source:some/file#L120 -> Link to line 120 of the file
728 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
729 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
729 # export:some/file -> Force the download of the file
730 # export:some/file -> Force the download of the file
730 # Forum messages:
731 # Forum messages:
731 # message#1218 -> Link to message with id 1218
732 # message#1218 -> Link to message with id 1218
732 # Projects:
733 # Projects:
733 # project:someproject -> Link to project named "someproject"
734 # project:someproject -> Link to project named "someproject"
734 # project#3 -> Link to project with id 3
735 # project#3 -> Link to project with id 3
735 #
736 #
736 # Links can refer other objects from other projects, using project identifier:
737 # Links can refer other objects from other projects, using project identifier:
737 # identifier:r52
738 # identifier:r52
738 # identifier:document:"Some document"
739 # identifier:document:"Some document"
739 # identifier:version:1.0.0
740 # identifier:version:1.0.0
740 # identifier:source:some/file
741 # identifier:source:some/file
741 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
742 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
742 text.gsub!(%r{<a( [^>]+?)?>(.*?)</a>|([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-_]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m|
743 text.gsub!(%r{<a( [^>]+?)?>(.*?)</a>|([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-_]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m|
743 tag_content, leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $3, $4, $5, $6, $7, $12, $13, $10 || $14 || $20, $16 || $21, $17, $19
744 tag_content, leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $3, $4, $5, $6, $7, $12, $13, $10 || $14 || $20, $16 || $21, $17, $19
744 if tag_content
745 if tag_content
745 $&
746 $&
746 else
747 else
747 link = nil
748 link = nil
748 project = default_project
749 project = default_project
749 if project_identifier
750 if project_identifier
750 project = Project.visible.find_by_identifier(project_identifier)
751 project = Project.visible.find_by_identifier(project_identifier)
751 end
752 end
752 if esc.nil?
753 if esc.nil?
753 if prefix.nil? && sep == 'r'
754 if prefix.nil? && sep == 'r'
754 if project
755 if project
755 repository = nil
756 repository = nil
756 if repo_identifier
757 if repo_identifier
757 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
758 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
758 else
759 else
759 repository = project.repository
760 repository = project.repository
760 end
761 end
761 # project.changesets.visible raises an SQL error because of a double join on repositories
762 # project.changesets.visible raises an SQL error because of a double join on repositories
762 if repository &&
763 if repository &&
763 (changeset = Changeset.visible.
764 (changeset = Changeset.visible.
764 find_by_repository_id_and_revision(repository.id, identifier))
765 find_by_repository_id_and_revision(repository.id, identifier))
765 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"),
766 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"),
766 {:only_path => only_path, :controller => 'repositories',
767 {:only_path => only_path, :controller => 'repositories',
767 :action => 'revision', :id => project,
768 :action => 'revision', :id => project,
768 :repository_id => repository.identifier_param,
769 :repository_id => repository.identifier_param,
769 :rev => changeset.revision},
770 :rev => changeset.revision},
770 :class => 'changeset',
771 :class => 'changeset',
771 :title => truncate_single_line_raw(changeset.comments, 100))
772 :title => truncate_single_line_raw(changeset.comments, 100))
772 end
773 end
773 end
774 end
774 elsif sep == '#'
775 elsif sep == '#'
775 oid = identifier.to_i
776 oid = identifier.to_i
776 case prefix
777 case prefix
777 when nil
778 when nil
778 if oid.to_s == identifier &&
779 if oid.to_s == identifier &&
779 issue = Issue.visible.find_by_id(oid)
780 issue = Issue.visible.find_by_id(oid)
780 anchor = comment_id ? "note-#{comment_id}" : nil
781 anchor = comment_id ? "note-#{comment_id}" : nil
781 link = link_to("##{oid}#{comment_suffix}",
782 link = link_to("##{oid}#{comment_suffix}",
782 issue_url(issue, :only_path => only_path, :anchor => anchor),
783 issue_url(issue, :only_path => only_path, :anchor => anchor),
783 :class => issue.css_classes,
784 :class => issue.css_classes,
784 :title => "#{issue.tracker.name}: #{issue.subject.truncate(100)} (#{issue.status.name})")
785 :title => "#{issue.tracker.name}: #{issue.subject.truncate(100)} (#{issue.status.name})")
785 end
786 end
786 when 'document'
787 when 'document'
787 if document = Document.visible.find_by_id(oid)
788 if document = Document.visible.find_by_id(oid)
788 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
789 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
789 end
790 end
790 when 'version'
791 when 'version'
791 if version = Version.visible.find_by_id(oid)
792 if version = Version.visible.find_by_id(oid)
792 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
793 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
793 end
794 end
794 when 'message'
795 when 'message'
795 if message = Message.visible.find_by_id(oid)
796 if message = Message.visible.find_by_id(oid)
796 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
797 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
797 end
798 end
798 when 'forum'
799 when 'forum'
799 if board = Board.visible.find_by_id(oid)
800 if board = Board.visible.find_by_id(oid)
800 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
801 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
801 end
802 end
802 when 'news'
803 when 'news'
803 if news = News.visible.find_by_id(oid)
804 if news = News.visible.find_by_id(oid)
804 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
805 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
805 end
806 end
806 when 'project'
807 when 'project'
807 if p = Project.visible.find_by_id(oid)
808 if p = Project.visible.find_by_id(oid)
808 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
809 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
809 end
810 end
810 end
811 end
811 elsif sep == ':'
812 elsif sep == ':'
812 # removes the double quotes if any
813 # removes the double quotes if any
813 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
814 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
814 name = CGI.unescapeHTML(name)
815 name = CGI.unescapeHTML(name)
815 case prefix
816 case prefix
816 when 'document'
817 when 'document'
817 if project && document = project.documents.visible.find_by_title(name)
818 if project && document = project.documents.visible.find_by_title(name)
818 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
819 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
819 end
820 end
820 when 'version'
821 when 'version'
821 if project && version = project.versions.visible.find_by_name(name)
822 if project && version = project.versions.visible.find_by_name(name)
822 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
823 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
823 end
824 end
824 when 'forum'
825 when 'forum'
825 if project && board = project.boards.visible.find_by_name(name)
826 if project && board = project.boards.visible.find_by_name(name)
826 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
827 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
827 end
828 end
828 when 'news'
829 when 'news'
829 if project && news = project.news.visible.find_by_title(name)
830 if project && news = project.news.visible.find_by_title(name)
830 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
831 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
831 end
832 end
832 when 'commit', 'source', 'export'
833 when 'commit', 'source', 'export'
833 if project
834 if project
834 repository = nil
835 repository = nil
835 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
836 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
836 repo_prefix, repo_identifier, name = $1, $2, $3
837 repo_prefix, repo_identifier, name = $1, $2, $3
837 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
838 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
838 else
839 else
839 repository = project.repository
840 repository = project.repository
840 end
841 end
841 if prefix == 'commit'
842 if prefix == 'commit'
842 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
843 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
843 link = link_to h("#{project_prefix}#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.identifier},
844 link = link_to h("#{project_prefix}#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.identifier},
844 :class => 'changeset',
845 :class => 'changeset',
845 :title => truncate_single_line_raw(changeset.comments, 100)
846 :title => truncate_single_line_raw(changeset.comments, 100)
846 end
847 end
847 else
848 else
848 if repository && User.current.allowed_to?(:browse_repository, project)
849 if repository && User.current.allowed_to?(:browse_repository, project)
849 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
850 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
850 path, rev, anchor = $1, $3, $5
851 path, rev, anchor = $1, $3, $5
851 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
852 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
852 :path => to_path_param(path),
853 :path => to_path_param(path),
853 :rev => rev,
854 :rev => rev,
854 :anchor => anchor},
855 :anchor => anchor},
855 :class => (prefix == 'export' ? 'source download' : 'source')
856 :class => (prefix == 'export' ? 'source download' : 'source')
856 end
857 end
857 end
858 end
858 repo_prefix = nil
859 repo_prefix = nil
859 end
860 end
860 when 'attachment'
861 when 'attachment'
861 attachments = options[:attachments] || []
862 attachments = options[:attachments] || []
862 attachments += obj.attachments if obj.respond_to?(:attachments)
863 attachments += obj.attachments if obj.respond_to?(:attachments)
863 if attachments && attachment = Attachment.latest_attach(attachments, name)
864 if attachments && attachment = Attachment.latest_attach(attachments, name)
864 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
865 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
865 end
866 end
866 when 'project'
867 when 'project'
867 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
868 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
868 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
869 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
869 end
870 end
870 end
871 end
871 end
872 end
872 end
873 end
873 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
874 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
874 end
875 end
875 end
876 end
876 end
877 end
877
878
878 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
879 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
879
880
880 def parse_sections(text, project, obj, attr, only_path, options)
881 def parse_sections(text, project, obj, attr, only_path, options)
881 return unless options[:edit_section_links]
882 return unless options[:edit_section_links]
882 text.gsub!(HEADING_RE) do
883 text.gsub!(HEADING_RE) do
883 heading = $1
884 heading = $1
884 @current_section += 1
885 @current_section += 1
885 if @current_section > 1
886 if @current_section > 1
886 content_tag('div',
887 content_tag('div',
887 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
888 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
888 :class => 'contextual',
889 :class => 'contextual',
889 :title => l(:button_edit_section),
890 :title => l(:button_edit_section),
890 :id => "section-#{@current_section}") + heading.html_safe
891 :id => "section-#{@current_section}") + heading.html_safe
891 else
892 else
892 heading
893 heading
893 end
894 end
894 end
895 end
895 end
896 end
896
897
897 # Headings and TOC
898 # Headings and TOC
898 # Adds ids and links to headings unless options[:headings] is set to false
899 # Adds ids and links to headings unless options[:headings] is set to false
899 def parse_headings(text, project, obj, attr, only_path, options)
900 def parse_headings(text, project, obj, attr, only_path, options)
900 return if options[:headings] == false
901 return if options[:headings] == false
901
902
902 text.gsub!(HEADING_RE) do
903 text.gsub!(HEADING_RE) do
903 level, attrs, content = $2.to_i, $3, $4
904 level, attrs, content = $2.to_i, $3, $4
904 item = strip_tags(content).strip
905 item = strip_tags(content).strip
905 anchor = sanitize_anchor_name(item)
906 anchor = sanitize_anchor_name(item)
906 # used for single-file wiki export
907 # used for single-file wiki export
907 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
908 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
908 @heading_anchors[anchor] ||= 0
909 @heading_anchors[anchor] ||= 0
909 idx = (@heading_anchors[anchor] += 1)
910 idx = (@heading_anchors[anchor] += 1)
910 if idx > 1
911 if idx > 1
911 anchor = "#{anchor}-#{idx}"
912 anchor = "#{anchor}-#{idx}"
912 end
913 end
913 @parsed_headings << [level, anchor, item]
914 @parsed_headings << [level, anchor, item]
914 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
915 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
915 end
916 end
916 end
917 end
917
918
918 MACROS_RE = /(
919 MACROS_RE = /(
919 (!)? # escaping
920 (!)? # escaping
920 (
921 (
921 \{\{ # opening tag
922 \{\{ # opening tag
922 ([\w]+) # macro name
923 ([\w]+) # macro name
923 (\(([^\n\r]*?)\))? # optional arguments
924 (\(([^\n\r]*?)\))? # optional arguments
924 ([\n\r].*?[\n\r])? # optional block of text
925 ([\n\r].*?[\n\r])? # optional block of text
925 \}\} # closing tag
926 \}\} # closing tag
926 )
927 )
927 )/mx unless const_defined?(:MACROS_RE)
928 )/mx unless const_defined?(:MACROS_RE)
928
929
929 MACRO_SUB_RE = /(
930 MACRO_SUB_RE = /(
930 \{\{
931 \{\{
931 macro\((\d+)\)
932 macro\((\d+)\)
932 \}\}
933 \}\}
933 )/x unless const_defined?(:MACRO_SUB_RE)
934 )/x unless const_defined?(:MACRO_SUB_RE)
934
935
935 # Extracts macros from text
936 # Extracts macros from text
936 def catch_macros(text)
937 def catch_macros(text)
937 macros = {}
938 macros = {}
938 text.gsub!(MACROS_RE) do
939 text.gsub!(MACROS_RE) do
939 all, macro = $1, $4.downcase
940 all, macro = $1, $4.downcase
940 if macro_exists?(macro) || all =~ MACRO_SUB_RE
941 if macro_exists?(macro) || all =~ MACRO_SUB_RE
941 index = macros.size
942 index = macros.size
942 macros[index] = all
943 macros[index] = all
943 "{{macro(#{index})}}"
944 "{{macro(#{index})}}"
944 else
945 else
945 all
946 all
946 end
947 end
947 end
948 end
948 macros
949 macros
949 end
950 end
950
951
951 # Executes and replaces macros in text
952 # Executes and replaces macros in text
952 def inject_macros(text, obj, macros, execute=true)
953 def inject_macros(text, obj, macros, execute=true)
953 text.gsub!(MACRO_SUB_RE) do
954 text.gsub!(MACRO_SUB_RE) do
954 all, index = $1, $2.to_i
955 all, index = $1, $2.to_i
955 orig = macros.delete(index)
956 orig = macros.delete(index)
956 if execute && orig && orig =~ MACROS_RE
957 if execute && orig && orig =~ MACROS_RE
957 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
958 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
958 if esc.nil?
959 if esc.nil?
959 h(exec_macro(macro, obj, args, block) || all)
960 h(exec_macro(macro, obj, args, block) || all)
960 else
961 else
961 h(all)
962 h(all)
962 end
963 end
963 elsif orig
964 elsif orig
964 h(orig)
965 h(orig)
965 else
966 else
966 h(all)
967 h(all)
967 end
968 end
968 end
969 end
969 end
970 end
970
971
971 TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
972 TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
972
973
973 # Renders the TOC with given headings
974 # Renders the TOC with given headings
974 def replace_toc(text, headings)
975 def replace_toc(text, headings)
975 text.gsub!(TOC_RE) do
976 text.gsub!(TOC_RE) do
976 left_align, right_align = $2, $3
977 left_align, right_align = $2, $3
977 # Keep only the 4 first levels
978 # Keep only the 4 first levels
978 headings = headings.select{|level, anchor, item| level <= 4}
979 headings = headings.select{|level, anchor, item| level <= 4}
979 if headings.empty?
980 if headings.empty?
980 ''
981 ''
981 else
982 else
982 div_class = 'toc'
983 div_class = 'toc'
983 div_class << ' right' if right_align
984 div_class << ' right' if right_align
984 div_class << ' left' if left_align
985 div_class << ' left' if left_align
985 out = "<ul class=\"#{div_class}\"><li>"
986 out = "<ul class=\"#{div_class}\"><li>"
986 root = headings.map(&:first).min
987 root = headings.map(&:first).min
987 current = root
988 current = root
988 started = false
989 started = false
989 headings.each do |level, anchor, item|
990 headings.each do |level, anchor, item|
990 if level > current
991 if level > current
991 out << '<ul><li>' * (level - current)
992 out << '<ul><li>' * (level - current)
992 elsif level < current
993 elsif level < current
993 out << "</li></ul>\n" * (current - level) + "</li><li>"
994 out << "</li></ul>\n" * (current - level) + "</li><li>"
994 elsif started
995 elsif started
995 out << '</li><li>'
996 out << '</li><li>'
996 end
997 end
997 out << "<a href=\"##{anchor}\">#{item}</a>"
998 out << "<a href=\"##{anchor}\">#{item}</a>"
998 current = level
999 current = level
999 started = true
1000 started = true
1000 end
1001 end
1001 out << '</li></ul>' * (current - root)
1002 out << '</li></ul>' * (current - root)
1002 out << '</li></ul>'
1003 out << '</li></ul>'
1003 end
1004 end
1004 end
1005 end
1005 end
1006 end
1006
1007
1007 # Same as Rails' simple_format helper without using paragraphs
1008 # Same as Rails' simple_format helper without using paragraphs
1008 def simple_format_without_paragraph(text)
1009 def simple_format_without_paragraph(text)
1009 text.to_s.
1010 text.to_s.
1010 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
1011 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
1011 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
1012 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
1012 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
1013 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
1013 html_safe
1014 html_safe
1014 end
1015 end
1015
1016
1016 def lang_options_for_select(blank=true)
1017 def lang_options_for_select(blank=true)
1017 (blank ? [["(auto)", ""]] : []) + languages_options
1018 (blank ? [["(auto)", ""]] : []) + languages_options
1018 end
1019 end
1019
1020
1020 def labelled_form_for(*args, &proc)
1021 def labelled_form_for(*args, &proc)
1021 args << {} unless args.last.is_a?(Hash)
1022 args << {} unless args.last.is_a?(Hash)
1022 options = args.last
1023 options = args.last
1023 if args.first.is_a?(Symbol)
1024 if args.first.is_a?(Symbol)
1024 options.merge!(:as => args.shift)
1025 options.merge!(:as => args.shift)
1025 end
1026 end
1026 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1027 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1027 form_for(*args, &proc)
1028 form_for(*args, &proc)
1028 end
1029 end
1029
1030
1030 def labelled_fields_for(*args, &proc)
1031 def labelled_fields_for(*args, &proc)
1031 args << {} unless args.last.is_a?(Hash)
1032 args << {} unless args.last.is_a?(Hash)
1032 options = args.last
1033 options = args.last
1033 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1034 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1034 fields_for(*args, &proc)
1035 fields_for(*args, &proc)
1035 end
1036 end
1036
1037
1037 def error_messages_for(*objects)
1038 def error_messages_for(*objects)
1038 html = ""
1039 html = ""
1039 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1040 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1040 errors = objects.map {|o| o.errors.full_messages}.flatten
1041 errors = objects.map {|o| o.errors.full_messages}.flatten
1041 if errors.any?
1042 if errors.any?
1042 html << "<div id='errorExplanation'><ul>\n"
1043 html << "<div id='errorExplanation'><ul>\n"
1043 errors.each do |error|
1044 errors.each do |error|
1044 html << "<li>#{h error}</li>\n"
1045 html << "<li>#{h error}</li>\n"
1045 end
1046 end
1046 html << "</ul></div>\n"
1047 html << "</ul></div>\n"
1047 end
1048 end
1048 html.html_safe
1049 html.html_safe
1049 end
1050 end
1050
1051
1051 def delete_link(url, options={})
1052 def delete_link(url, options={})
1052 options = {
1053 options = {
1053 :method => :delete,
1054 :method => :delete,
1054 :data => {:confirm => l(:text_are_you_sure)},
1055 :data => {:confirm => l(:text_are_you_sure)},
1055 :class => 'icon icon-del'
1056 :class => 'icon icon-del'
1056 }.merge(options)
1057 }.merge(options)
1057
1058
1058 link_to l(:button_delete), url, options
1059 link_to l(:button_delete), url, options
1059 end
1060 end
1060
1061
1061 def preview_link(url, form, target='preview', options={})
1062 def preview_link(url, form, target='preview', options={})
1062 content_tag 'a', l(:label_preview), {
1063 content_tag 'a', l(:label_preview), {
1063 :href => "#",
1064 :href => "#",
1064 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1065 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1065 :accesskey => accesskey(:preview)
1066 :accesskey => accesskey(:preview)
1066 }.merge(options)
1067 }.merge(options)
1067 end
1068 end
1068
1069
1069 def link_to_function(name, function, html_options={})
1070 def link_to_function(name, function, html_options={})
1070 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1071 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1071 end
1072 end
1072
1073
1073 # Helper to render JSON in views
1074 # Helper to render JSON in views
1074 def raw_json(arg)
1075 def raw_json(arg)
1075 arg.to_json.to_s.gsub('/', '\/').html_safe
1076 arg.to_json.to_s.gsub('/', '\/').html_safe
1076 end
1077 end
1077
1078
1078 def back_url
1079 def back_url
1079 url = params[:back_url]
1080 url = params[:back_url]
1080 if url.nil? && referer = request.env['HTTP_REFERER']
1081 if url.nil? && referer = request.env['HTTP_REFERER']
1081 url = CGI.unescape(referer.to_s)
1082 url = CGI.unescape(referer.to_s)
1082 end
1083 end
1083 url
1084 url
1084 end
1085 end
1085
1086
1086 def back_url_hidden_field_tag
1087 def back_url_hidden_field_tag
1087 url = back_url
1088 url = back_url
1088 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1089 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1089 end
1090 end
1090
1091
1091 def check_all_links(form_name)
1092 def check_all_links(form_name)
1092 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1093 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1093 " | ".html_safe +
1094 " | ".html_safe +
1094 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1095 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1095 end
1096 end
1096
1097
1097 def toggle_checkboxes_link(selector)
1098 def toggle_checkboxes_link(selector)
1098 link_to_function image_tag('toggle_check.png'),
1099 link_to_function image_tag('toggle_check.png'),
1099 "toggleCheckboxesBySelector('#{selector}')",
1100 "toggleCheckboxesBySelector('#{selector}')",
1100 :title => "#{l(:button_check_all)} / #{l(:button_uncheck_all)}"
1101 :title => "#{l(:button_check_all)} / #{l(:button_uncheck_all)}"
1101 end
1102 end
1102
1103
1103 def progress_bar(pcts, options={})
1104 def progress_bar(pcts, options={})
1104 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1105 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1105 pcts = pcts.collect(&:round)
1106 pcts = pcts.collect(&:round)
1106 pcts[1] = pcts[1] - pcts[0]
1107 pcts[1] = pcts[1] - pcts[0]
1107 pcts << (100 - pcts[1] - pcts[0])
1108 pcts << (100 - pcts[1] - pcts[0])
1108 width = options[:width] || '100px;'
1109 width = options[:width] || '100px;'
1109 legend = options[:legend] || ''
1110 legend = options[:legend] || ''
1110 content_tag('table',
1111 content_tag('table',
1111 content_tag('tr',
1112 content_tag('tr',
1112 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1113 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1113 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1114 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1114 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1115 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1115 ), :class => "progress progress-#{pcts[0]}", :style => "width: #{width};").html_safe +
1116 ), :class => "progress progress-#{pcts[0]}", :style => "width: #{width};").html_safe +
1116 content_tag('p', legend, :class => 'percent').html_safe
1117 content_tag('p', legend, :class => 'percent').html_safe
1117 end
1118 end
1118
1119
1119 def checked_image(checked=true)
1120 def checked_image(checked=true)
1120 if checked
1121 if checked
1121 @checked_image_tag ||= image_tag('toggle_check.png')
1122 @checked_image_tag ||= image_tag('toggle_check.png')
1122 end
1123 end
1123 end
1124 end
1124
1125
1125 def context_menu(url)
1126 def context_menu(url)
1126 unless @context_menu_included
1127 unless @context_menu_included
1127 content_for :header_tags do
1128 content_for :header_tags do
1128 javascript_include_tag('context_menu') +
1129 javascript_include_tag('context_menu') +
1129 stylesheet_link_tag('context_menu')
1130 stylesheet_link_tag('context_menu')
1130 end
1131 end
1131 if l(:direction) == 'rtl'
1132 if l(:direction) == 'rtl'
1132 content_for :header_tags do
1133 content_for :header_tags do
1133 stylesheet_link_tag('context_menu_rtl')
1134 stylesheet_link_tag('context_menu_rtl')
1134 end
1135 end
1135 end
1136 end
1136 @context_menu_included = true
1137 @context_menu_included = true
1137 end
1138 end
1138 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1139 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1139 end
1140 end
1140
1141
1141 def calendar_for(field_id)
1142 def calendar_for(field_id)
1142 include_calendar_headers_tags
1143 include_calendar_headers_tags
1143 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1144 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1144 end
1145 end
1145
1146
1146 def include_calendar_headers_tags
1147 def include_calendar_headers_tags
1147 unless @calendar_headers_tags_included
1148 unless @calendar_headers_tags_included
1148 tags = ''.html_safe
1149 tags = ''.html_safe
1149 @calendar_headers_tags_included = true
1150 @calendar_headers_tags_included = true
1150 content_for :header_tags do
1151 content_for :header_tags do
1151 start_of_week = Setting.start_of_week
1152 start_of_week = Setting.start_of_week
1152 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1153 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1153 # Redmine uses 1..7 (monday..sunday) in settings and locales
1154 # Redmine uses 1..7 (monday..sunday) in settings and locales
1154 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1155 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1155 start_of_week = start_of_week.to_i % 7
1156 start_of_week = start_of_week.to_i % 7
1156 tags << javascript_tag(
1157 tags << javascript_tag(
1157 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1158 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1158 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1159 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1159 path_to_image('/images/calendar.png') +
1160 path_to_image('/images/calendar.png') +
1160 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1161 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1161 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1162 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1162 "beforeShow: beforeShowDatePicker};")
1163 "beforeShow: beforeShowDatePicker};")
1163 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1164 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1164 unless jquery_locale == 'en'
1165 unless jquery_locale == 'en'
1165 tags << javascript_include_tag("i18n/datepicker-#{jquery_locale}.js")
1166 tags << javascript_include_tag("i18n/datepicker-#{jquery_locale}.js")
1166 end
1167 end
1167 tags
1168 tags
1168 end
1169 end
1169 end
1170 end
1170 end
1171 end
1171
1172
1172 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1173 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1173 # Examples:
1174 # Examples:
1174 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1175 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1175 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1176 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1176 #
1177 #
1177 def stylesheet_link_tag(*sources)
1178 def stylesheet_link_tag(*sources)
1178 options = sources.last.is_a?(Hash) ? sources.pop : {}
1179 options = sources.last.is_a?(Hash) ? sources.pop : {}
1179 plugin = options.delete(:plugin)
1180 plugin = options.delete(:plugin)
1180 sources = sources.map do |source|
1181 sources = sources.map do |source|
1181 if plugin
1182 if plugin
1182 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1183 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1183 elsif current_theme && current_theme.stylesheets.include?(source)
1184 elsif current_theme && current_theme.stylesheets.include?(source)
1184 current_theme.stylesheet_path(source)
1185 current_theme.stylesheet_path(source)
1185 else
1186 else
1186 source
1187 source
1187 end
1188 end
1188 end
1189 end
1189 super *sources, options
1190 super *sources, options
1190 end
1191 end
1191
1192
1192 # Overrides Rails' image_tag with themes and plugins support.
1193 # Overrides Rails' image_tag with themes and plugins support.
1193 # Examples:
1194 # Examples:
1194 # image_tag('image.png') # => picks image.png from the current theme or defaults
1195 # image_tag('image.png') # => picks image.png from the current theme or defaults
1195 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1196 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1196 #
1197 #
1197 def image_tag(source, options={})
1198 def image_tag(source, options={})
1198 if plugin = options.delete(:plugin)
1199 if plugin = options.delete(:plugin)
1199 source = "/plugin_assets/#{plugin}/images/#{source}"
1200 source = "/plugin_assets/#{plugin}/images/#{source}"
1200 elsif current_theme && current_theme.images.include?(source)
1201 elsif current_theme && current_theme.images.include?(source)
1201 source = current_theme.image_path(source)
1202 source = current_theme.image_path(source)
1202 end
1203 end
1203 super source, options
1204 super source, options
1204 end
1205 end
1205
1206
1206 # Overrides Rails' javascript_include_tag with plugins support
1207 # Overrides Rails' javascript_include_tag with plugins support
1207 # Examples:
1208 # Examples:
1208 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1209 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1209 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1210 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1210 #
1211 #
1211 def javascript_include_tag(*sources)
1212 def javascript_include_tag(*sources)
1212 options = sources.last.is_a?(Hash) ? sources.pop : {}
1213 options = sources.last.is_a?(Hash) ? sources.pop : {}
1213 if plugin = options.delete(:plugin)
1214 if plugin = options.delete(:plugin)
1214 sources = sources.map do |source|
1215 sources = sources.map do |source|
1215 if plugin
1216 if plugin
1216 "/plugin_assets/#{plugin}/javascripts/#{source}"
1217 "/plugin_assets/#{plugin}/javascripts/#{source}"
1217 else
1218 else
1218 source
1219 source
1219 end
1220 end
1220 end
1221 end
1221 end
1222 end
1222 super *sources, options
1223 super *sources, options
1223 end
1224 end
1224
1225
1225 def sidebar_content?
1226 def sidebar_content?
1226 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1227 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1227 end
1228 end
1228
1229
1229 def view_layouts_base_sidebar_hook_response
1230 def view_layouts_base_sidebar_hook_response
1230 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1231 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1231 end
1232 end
1232
1233
1233 def email_delivery_enabled?
1234 def email_delivery_enabled?
1234 !!ActionMailer::Base.perform_deliveries
1235 !!ActionMailer::Base.perform_deliveries
1235 end
1236 end
1236
1237
1237 # Returns the avatar image tag for the given +user+ if avatars are enabled
1238 # Returns the avatar image tag for the given +user+ if avatars are enabled
1238 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1239 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1239 def avatar(user, options = { })
1240 def avatar(user, options = { })
1240 if Setting.gravatar_enabled?
1241 if Setting.gravatar_enabled?
1241 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1242 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1242 email = nil
1243 email = nil
1243 if user.respond_to?(:mail)
1244 if user.respond_to?(:mail)
1244 email = user.mail
1245 email = user.mail
1245 elsif user.to_s =~ %r{<(.+?)>}
1246 elsif user.to_s =~ %r{<(.+?)>}
1246 email = $1
1247 email = $1
1247 end
1248 end
1248 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1249 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1249 else
1250 else
1250 ''
1251 ''
1251 end
1252 end
1252 end
1253 end
1253
1254
1254 # Returns a link to edit user's avatar if avatars are enabled
1255 # Returns a link to edit user's avatar if avatars are enabled
1255 def avatar_edit_link(user, options={})
1256 def avatar_edit_link(user, options={})
1256 if Setting.gravatar_enabled?
1257 if Setting.gravatar_enabled?
1257 url = "https://gravatar.com"
1258 url = "https://gravatar.com"
1258 link_to avatar(user, {:title => l(:button_edit)}.merge(options)), url, :target => '_blank'
1259 link_to avatar(user, {:title => l(:button_edit)}.merge(options)), url, :target => '_blank'
1259 end
1260 end
1260 end
1261 end
1261
1262
1262 def sanitize_anchor_name(anchor)
1263 def sanitize_anchor_name(anchor)
1263 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1264 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1264 end
1265 end
1265
1266
1266 # Returns the javascript tags that are included in the html layout head
1267 # Returns the javascript tags that are included in the html layout head
1267 def javascript_heads
1268 def javascript_heads
1268 tags = javascript_include_tag('jquery-1.11.1-ui-1.11.0-ujs-3.1.4', 'application')
1269 tags = javascript_include_tag('jquery-1.11.1-ui-1.11.0-ujs-3.1.4', 'application')
1269 unless User.current.pref.warn_on_leaving_unsaved == '0'
1270 unless User.current.pref.warn_on_leaving_unsaved == '0'
1270 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1271 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1271 end
1272 end
1272 tags
1273 tags
1273 end
1274 end
1274
1275
1275 def favicon
1276 def favicon
1276 "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1277 "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1277 end
1278 end
1278
1279
1279 # Returns the path to the favicon
1280 # Returns the path to the favicon
1280 def favicon_path
1281 def favicon_path
1281 icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1282 icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1282 image_path(icon)
1283 image_path(icon)
1283 end
1284 end
1284
1285
1285 # Returns the full URL to the favicon
1286 # Returns the full URL to the favicon
1286 def favicon_url
1287 def favicon_url
1287 # TODO: use #image_url introduced in Rails4
1288 # TODO: use #image_url introduced in Rails4
1288 path = favicon_path
1289 path = favicon_path
1289 base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1290 base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1290 base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1291 base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1291 end
1292 end
1292
1293
1293 def robot_exclusion_tag
1294 def robot_exclusion_tag
1294 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1295 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1295 end
1296 end
1296
1297
1297 # Returns true if arg is expected in the API response
1298 # Returns true if arg is expected in the API response
1298 def include_in_api_response?(arg)
1299 def include_in_api_response?(arg)
1299 unless @included_in_api_response
1300 unless @included_in_api_response
1300 param = params[:include]
1301 param = params[:include]
1301 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1302 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1302 @included_in_api_response.collect!(&:strip)
1303 @included_in_api_response.collect!(&:strip)
1303 end
1304 end
1304 @included_in_api_response.include?(arg.to_s)
1305 @included_in_api_response.include?(arg.to_s)
1305 end
1306 end
1306
1307
1307 # Returns options or nil if nometa param or X-Redmine-Nometa header
1308 # Returns options or nil if nometa param or X-Redmine-Nometa header
1308 # was set in the request
1309 # was set in the request
1309 def api_meta(options)
1310 def api_meta(options)
1310 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1311 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1311 # compatibility mode for activeresource clients that raise
1312 # compatibility mode for activeresource clients that raise
1312 # an error when deserializing an array with attributes
1313 # an error when deserializing an array with attributes
1313 nil
1314 nil
1314 else
1315 else
1315 options
1316 options
1316 end
1317 end
1317 end
1318 end
1318
1319
1319 def generate_csv(&block)
1320 def generate_csv(&block)
1320 decimal_separator = l(:general_csv_decimal_separator)
1321 decimal_separator = l(:general_csv_decimal_separator)
1321 encoding = l(:general_csv_encoding)
1322 encoding = l(:general_csv_encoding)
1322 end
1323 end
1323
1324
1324 private
1325 private
1325
1326
1326 def wiki_helper
1327 def wiki_helper
1327 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1328 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1328 extend helper
1329 extend helper
1329 return self
1330 return self
1330 end
1331 end
1331
1332
1332 def link_to_content_update(text, url_params = {}, html_options = {})
1333 def link_to_content_update(text, url_params = {}, html_options = {})
1333 link_to(text, url_params, html_options)
1334 link_to(text, url_params, html_options)
1334 end
1335 end
1335 end
1336 end
@@ -1,175 +1,172
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 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 module Redmine
18 module Redmine
19 module Hook
19 module Hook
20 @@listener_classes = []
20 @@listener_classes = []
21 @@listeners = nil
21 @@listeners = nil
22 @@hook_listeners = {}
22 @@hook_listeners = {}
23
23
24 class << self
24 class << self
25 # Adds a listener class.
25 # Adds a listener class.
26 # Automatically called when a class inherits from Redmine::Hook::Listener.
26 # Automatically called when a class inherits from Redmine::Hook::Listener.
27 def add_listener(klass)
27 def add_listener(klass)
28 raise "Hooks must include Singleton module." unless klass.included_modules.include?(Singleton)
28 raise "Hooks must include Singleton module." unless klass.included_modules.include?(Singleton)
29 @@listener_classes << klass
29 @@listener_classes << klass
30 clear_listeners_instances
30 clear_listeners_instances
31 end
31 end
32
32
33 # Returns all the listener instances.
33 # Returns all the listener instances.
34 def listeners
34 def listeners
35 @@listeners ||= @@listener_classes.collect {|listener| listener.instance}
35 @@listeners ||= @@listener_classes.collect {|listener| listener.instance}
36 end
36 end
37
37
38 # Returns the listener instances for the given hook.
38 # Returns the listener instances for the given hook.
39 def hook_listeners(hook)
39 def hook_listeners(hook)
40 @@hook_listeners[hook] ||= listeners.select {|listener| listener.respond_to?(hook)}
40 @@hook_listeners[hook] ||= listeners.select {|listener| listener.respond_to?(hook)}
41 end
41 end
42
42
43 # Clears all the listeners.
43 # Clears all the listeners.
44 def clear_listeners
44 def clear_listeners
45 @@listener_classes = []
45 @@listener_classes = []
46 clear_listeners_instances
46 clear_listeners_instances
47 end
47 end
48
48
49 # Clears all the listeners instances.
49 # Clears all the listeners instances.
50 def clear_listeners_instances
50 def clear_listeners_instances
51 @@listeners = nil
51 @@listeners = nil
52 @@hook_listeners = {}
52 @@hook_listeners = {}
53 end
53 end
54
54
55 # Calls a hook.
55 # Calls a hook.
56 # Returns the listeners response.
56 # Returns the listeners response.
57 def call_hook(hook, context={})
57 def call_hook(hook, context={})
58 [].tap do |response|
58 [].tap do |response|
59 hls = hook_listeners(hook)
59 hls = hook_listeners(hook)
60 if hls.any?
60 if hls.any?
61 hls.each {|listener| response << listener.send(hook, context)}
61 hls.each {|listener| response << listener.send(hook, context)}
62 end
62 end
63 end
63 end
64 end
64 end
65 end
65 end
66
66
67 # Base class for hook listeners.
67 # Base class for hook listeners.
68 class Listener
68 class Listener
69 include Singleton
69 include Singleton
70 include Redmine::I18n
70 include Redmine::I18n
71
71
72 # Registers the listener
72 # Registers the listener
73 def self.inherited(child)
73 def self.inherited(child)
74 Redmine::Hook.add_listener(child)
74 Redmine::Hook.add_listener(child)
75 super
75 super
76 end
76 end
77
77
78 end
78 end
79
79
80 # Listener class used for views hooks.
80 # Listener class used for views hooks.
81 # Listeners that inherit this class will include various helpers by default.
81 # Listeners that inherit this class will include various helpers by default.
82 class ViewListener < Listener
82 class ViewListener < Listener
83 include ERB::Util
83 include ERB::Util
84 include ActionView::Helpers::TagHelper
84 include ActionView::Helpers::TagHelper
85 include ActionView::Helpers::FormHelper
85 include ActionView::Helpers::FormHelper
86 include ActionView::Helpers::FormTagHelper
86 include ActionView::Helpers::FormTagHelper
87 include ActionView::Helpers::FormOptionsHelper
87 include ActionView::Helpers::FormOptionsHelper
88 include ActionView::Helpers::JavaScriptHelper
88 include ActionView::Helpers::JavaScriptHelper
89 include ActionView::Helpers::NumberHelper
89 include ActionView::Helpers::NumberHelper
90 include ActionView::Helpers::UrlHelper
90 include ActionView::Helpers::UrlHelper
91 include ActionView::Helpers::AssetTagHelper
91 include ActionView::Helpers::AssetTagHelper
92 include ActionView::Helpers::TextHelper
92 include ActionView::Helpers::TextHelper
93 include Rails.application.routes.url_helpers
93 include Rails.application.routes.url_helpers
94 include ApplicationHelper
94 include ApplicationHelper
95
95
96 # Default to creating links using only the path. Subclasses can
96 # Default to creating links using only the path. Subclasses can
97 # change this default as needed
97 # change this default as needed
98 def self.default_url_options
98 def self.default_url_options
99 {:only_path => true, :script_name => Redmine::Utils.relative_url_root}
99 {:only_path => true, :script_name => Redmine::Utils.relative_url_root}
100 end
100 end
101
101
102 # Helper method to directly render using the context,
102 # Helper method to directly render using the context,
103 # render_options must be valid #render options.
103 # render_options must be valid #render options.
104 #
104 #
105 # class MyHook < Redmine::Hook::ViewListener
105 # class MyHook < Redmine::Hook::ViewListener
106 # render_on :view_issues_show_details_bottom, :partial => "show_more_data"
106 # render_on :view_issues_show_details_bottom, :partial => "show_more_data"
107 # end
107 # end
108 #
108 #
109 # class MultipleHook < Redmine::Hook::ViewListener
109 # class MultipleHook < Redmine::Hook::ViewListener
110 # render_on :view_issues_show_details_bottom,
110 # render_on :view_issues_show_details_bottom,
111 # {:partial => "show_more_data"},
111 # {:partial => "show_more_data"},
112 # {:partial => "show_even_more_data"}
112 # {:partial => "show_even_more_data"}
113 # end
113 # end
114 #
114 #
115 def self.render_on(hook, *render_options)
115 def self.render_on(hook, *render_options)
116 define_method hook do |context|
116 define_method hook do |context|
117 render_options.map do |options|
117 render_options.map do |options|
118 if context[:hook_caller].respond_to?(:render)
118 if context[:hook_caller].respond_to?(:render)
119 context[:hook_caller].send(:render, {:locals => context}.merge(options))
119 context[:hook_caller].send(:render, {:locals => context}.merge(options))
120 elsif context[:controller].is_a?(ActionController::Base)
120 elsif context[:controller].is_a?(ActionController::Base)
121 context[:controller].send(:render_to_string, {:locals => context}.merge(options))
121 context[:controller].send(:render_to_string, {:locals => context}.merge(options))
122 else
122 else
123 raise "Cannot render #{self.name} hook from #{context[:hook_caller].class.name}"
123 raise "Cannot render #{self.name} hook from #{context[:hook_caller].class.name}"
124 end
124 end
125 end
125 end
126 end
126 end
127 end
127 end
128
128
129 def controller
129 def controller
130 nil
130 nil
131 end
131 end
132
132
133 def config
133 def config
134 ActionController::Base.config
134 ActionController::Base.config
135 end
135 end
136 end
136 end
137
137
138 # Helper module included in ApplicationHelper and ActionController so that
138 # Helper module included in ApplicationHelper and ActionController so that
139 # hooks can be called in views like this:
139 # hooks can be called in views like this:
140 #
140 #
141 # <%= call_hook(:some_hook) %>
141 # <%= call_hook(:some_hook) %>
142 # <%= call_hook(:another_hook, :foo => 'bar') %>
142 # <%= call_hook(:another_hook, :foo => 'bar') %>
143 #
143 #
144 # Or in controllers like:
144 # Or in controllers like:
145 # call_hook(:some_hook)
145 # call_hook(:some_hook)
146 # call_hook(:another_hook, :foo => 'bar')
146 # call_hook(:another_hook, :foo => 'bar')
147 #
147 #
148 # Hooks added to views will be concatenated into a string. Hooks added to
148 # Hooks added to views will be concatenated into a string. Hooks added to
149 # controllers will return an array of results.
149 # controllers will return an array of results.
150 #
150 #
151 # Several objects are automatically added to the call context:
151 # Several objects are automatically added to the call context:
152 #
152 #
153 # * project => current project
153 # * project => current project
154 # * request => Request instance
154 # * request => Request instance
155 # * controller => current Controller instance
155 # * controller => current Controller instance
156 # * hook_caller => object that called the hook
156 # * hook_caller => object that called the hook
157 #
157 #
158 module Helper
158 module Helper
159 def call_hook(hook, context={})
159 def call_hook(hook, context={})
160 if is_a?(ActionController::Base)
160 if is_a?(ActionController::Base)
161 default_context = {:controller => self, :project => @project, :request => request, :hook_caller => self}
161 default_context = {:controller => self, :project => @project, :request => request, :hook_caller => self}
162 Redmine::Hook.call_hook(hook, default_context.merge(context))
162 Redmine::Hook.call_hook(hook, default_context.merge(context))
163 else
163 else
164 default_context = { :project => @project, :hook_caller => self }
164 default_context = { :project => @project, :hook_caller => self }
165 default_context[:controller] = controller if respond_to?(:controller)
165 default_context[:controller] = controller if respond_to?(:controller)
166 default_context[:request] = request if respond_to?(:request)
166 default_context[:request] = request if respond_to?(:request)
167 Redmine::Hook.call_hook(hook, default_context.merge(context)).join(' ').html_safe
167 Redmine::Hook.call_hook(hook, default_context.merge(context)).join(' ').html_safe
168 end
168 end
169 end
169 end
170 end
170 end
171 end
171 end
172 end
172 end
173
174 ApplicationHelper.send(:include, Redmine::Hook::Helper)
175 ActionController::Base.send(:include, Redmine::Hook::Helper)
General Comments 0
You need to be logged in to leave comments. Login now