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