##// END OF EJS Templates
Fixed escaping issues in #textilizable with Rails 3.1....
Jean-Philippe Lang -
r8865:30282f20daec
parent child
Show More
@@ -1,1106 +1,1107
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2011 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
28 28 extend Forwardable
29 29 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
30 30
31 31 # Return true if user is authorized for controller/action, otherwise false
32 32 def authorize_for(controller, action)
33 33 User.current.allowed_to?({:controller => controller, :action => action}, @project)
34 34 end
35 35
36 36 # Display a link if user is authorized
37 37 #
38 38 # @param [String] name Anchor text (passed to link_to)
39 39 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
40 40 # @param [optional, Hash] html_options Options passed to link_to
41 41 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
42 42 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
43 43 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
44 44 end
45 45
46 46 # Display a link to remote if user is authorized
47 47 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
48 48 url = options[:url] || {}
49 49 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
50 50 end
51 51
52 52 # Displays a link to user's account page if active
53 53 def link_to_user(user, options={})
54 54 if user.is_a?(User)
55 55 name = h(user.name(options[:format]))
56 56 if user.active?
57 57 link_to name, :controller => 'users', :action => 'show', :id => user
58 58 else
59 59 name
60 60 end
61 61 else
62 62 h(user.to_s)
63 63 end
64 64 end
65 65
66 66 # Displays a link to +issue+ with its subject.
67 67 # Examples:
68 68 #
69 69 # link_to_issue(issue) # => Defect #6: This is the subject
70 70 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
71 71 # link_to_issue(issue, :subject => false) # => Defect #6
72 72 # link_to_issue(issue, :project => true) # => Foo - Defect #6
73 73 #
74 74 def link_to_issue(issue, options={})
75 75 title = nil
76 76 subject = nil
77 77 if options[:subject] == false
78 78 title = truncate(issue.subject, :length => 60)
79 79 else
80 80 subject = issue.subject
81 81 if options[:truncate]
82 82 subject = truncate(subject, :length => options[:truncate])
83 83 end
84 84 end
85 85 s = link_to "#{h(issue.tracker)} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
86 86 :class => issue.css_classes,
87 87 :title => title
88 88 s << h(": #{subject}") if subject
89 89 s = h("#{issue.project} - ") + s if options[:project]
90 90 s
91 91 end
92 92
93 93 # Generates a link to an attachment.
94 94 # Options:
95 95 # * :text - Link text (default to attachment filename)
96 96 # * :download - Force download (default: false)
97 97 def link_to_attachment(attachment, options={})
98 98 text = options.delete(:text) || attachment.filename
99 99 action = options.delete(:download) ? 'download' : 'show'
100 100 link_to(h(text),
101 101 {:controller => 'attachments', :action => action,
102 102 :id => attachment, :filename => attachment.filename },
103 103 options)
104 104 end
105 105
106 106 # Generates a link to a SCM revision
107 107 # Options:
108 108 # * :text - Link text (default to the formatted revision)
109 109 def link_to_revision(revision, repository, options={})
110 110 if repository.is_a?(Project)
111 111 repository = repository.repository
112 112 end
113 113 text = options.delete(:text) || format_revision(revision)
114 114 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
115 115 link_to(
116 116 h(text),
117 117 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
118 118 :title => l(:label_revision_id, format_revision(revision))
119 119 )
120 120 end
121 121
122 122 # Generates a link to a message
123 123 def link_to_message(message, options={}, html_options = nil)
124 124 link_to(
125 125 h(truncate(message.subject, :length => 60)),
126 126 { :controller => 'messages', :action => 'show',
127 127 :board_id => message.board_id,
128 128 :id => message.root,
129 129 :r => (message.parent_id && message.id),
130 130 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
131 131 }.merge(options),
132 132 html_options
133 133 )
134 134 end
135 135
136 136 # Generates a link to a project if active
137 137 # Examples:
138 138 #
139 139 # link_to_project(project) # => link to the specified project overview
140 140 # link_to_project(project, :action=>'settings') # => link to project settings
141 141 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
142 142 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
143 143 #
144 144 def link_to_project(project, options={}, html_options = nil)
145 145 if project.active?
146 146 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
147 147 link_to(h(project), url, html_options)
148 148 else
149 149 h(project)
150 150 end
151 151 end
152 152
153 153 def toggle_link(name, id, options={})
154 154 onclick = "Element.toggle('#{id}'); "
155 155 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
156 156 onclick << "return false;"
157 157 link_to(name, "#", :onclick => onclick)
158 158 end
159 159
160 160 def image_to_function(name, function, html_options = {})
161 161 html_options.symbolize_keys!
162 162 tag(:input, html_options.merge({
163 163 :type => "image", :src => image_path(name),
164 164 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
165 165 }))
166 166 end
167 167
168 168 def prompt_to_remote(name, text, param, url, html_options = {})
169 169 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
170 170 link_to name, {}, html_options
171 171 end
172 172
173 173 def format_activity_title(text)
174 174 h(truncate_single_line(text, :length => 100))
175 175 end
176 176
177 177 def format_activity_day(date)
178 178 date == Date.today ? l(:label_today).titleize : format_date(date)
179 179 end
180 180
181 181 def format_activity_description(text)
182 182 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
183 183 ).gsub(/[\r\n]+/, "<br />").html_safe
184 184 end
185 185
186 186 def format_version_name(version)
187 187 if version.project == @project
188 188 h(version)
189 189 else
190 190 h("#{version.project} - #{version}")
191 191 end
192 192 end
193 193
194 194 def due_date_distance_in_words(date)
195 195 if date
196 196 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
197 197 end
198 198 end
199 199
200 200 def render_page_hierarchy(pages, node=nil, options={})
201 201 content = ''
202 202 if pages[node]
203 203 content << "<ul class=\"pages-hierarchy\">\n"
204 204 pages[node].each do |page|
205 205 content << "<li>"
206 206 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
207 207 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
208 208 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
209 209 content << "</li>\n"
210 210 end
211 211 content << "</ul>\n"
212 212 end
213 213 content.html_safe
214 214 end
215 215
216 216 # Renders flash messages
217 217 def render_flash_messages
218 218 s = ''
219 219 flash.each do |k,v|
220 220 s << (content_tag('div', v.html_safe, :class => "flash #{k}"))
221 221 end
222 222 s.html_safe
223 223 end
224 224
225 225 # Renders tabs and their content
226 226 def render_tabs(tabs)
227 227 if tabs.any?
228 228 render :partial => 'common/tabs', :locals => {:tabs => tabs}
229 229 else
230 230 content_tag 'p', l(:label_no_data), :class => "nodata"
231 231 end
232 232 end
233 233
234 234 # Renders the project quick-jump box
235 235 def render_project_jump_box
236 236 return unless User.current.logged?
237 237 projects = User.current.memberships.collect(&:project).compact.uniq
238 238 if projects.any?
239 239 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
240 240 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
241 241 '<option value="" disabled="disabled">---</option>'
242 242 s << project_tree_options_for_select(projects, :selected => @project) do |p|
243 243 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
244 244 end
245 245 s << '</select>'
246 246 s.html_safe
247 247 end
248 248 end
249 249
250 250 def project_tree_options_for_select(projects, options = {})
251 251 s = ''
252 252 project_tree(projects) do |project, level|
253 253 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
254 254 tag_options = {:value => project.id}
255 255 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
256 256 tag_options[:selected] = 'selected'
257 257 else
258 258 tag_options[:selected] = nil
259 259 end
260 260 tag_options.merge!(yield(project)) if block_given?
261 261 s << content_tag('option', name_prefix + h(project), tag_options)
262 262 end
263 263 s.html_safe
264 264 end
265 265
266 266 # Yields the given block for each project with its level in the tree
267 267 #
268 268 # Wrapper for Project#project_tree
269 269 def project_tree(projects, &block)
270 270 Project.project_tree(projects, &block)
271 271 end
272 272
273 273 def project_nested_ul(projects, &block)
274 274 s = ''
275 275 if projects.any?
276 276 ancestors = []
277 277 projects.sort_by(&:lft).each do |project|
278 278 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
279 279 s << "<ul>\n"
280 280 else
281 281 ancestors.pop
282 282 s << "</li>"
283 283 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
284 284 ancestors.pop
285 285 s << "</ul></li>\n"
286 286 end
287 287 end
288 288 s << "<li>"
289 289 s << yield(project).to_s
290 290 ancestors << project
291 291 end
292 292 s << ("</li></ul>\n" * ancestors.size)
293 293 end
294 294 s.html_safe
295 295 end
296 296
297 297 def principals_check_box_tags(name, principals)
298 298 s = ''
299 299 principals.sort.each do |principal|
300 300 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
301 301 end
302 302 s.html_safe
303 303 end
304 304
305 305 # Returns a string for users/groups option tags
306 306 def principals_options_for_select(collection, selected=nil)
307 307 s = ''
308 308 if collection.include?(User.current)
309 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
309 s << content_tag('option', "<< #{l(:label_me)} >>".html_safe, :value => User.current.id)
310 310 end
311 311 groups = ''
312 312 collection.sort.each do |element|
313 313 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected)
314 314 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
315 315 end
316 316 unless groups.empty?
317 317 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
318 318 end
319 319 s
320 320 end
321 321
322 322 # Truncates and returns the string as a single line
323 323 def truncate_single_line(string, *args)
324 324 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
325 325 end
326 326
327 327 # Truncates at line break after 250 characters or options[:length]
328 328 def truncate_lines(string, options={})
329 329 length = options[:length] || 250
330 330 if string.to_s =~ /\A(.{#{length}}.*?)$/m
331 331 "#{$1}..."
332 332 else
333 333 string
334 334 end
335 335 end
336 336
337 337 def anchor(text)
338 338 text.to_s.gsub(' ', '_')
339 339 end
340 340
341 341 def html_hours(text)
342 342 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
343 343 end
344 344
345 345 def authoring(created, author, options={})
346 346 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
347 347 end
348 348
349 349 def time_tag(time)
350 350 text = distance_of_time_in_words(Time.now, time)
351 351 if @project
352 352 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
353 353 else
354 354 content_tag('acronym', text, :title => format_time(time))
355 355 end
356 356 end
357 357
358 358 def syntax_highlight(name, content)
359 359 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
360 360 end
361 361
362 362 def to_path_param(path)
363 363 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
364 364 end
365 365
366 366 def pagination_links_full(paginator, count=nil, options={})
367 367 page_param = options.delete(:page_param) || :page
368 368 per_page_links = options.delete(:per_page_links)
369 369 url_param = params.dup
370 370
371 371 html = ''
372 372 if paginator.current.previous
373 373 # \xc2\xab(utf-8) = &#171;
374 374 html << link_to_content_update(
375 375 "\xc2\xab " + l(:label_previous),
376 376 url_param.merge(page_param => paginator.current.previous)) + ' '
377 377 end
378 378
379 379 html << (pagination_links_each(paginator, options) do |n|
380 380 link_to_content_update(n.to_s, url_param.merge(page_param => n))
381 381 end || '')
382 382
383 383 if paginator.current.next
384 384 # \xc2\xbb(utf-8) = &#187;
385 385 html << ' ' + link_to_content_update(
386 386 (l(:label_next) + " \xc2\xbb"),
387 387 url_param.merge(page_param => paginator.current.next))
388 388 end
389 389
390 390 unless count.nil?
391 391 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
392 392 if per_page_links != false && links = per_page_links(paginator.items_per_page)
393 393 html << " | #{links}"
394 394 end
395 395 end
396 396
397 397 html.html_safe
398 398 end
399 399
400 400 def per_page_links(selected=nil)
401 401 links = Setting.per_page_options_array.collect do |n|
402 402 n == selected ? n : link_to_content_update(n, params.merge(:per_page => n))
403 403 end
404 404 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
405 405 end
406 406
407 407 def reorder_links(name, url, method = :post)
408 408 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
409 409 url.merge({"#{name}[move_to]" => 'highest'}),
410 410 :method => method, :title => l(:label_sort_highest)) +
411 411 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
412 412 url.merge({"#{name}[move_to]" => 'higher'}),
413 413 :method => method, :title => l(:label_sort_higher)) +
414 414 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
415 415 url.merge({"#{name}[move_to]" => 'lower'}),
416 416 :method => method, :title => l(:label_sort_lower)) +
417 417 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
418 418 url.merge({"#{name}[move_to]" => 'lowest'}),
419 419 :method => method, :title => l(:label_sort_lowest))
420 420 end
421 421
422 422 def breadcrumb(*args)
423 423 elements = args.flatten
424 424 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
425 425 end
426 426
427 427 def other_formats_links(&block)
428 428 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
429 429 yield Redmine::Views::OtherFormatsBuilder.new(self)
430 430 concat('</p>'.html_safe)
431 431 end
432 432
433 433 def page_header_title
434 434 if @project.nil? || @project.new_record?
435 435 h(Setting.app_title)
436 436 else
437 437 b = []
438 438 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
439 439 if ancestors.any?
440 440 root = ancestors.shift
441 441 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
442 442 if ancestors.size > 2
443 443 b << "\xe2\x80\xa6"
444 444 ancestors = ancestors[-2, 2]
445 445 end
446 446 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
447 447 end
448 448 b << h(@project)
449 449 b.join(" \xc2\xbb ").html_safe
450 450 end
451 451 end
452 452
453 453 def html_title(*args)
454 454 if args.empty?
455 455 title = @html_title || []
456 456 title << @project.name if @project
457 457 title << Setting.app_title unless Setting.app_title == title.last
458 458 title.select {|t| !t.blank? }.join(' - ')
459 459 else
460 460 @html_title ||= []
461 461 @html_title += args
462 462 end
463 463 end
464 464
465 465 # Returns the theme, controller name, and action as css classes for the
466 466 # HTML body.
467 467 def body_css_classes
468 468 css = []
469 469 if theme = Redmine::Themes.theme(Setting.ui_theme)
470 470 css << 'theme-' + theme.name
471 471 end
472 472
473 473 css << 'controller-' + params[:controller]
474 474 css << 'action-' + params[:action]
475 475 css.join(' ')
476 476 end
477 477
478 478 def accesskey(s)
479 479 Redmine::AccessKeys.key_for s
480 480 end
481 481
482 482 # Formats text according to system settings.
483 483 # 2 ways to call this method:
484 484 # * with a String: textilizable(text, options)
485 485 # * with an object and one of its attribute: textilizable(issue, :description, options)
486 486 def textilizable(*args)
487 487 options = args.last.is_a?(Hash) ? args.pop : {}
488 488 case args.size
489 489 when 1
490 490 obj = options[:object]
491 491 text = args.shift
492 492 when 2
493 493 obj = args.shift
494 494 attr = args.shift
495 495 text = obj.send(attr).to_s
496 496 else
497 497 raise ArgumentError, 'invalid arguments to textilizable'
498 498 end
499 499 return '' if text.blank?
500 500 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
501 501 only_path = options.delete(:only_path) == false ? false : true
502 502
503 503 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
504 504
505 505 @parsed_headings = []
506 506 @heading_anchors = {}
507 507 @current_section = 0 if options[:edit_section_links]
508 508
509 509 parse_sections(text, project, obj, attr, only_path, options)
510 510 text = parse_non_pre_blocks(text) do |text|
511 511 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_macros].each do |method_name|
512 512 send method_name, text, project, obj, attr, only_path, options
513 513 end
514 514 end
515 515 parse_headings(text, project, obj, attr, only_path, options)
516 516
517 517 if @parsed_headings.any?
518 518 replace_toc(text, @parsed_headings)
519 519 end
520 520
521 521 text.html_safe
522 522 end
523 523
524 524 def parse_non_pre_blocks(text)
525 525 s = StringScanner.new(text)
526 526 tags = []
527 527 parsed = ''
528 528 while !s.eos?
529 529 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
530 530 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
531 531 if tags.empty?
532 532 yield text
533 533 end
534 534 parsed << text
535 535 if tag
536 536 if closing
537 537 if tags.last == tag.downcase
538 538 tags.pop
539 539 end
540 540 else
541 541 tags << tag.downcase
542 542 end
543 543 parsed << full_tag
544 544 end
545 545 end
546 546 # Close any non closing tags
547 547 while tag = tags.pop
548 548 parsed << "</#{tag}>"
549 549 end
550 parsed.html_safe
550 parsed
551 551 end
552 552
553 553 def parse_inline_attachments(text, project, obj, attr, only_path, options)
554 554 # when using an image link, try to use an attachment, if possible
555 555 if options[:attachments] || (obj && obj.respond_to?(:attachments))
556 556 attachments = options[:attachments] || obj.attachments
557 557 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
558 558 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
559 559 # search for the picture in attachments
560 560 if found = Attachment.latest_attach(attachments, filename)
561 561 image_url = url_for :only_path => only_path, :controller => 'attachments',
562 562 :action => 'download', :id => found
563 563 desc = found.description.to_s.gsub('"', '')
564 564 if !desc.blank? && alttext.blank?
565 565 alt = " title=\"#{desc}\" alt=\"#{desc}\""
566 566 end
567 "src=\"#{image_url}\"#{alt}".html_safe
567 "src=\"#{image_url}\"#{alt}"
568 568 else
569 m.html_safe
569 m
570 570 end
571 571 end
572 572 end
573 573 end
574 574
575 575 # Wiki links
576 576 #
577 577 # Examples:
578 578 # [[mypage]]
579 579 # [[mypage|mytext]]
580 580 # wiki links can refer other project wikis, using project name or identifier:
581 581 # [[project:]] -> wiki starting page
582 582 # [[project:|mytext]]
583 583 # [[project:mypage]]
584 584 # [[project:mypage|mytext]]
585 585 def parse_wiki_links(text, project, obj, attr, only_path, options)
586 586 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
587 587 link_project = project
588 588 esc, all, page, title = $1, $2, $3, $5
589 589 if esc.nil?
590 590 if page =~ /^([^\:]+)\:(.*)$/
591 591 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
592 592 page = $2
593 593 title ||= $1 if page.blank?
594 594 end
595 595
596 596 if link_project && link_project.wiki
597 597 # extract anchor
598 598 anchor = nil
599 599 if page =~ /^(.+?)\#(.+)$/
600 600 page, anchor = $1, $2
601 601 end
602 602 anchor = sanitize_anchor_name(anchor) if anchor.present?
603 603 # check if page exists
604 604 wiki_page = link_project.wiki.find_page(page)
605 605 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
606 606 "##{anchor}"
607 607 else
608 608 case options[:wiki_links]
609 609 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
610 610 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
611 611 else
612 612 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
613 613 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
614 614 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
615 615 :id => wiki_page_id, :anchor => anchor, :parent => parent)
616 616 end
617 617 end
618 618 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
619 619 else
620 620 # project or wiki doesn't exist
621 all.html_safe
621 all
622 622 end
623 623 else
624 all.html_safe
624 all
625 625 end
626 626 end
627 627 end
628 628
629 629 # Redmine links
630 630 #
631 631 # Examples:
632 632 # Issues:
633 633 # #52 -> Link to issue #52
634 634 # Changesets:
635 635 # r52 -> Link to revision 52
636 636 # commit:a85130f -> Link to scmid starting with a85130f
637 637 # Documents:
638 638 # document#17 -> Link to document with id 17
639 639 # document:Greetings -> Link to the document with title "Greetings"
640 640 # document:"Some document" -> Link to the document with title "Some document"
641 641 # Versions:
642 642 # version#3 -> Link to version with id 3
643 643 # version:1.0.0 -> Link to version named "1.0.0"
644 644 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
645 645 # Attachments:
646 646 # attachment:file.zip -> Link to the attachment of the current object named file.zip
647 647 # Source files:
648 648 # source:some/file -> Link to the file located at /some/file in the project's repository
649 649 # source:some/file@52 -> Link to the file's revision 52
650 650 # source:some/file#L120 -> Link to line 120 of the file
651 651 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
652 652 # export:some/file -> Force the download of the file
653 653 # Forum messages:
654 654 # message#1218 -> Link to message with id 1218
655 655 #
656 656 # Links can refer other objects from other projects, using project identifier:
657 657 # identifier:r52
658 658 # identifier:document:"Some document"
659 659 # identifier:version:1.0.0
660 660 # identifier:source:some/file
661 661 def parse_redmine_links(text, project, obj, attr, only_path, options)
662 662 text.gsub!(%r{([\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|
663 663 leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $2, $3, $4, $5, $10, $11, $8 || $12 || $18, $14 || $19, $15, $17
664 664 link = nil
665 665 if project_identifier
666 666 project = Project.visible.find_by_identifier(project_identifier)
667 667 end
668 668 if esc.nil?
669 669 if prefix.nil? && sep == 'r'
670 670 if project
671 671 repository = nil
672 672 if repo_identifier
673 673 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
674 674 else
675 675 repository = project.repository
676 676 end
677 677 # project.changesets.visible raises an SQL error because of a double join on repositories
678 678 if repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(repository.id, identifier))
679 679 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.revision},
680 680 :class => 'changeset',
681 681 :title => truncate_single_line(changeset.comments, :length => 100))
682 682 end
683 683 end
684 684 elsif sep == '#'
685 685 oid = identifier.to_i
686 686 case prefix
687 687 when nil
688 688 if issue = Issue.visible.find_by_id(oid, :include => :status)
689 689 anchor = comment_id ? "note-#{comment_id}" : nil
690 690 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid, :anchor => anchor},
691 691 :class => issue.css_classes,
692 692 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
693 693 end
694 694 when 'document'
695 695 if document = Document.visible.find_by_id(oid)
696 696 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
697 697 :class => 'document'
698 698 end
699 699 when 'version'
700 700 if version = Version.visible.find_by_id(oid)
701 701 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
702 702 :class => 'version'
703 703 end
704 704 when 'message'
705 705 if message = Message.visible.find_by_id(oid, :include => :parent)
706 706 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
707 707 end
708 708 when 'forum'
709 709 if board = Board.visible.find_by_id(oid)
710 710 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
711 711 :class => 'board'
712 712 end
713 713 when 'news'
714 714 if news = News.visible.find_by_id(oid)
715 715 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
716 716 :class => 'news'
717 717 end
718 718 when 'project'
719 719 if p = Project.visible.find_by_id(oid)
720 720 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
721 721 end
722 722 end
723 723 elsif sep == ':'
724 724 # removes the double quotes if any
725 725 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
726 726 case prefix
727 727 when 'document'
728 728 if project && document = project.documents.visible.find_by_title(name)
729 729 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
730 730 :class => 'document'
731 731 end
732 732 when 'version'
733 733 if project && version = project.versions.visible.find_by_name(name)
734 734 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
735 735 :class => 'version'
736 736 end
737 737 when 'forum'
738 738 if project && board = project.boards.visible.find_by_name(name)
739 739 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
740 740 :class => 'board'
741 741 end
742 742 when 'news'
743 743 if project && news = project.news.visible.find_by_title(name)
744 744 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
745 745 :class => 'news'
746 746 end
747 747 when 'commit', 'source', 'export'
748 748 if project
749 749 repository = nil
750 750 if name =~ %r{^(([a-z0-9\-]+)\|)(.+)$}
751 751 repo_prefix, repo_identifier, name = $1, $2, $3
752 752 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
753 753 else
754 754 repository = project.repository
755 755 end
756 756 if prefix == 'commit'
757 757 if repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%"]))
758 758 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},
759 759 :class => 'changeset',
760 760 :title => truncate_single_line(h(changeset.comments), :length => 100)
761 761 end
762 762 else
763 763 if repository && User.current.allowed_to?(:browse_repository, project)
764 764 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
765 765 path, rev, anchor = $1, $3, $5
766 766 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => 'entry', :id => project, :repository_id => repository.identifier_param,
767 767 :path => to_path_param(path),
768 768 :rev => rev,
769 769 :anchor => anchor,
770 770 :format => (prefix == 'export' ? 'raw' : nil)},
771 771 :class => (prefix == 'export' ? 'source download' : 'source')
772 772 end
773 773 end
774 774 repo_prefix = nil
775 775 end
776 776 when 'attachment'
777 777 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
778 778 if attachments && attachment = attachments.detect {|a| a.filename == name }
779 779 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
780 780 :class => 'attachment'
781 781 end
782 782 when 'project'
783 783 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
784 784 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
785 785 end
786 786 end
787 787 end
788 788 end
789 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}")).html_safe
789 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
790 790 end
791 791 end
792 792
793 793 HEADING_RE = /(<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>)/i unless const_defined?(:HEADING_RE)
794 794
795 795 def parse_sections(text, project, obj, attr, only_path, options)
796 796 return unless options[:edit_section_links]
797 797 text.gsub!(HEADING_RE) do
798 heading = $1
798 799 @current_section += 1
799 800 if @current_section > 1
800 801 content_tag('div',
801 802 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
802 803 :class => 'contextual',
803 :title => l(:button_edit_section)) + $1
804 :title => l(:button_edit_section)) + heading.html_safe
804 805 else
805 $1
806 heading
806 807 end
807 808 end
808 809 end
809 810
810 811 # Headings and TOC
811 812 # Adds ids and links to headings unless options[:headings] is set to false
812 813 def parse_headings(text, project, obj, attr, only_path, options)
813 814 return if options[:headings] == false
814 815
815 816 text.gsub!(HEADING_RE) do
816 817 level, attrs, content = $2.to_i, $3, $4
817 818 item = strip_tags(content).strip
818 819 anchor = sanitize_anchor_name(item)
819 820 # used for single-file wiki export
820 821 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
821 822 @heading_anchors[anchor] ||= 0
822 823 idx = (@heading_anchors[anchor] += 1)
823 824 if idx > 1
824 825 anchor = "#{anchor}-#{idx}"
825 826 end
826 827 @parsed_headings << [level, anchor, item]
827 828 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
828 829 end
829 830 end
830 831
831 832 MACROS_RE = /
832 833 (!)? # escaping
833 834 (
834 835 \{\{ # opening tag
835 836 ([\w]+) # macro name
836 837 (\(([^\}]*)\))? # optional arguments
837 838 \}\} # closing tag
838 839 )
839 840 /x unless const_defined?(:MACROS_RE)
840 841
841 842 # Macros substitution
842 843 def parse_macros(text, project, obj, attr, only_path, options)
843 844 text.gsub!(MACROS_RE) do
844 845 esc, all, macro = $1, $2, $3.downcase
845 846 args = ($5 || '').split(',').each(&:strip)
846 847 if esc.nil?
847 848 begin
848 849 exec_macro(macro, obj, args)
849 850 rescue => e
850 851 "<div class=\"flash error\">Error executing the <strong>#{macro}</strong> macro (#{e})</div>"
851 852 end || all
852 853 else
853 854 all
854 855 end
855 856 end
856 857 end
857 858
858 859 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
859 860
860 861 # Renders the TOC with given headings
861 862 def replace_toc(text, headings)
862 863 text.gsub!(TOC_RE) do
863 864 if headings.empty?
864 865 ''
865 866 else
866 867 div_class = 'toc'
867 868 div_class << ' right' if $1 == '>'
868 869 div_class << ' left' if $1 == '<'
869 870 out = "<ul class=\"#{div_class}\"><li>"
870 871 root = headings.map(&:first).min
871 872 current = root
872 873 started = false
873 874 headings.each do |level, anchor, item|
874 875 if level > current
875 876 out << '<ul><li>' * (level - current)
876 877 elsif level < current
877 878 out << "</li></ul>\n" * (current - level) + "</li><li>"
878 879 elsif started
879 880 out << '</li><li>'
880 881 end
881 882 out << "<a href=\"##{anchor}\">#{item}</a>"
882 883 current = level
883 884 started = true
884 885 end
885 886 out << '</li></ul>' * (current - root)
886 887 out << '</li></ul>'
887 888 end
888 889 end
889 890 end
890 891
891 892 # Same as Rails' simple_format helper without using paragraphs
892 893 def simple_format_without_paragraph(text)
893 894 text.to_s.
894 895 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
895 896 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
896 897 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
897 898 html_safe
898 899 end
899 900
900 901 def lang_options_for_select(blank=true)
901 902 (blank ? [["(auto)", ""]] : []) +
902 903 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
903 904 end
904 905
905 906 def label_tag_for(name, option_tags = nil, options = {})
906 907 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
907 908 content_tag("label", label_text)
908 909 end
909 910
910 911 def labelled_tabular_form_for(*args, &proc)
911 912 ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_tabular_form_for is deprecated and will be removed in Redmine 1.5. Use #labelled_form_for instead."
912 913 args << {} unless args.last.is_a?(Hash)
913 914 options = args.last
914 915 options[:html] ||= {}
915 916 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
916 917 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
917 918 form_for(*args, &proc)
918 919 end
919 920
920 921 def labelled_form_for(*args, &proc)
921 922 args << {} unless args.last.is_a?(Hash)
922 923 options = args.last
923 924 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
924 925 form_for(*args, &proc)
925 926 end
926 927
927 928 def labelled_fields_for(*args, &proc)
928 929 args << {} unless args.last.is_a?(Hash)
929 930 options = args.last
930 931 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
931 932 fields_for(*args, &proc)
932 933 end
933 934
934 935 def labelled_remote_form_for(*args, &proc)
935 936 args << {} unless args.last.is_a?(Hash)
936 937 options = args.last
937 938 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
938 939 remote_form_for(*args, &proc)
939 940 end
940 941
941 942 def back_url_hidden_field_tag
942 943 back_url = params[:back_url] || request.env['HTTP_REFERER']
943 944 back_url = CGI.unescape(back_url.to_s)
944 945 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
945 946 end
946 947
947 948 def check_all_links(form_name)
948 949 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
949 950 " | ".html_safe +
950 951 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
951 952 end
952 953
953 954 def progress_bar(pcts, options={})
954 955 pcts = [pcts, pcts] unless pcts.is_a?(Array)
955 956 pcts = pcts.collect(&:round)
956 957 pcts[1] = pcts[1] - pcts[0]
957 958 pcts << (100 - pcts[1] - pcts[0])
958 959 width = options[:width] || '100px;'
959 960 legend = options[:legend] || ''
960 961 content_tag('table',
961 962 content_tag('tr',
962 963 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
963 964 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
964 965 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
965 966 ), :class => 'progress', :style => "width: #{width};").html_safe +
966 967 content_tag('p', legend, :class => 'pourcent').html_safe
967 968 end
968 969
969 970 def checked_image(checked=true)
970 971 if checked
971 972 image_tag 'toggle_check.png'
972 973 end
973 974 end
974 975
975 976 def context_menu(url)
976 977 unless @context_menu_included
977 978 content_for :header_tags do
978 979 javascript_include_tag('context_menu') +
979 980 stylesheet_link_tag('context_menu')
980 981 end
981 982 if l(:direction) == 'rtl'
982 983 content_for :header_tags do
983 984 stylesheet_link_tag('context_menu_rtl')
984 985 end
985 986 end
986 987 @context_menu_included = true
987 988 end
988 989 javascript_tag "new ContextMenu('#{ url_for(url) }')"
989 990 end
990 991
991 992 def calendar_for(field_id)
992 993 include_calendar_headers_tags
993 994 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
994 995 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
995 996 end
996 997
997 998 def include_calendar_headers_tags
998 999 unless @calendar_headers_tags_included
999 1000 @calendar_headers_tags_included = true
1000 1001 content_for :header_tags do
1001 1002 start_of_week = case Setting.start_of_week.to_i
1002 1003 when 1
1003 1004 'Calendar._FD = 1;' # Monday
1004 1005 when 7
1005 1006 'Calendar._FD = 0;' # Sunday
1006 1007 when 6
1007 1008 'Calendar._FD = 6;' # Saturday
1008 1009 else
1009 1010 '' # use language
1010 1011 end
1011 1012
1012 1013 javascript_include_tag('calendar/calendar') +
1013 1014 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
1014 1015 javascript_tag(start_of_week) +
1015 1016 javascript_include_tag('calendar/calendar-setup') +
1016 1017 stylesheet_link_tag('calendar')
1017 1018 end
1018 1019 end
1019 1020 end
1020 1021
1021 1022 def content_for(name, content = nil, &block)
1022 1023 @has_content ||= {}
1023 1024 @has_content[name] = true
1024 1025 super(name, content, &block)
1025 1026 end
1026 1027
1027 1028 def has_content?(name)
1028 1029 (@has_content && @has_content[name]) || false
1029 1030 end
1030 1031
1031 1032 def email_delivery_enabled?
1032 1033 !!ActionMailer::Base.perform_deliveries
1033 1034 end
1034 1035
1035 1036 # Returns the avatar image tag for the given +user+ if avatars are enabled
1036 1037 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1037 1038 def avatar(user, options = { })
1038 1039 if Setting.gravatar_enabled?
1039 1040 options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
1040 1041 email = nil
1041 1042 if user.respond_to?(:mail)
1042 1043 email = user.mail
1043 1044 elsif user.to_s =~ %r{<(.+?)>}
1044 1045 email = $1
1045 1046 end
1046 1047 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1047 1048 else
1048 1049 ''
1049 1050 end
1050 1051 end
1051 1052
1052 1053 def sanitize_anchor_name(anchor)
1053 1054 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1054 1055 end
1055 1056
1056 1057 # Returns the javascript tags that are included in the html layout head
1057 1058 def javascript_heads
1058 1059 tags = javascript_include_tag(:defaults)
1059 1060 unless User.current.pref.warn_on_leaving_unsaved == '0'
1060 1061 tags << "\n".html_safe + javascript_tag("Event.observe(window, 'load', function(){ new WarnLeavingUnsaved('#{escape_javascript( l(:text_warn_on_leaving_unsaved) )}'); });")
1061 1062 end
1062 1063 tags
1063 1064 end
1064 1065
1065 1066 def favicon
1066 1067 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
1067 1068 end
1068 1069
1069 1070 def robot_exclusion_tag
1070 1071 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1071 1072 end
1072 1073
1073 1074 # Returns true if arg is expected in the API response
1074 1075 def include_in_api_response?(arg)
1075 1076 unless @included_in_api_response
1076 1077 param = params[:include]
1077 1078 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1078 1079 @included_in_api_response.collect!(&:strip)
1079 1080 end
1080 1081 @included_in_api_response.include?(arg.to_s)
1081 1082 end
1082 1083
1083 1084 # Returns options or nil if nometa param or X-Redmine-Nometa header
1084 1085 # was set in the request
1085 1086 def api_meta(options)
1086 1087 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1087 1088 # compatibility mode for activeresource clients that raise
1088 1089 # an error when unserializing an array with attributes
1089 1090 nil
1090 1091 else
1091 1092 options
1092 1093 end
1093 1094 end
1094 1095
1095 1096 private
1096 1097
1097 1098 def wiki_helper
1098 1099 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1099 1100 extend helper
1100 1101 return self
1101 1102 end
1102 1103
1103 1104 def link_to_content_update(text, url_params = {}, html_options = {})
1104 1105 link_to(text, url_params, html_options)
1105 1106 end
1106 1107 end
General Comments 0
You need to be logged in to leave comments. Login now