application_helper.rb 44.7 KB
Newer Older
1 2
# encoding: utf-8
#
3
# Redmine - project management software
jplang's avatar
jplang committed
4
# Copyright (C) 2006-2012  Jean-Philippe Lang
5 6 7 8 9
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
10
#
11 12 13 14
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
15
#
16 17 18 19
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

20
require 'forwardable'
21
require 'cgi'
22

23
module ApplicationHelper
24
  include Redmine::WikiFormatting::Macros::Definitions
25
  include Redmine::I18n
26
  include GravatarHelper::PublicMethods
27

28 29 30
  extend Forwardable
  def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter

31
  # Return true if user is authorized for controller/action, otherwise false
jplang's avatar
jplang committed
32 33
  def authorize_for(controller, action)
    User.current.allowed_to?({:controller => controller, :action => action}, @project)
34 35 36
  end

  # Display a link if user is authorized
37 38
  #
  # @param [String] name Anchor text (passed to link_to)
edavis10's avatar
edavis10 committed
39
  # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
40 41
  # @param [optional, Hash] html_options Options passed to link_to
  # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
42
  def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
edavis10's avatar
edavis10 committed
43
    link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
44
  end
45

46
  # Displays a link to user's account page if active
47
  def link_to_user(user, options={})
jplang's avatar
jplang committed
48
    if user.is_a?(User)
49 50 51 52 53 54
      name = h(user.name(options[:format]))
      if user.active?
        link_to name, :controller => 'users', :action => 'show', :id => user
      else
        name
      end
jplang's avatar
jplang committed
55
    else
56
      h(user.to_s)
jplang's avatar
jplang committed
57
    end
58
  end
59

60 61
  # Displays a link to +issue+ with its subject.
  # Examples:
62
  #
63 64 65
  #   link_to_issue(issue)                        # => Defect #6: This is the subject
  #   link_to_issue(issue, :truncate => 6)        # => Defect #6: This i...
  #   link_to_issue(issue, :subject => false)     # => Defect #6
66
  #   link_to_issue(issue, :project => true)      # => Foo - Defect #6
67
  #
68
  def link_to_issue(issue, options={})
69 70 71 72 73 74 75 76 77 78
    title = nil
    subject = nil
    if options[:subject] == false
      title = truncate(issue.subject, :length => 60)
    else
      subject = issue.subject
      if options[:truncate]
        subject = truncate(subject, :length => options[:truncate])
      end
    end
tmaruyama's avatar
tmaruyama committed
79
    s = link_to "#{h(issue.tracker)} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
80 81
                                                 :class => issue.css_classes,
                                                 :title => title
jplang's avatar
jplang committed
82 83
    s << h(": #{subject}") if subject
    s = h("#{issue.project} - ") + s if options[:project]
84
    s
jplang's avatar
jplang committed
85
  end
86

87 88 89 90 91 92 93
  # Generates a link to an attachment.
  # Options:
  # * :text - Link text (default to attachment filename)
  # * :download - Force download (default: false)
  def link_to_attachment(attachment, options={})
    text = options.delete(:text) || attachment.filename
    action = options.delete(:download) ? 'download' : 'show'
94 95
    opt_only_path = {}
    opt_only_path[:only_path] = (options[:only_path] == false ? false : true)
96
    options.delete(:only_path)
97 98
    link_to(h(text),
           {:controller => 'attachments', :action => action,
99
            :id => attachment, :filename => attachment.filename}.merge(opt_only_path),
100
           options)
101
  end
102

103 104 105
  # Generates a link to a SCM revision
  # Options:
  # * :text - Link text (default to the formatted revision)
106 107 108 109
  def link_to_revision(revision, repository, options={})
    if repository.is_a?(Project)
      repository = repository.repository
    end
110
    text = options.delete(:text) || format_revision(revision)
111
    rev = revision.respond_to?(:identifier) ? revision.identifier : revision
112 113
    link_to(
        h(text),
114
        {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
115 116
        :title => l(:label_revision_id, format_revision(revision))
      )
117
  end
118

119 120 121 122 123 124
  # Generates a link to a message
  def link_to_message(message, options={}, html_options = nil)
    link_to(
      h(truncate(message.subject, :length => 60)),
      { :controller => 'messages', :action => 'show',
        :board_id => message.board_id,
125
        :id => (message.parent_id || message.id),
126 127 128 129 130 131
        :r => (message.parent_id && message.id),
        :anchor => (message.parent_id ? "message-#{message.id}" : nil)
      }.merge(options),
      html_options
    )
  end
132

133 134
  # Generates a link to a project if active
  # Examples:
135
  #
136 137 138 139 140 141
  #   link_to_project(project)                          # => link to the specified project overview
  #   link_to_project(project, :action=>'settings')     # => link to project settings
  #   link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
  #   link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
  #
  def link_to_project(project, options={}, html_options = nil)
142 143 144
    if project.archived?
      h(project)
    else
145 146 147 148 149
      url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
      link_to(h(project), url, html_options)
    end
  end

150 151 152 153 154 155
  def thumbnail_tag(attachment)
    link_to image_tag(url_for(:controller => 'attachments', :action => 'thumbnail', :id => attachment)),
      {:controller => 'attachments', :action => 'show', :id => attachment, :filename => attachment.filename},
      :title => attachment.filename
  end

jplang's avatar
jplang committed
156
  def toggle_link(name, id, options={})
157 158
    onclick = "$('##{id}').toggle(); "
    onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
jplang's avatar
jplang committed
159 160 161
    onclick << "return false;"
    link_to(name, "#", :onclick => onclick)
  end
162

163 164
  def image_to_function(name, function, html_options = {})
    html_options.symbolize_keys!
165 166 167
    tag(:input, html_options.merge({
        :type => "image", :src => image_path(name),
        :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
168 169
        }))
  end
170

171
  def format_activity_title(text)
172
    h(truncate_single_line(text, :length => 100))
173
  end
174

175
  def format_activity_day(date)
176
    date == User.current.today ? l(:label_today).titleize : format_date(date)
177
  end
178

179
  def format_activity_description(text)
180 181
    h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
       ).gsub(/[\r\n]+/, "<br />").html_safe
182
  end
183

184 185 186 187 188 189 190
  def format_version_name(version)
    if version.project == @project
    	h(version)
    else
      h("#{version.project} - #{version}")
    end
  end
191

192 193 194 195 196
  def due_date_distance_in_words(date)
    if date
      l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
    end
  end
197

198
  def render_page_hierarchy(pages, node=nil, options={})
199 200 201 202 203
    content = ''
    if pages[node]
      content << "<ul class=\"pages-hierarchy\">\n"
      pages[node].each do |page|
        content << "<li>"
204
        content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
205 206
                           :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
        content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
207 208 209 210
        content << "</li>\n"
      end
      content << "</ul>\n"
    end
211
    content.html_safe
212
  end
213

214 215 216 217
  # Renders flash messages
  def render_flash_messages
    s = ''
    flash.each do |k,v|
218
      s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
219
    end
220
    s.html_safe
221
  end
222

jplang's avatar
jplang committed
223 224 225 226 227 228 229 230
  # Renders tabs and their content
  def render_tabs(tabs)
    if tabs.any?
      render :partial => 'common/tabs', :locals => {:tabs => tabs}
    else
      content_tag 'p', l(:label_no_data), :class => "nodata"
    end
  end
231

232 233
  # Renders the project quick-jump box
  def render_project_jump_box
234
    return unless User.current.logged?
235
    projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq
236
    if projects.any?
emassip's avatar
emassip committed
237 238 239 240 241 242
      options =
        ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
         '<option value="" disabled="disabled">---</option>').html_safe

      options << project_tree_options_for_select(projects, :selected => @project) do |p|
        { :value => project_path(:id => p, :jump => current_menu_item) }
243
      end
emassip's avatar
emassip committed
244 245

      select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
246 247
    end
  end
248

249 250 251
  def project_tree_options_for_select(projects, options = {})
    s = ''
    project_tree(projects) do |project, level|
emassip's avatar
emassip committed
252
      name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
253 254 255 256 257 258
      tag_options = {:value => project.id}
      if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
        tag_options[:selected] = 'selected'
      else
        tag_options[:selected] = nil
      end
259 260 261
      tag_options.merge!(yield(project)) if block_given?
      s << content_tag('option', name_prefix + h(project), tag_options)
    end
262
    s.html_safe
263
  end
264

265
  # Yields the given block for each project with its level in the tree
266 267
  #
  # Wrapper for Project#project_tree
268
  def project_tree(projects, &block)
269
    Project.project_tree(projects, &block)
270
  end
271

272 273 274 275 276 277 278 279 280 281
  def project_nested_ul(projects, &block)
    s = ''
    if projects.any?
      ancestors = []
      projects.sort_by(&:lft).each do |project|
        if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
          s << "<ul>\n"
        else
          ancestors.pop
          s << "</li>"
282
          while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
283 284 285 286 287 288 289 290 291 292
            ancestors.pop
            s << "</ul></li>\n"
          end
        end
        s << "<li>"
        s << yield(project).to_s
        ancestors << project
      end
      s << ("</li></ul>\n" * ancestors.size)
    end
293
    s.html_safe
294
  end
295

jplang's avatar
jplang committed
296 297
  def principals_check_box_tags(name, principals)
    s = ''
298
    principals.sort.each do |principal|
jplang's avatar
jplang committed
299 300
      s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
    end
301
    s.html_safe
jplang's avatar
jplang committed
302
  end
303

304 305 306
  # Returns a string for users/groups option tags
  def principals_options_for_select(collection, selected=nil)
    s = ''
307
    if collection.include?(User.current)
jplang's avatar
jplang committed
308
      s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
309
    end
310 311 312 313 314 315 316 317
    groups = ''
    collection.sort.each do |element|
      selected_attribute = ' selected="selected"' if option_value_selected?(element, selected)
      (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
    end
    unless groups.empty?
      s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
    end
318
    s.html_safe
319
  end
320

321 322
  # Truncates and returns the string as a single line
  def truncate_single_line(string, *args)
323
    truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
324
  end
325

326 327 328 329 330 331 332 333 334
  # Truncates at line break after 250 characters or options[:length]
  def truncate_lines(string, options={})
    length = options[:length] || 250
    if string.to_s =~ /\A(.{#{length}}.*?)$/m
      "#{$1}..."
    else
      string
    end
  end
335

jplang's avatar
jplang committed
336 337 338 339
  def anchor(text)
    text.to_s.gsub(' ', '_')
  end

340
  def html_hours(text)
341
    text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
342
  end
343

jplang's avatar
jplang committed
344
  def authoring(created, author, options={})
345
    l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
346
  end
347

348 349 350
  def time_tag(time)
    text = distance_of_time_in_words(Time.now, time)
    if @project
351
      link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time))
352 353 354
    else
      content_tag('acronym', text, :title => format_time(time))
    end
355 356
  end

357 358 359 360 361 362
  def syntax_highlight_lines(name, content)
    lines = []
    syntax_highlight(name, content).each_line { |line| lines << line }
    lines
  end

363
  def syntax_highlight(name, content)
364
    Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
365
  end
366

jplang's avatar
jplang committed
367
  def to_path_param(path)
368 369
    str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
    str.blank? ? nil : str
jplang's avatar
jplang committed
370 371
  end

372
  def pagination_links_full(paginator, count=nil, options={})
373
    page_param = options.delete(:page_param) || :page
374
    per_page_links = options.delete(:per_page_links)
375
    url_param = params.dup
376 377

    html = ''
378
    if paginator.current.previous
379 380 381 382
      # \xc2\xab(utf-8) = &#171;
      html << link_to_content_update(
                   "\xc2\xab " + l(:label_previous),
                   url_param.merge(page_param => paginator.current.previous)) + ' '
383
    end
384

385
    html << (pagination_links_each(paginator, options) do |n|
386
      link_to_content_update(n.to_s, url_param.merge(page_param => n))
387
    end || '')
388

389
    if paginator.current.next
390 391 392 393
      # \xc2\xbb(utf-8) = &#187;
      html << ' ' + link_to_content_update(
                      (l(:label_next) + " \xc2\xbb"),
                      url_param.merge(page_param => paginator.current.next))
394
    end
395

396
    unless count.nil?
397
      html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
398
      if per_page_links != false && links = per_page_links(paginator.items_per_page, count)
399 400
	      html << " | #{links}"
      end
401
    end
402

403
    html.html_safe
404
  end
405

406 407 408 409 410 411 412 413 414 415 416 417 418 419
  def per_page_links(selected=nil, item_count=nil)
    values = Setting.per_page_options_array
    if item_count && values.any?
      if item_count > values.first
        max = values.detect {|value| value >= item_count} || item_count
      else
        max = item_count
      end
      values = values.select {|value| value <= max || value == selected}
    end
    if values.empty? || (values.size == 1 && values.first == selected)
      return nil
    end
    links = values.collect do |n|
420
      n == selected ? n : link_to_content_update(n, params.merge(:per_page => n))
421
    end
422
    l(:label_display_per_page, links.join(', '))
423
  end
424

jplang's avatar
jplang committed
425
  def reorder_links(name, url, method = :post)
426 427
    link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
            url.merge({"#{name}[move_to]" => 'highest'}),
jplang's avatar
jplang committed
428
            :method => method, :title => l(:label_sort_highest)) +
429 430
    link_to(image_tag('1uparrow.png',   :alt => l(:label_sort_higher)),
            url.merge({"#{name}[move_to]" => 'higher'}),
jplang's avatar
jplang committed
431
           :method => method, :title => l(:label_sort_higher)) +
432 433
    link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
            url.merge({"#{name}[move_to]" => 'lower'}),
jplang's avatar
jplang committed
434
            :method => method, :title => l(:label_sort_lower)) +
435 436
    link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
            url.merge({"#{name}[move_to]" => 'lowest'}),
jplang's avatar
jplang committed
437
           :method => method, :title => l(:label_sort_lowest))
jplang's avatar
jplang committed
438
  end
439

440
  def breadcrumb(*args)
441
    elements = args.flatten
442
    elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
443
  end
444

445
  def other_formats_links(&block)
jplang's avatar
jplang committed
446
    concat('<p class="other-formats">'.html_safe + l(:label_export_to))
447
    yield Redmine::Views::OtherFormatsBuilder.new(self)
jplang's avatar
jplang committed
448
    concat('</p>'.html_safe)
449
  end
450

451 452 453 454 455
  def page_header_title
    if @project.nil? || @project.new_record?
      h(Setting.app_title)
    else
      b = []
jplang's avatar
jplang committed
456
      ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
457 458
      if ancestors.any?
        root = ancestors.shift
459
        b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
460
        if ancestors.size > 2
461
          b << "\xe2\x80\xa6"
462 463
          ancestors = ancestors[-2, 2]
        end
464
        b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
465 466
      end
      b << h(@project)
467
      b.join(" \xc2\xbb ").html_safe
468 469
    end
  end
470

471 472
  def html_title(*args)
    if args.empty?
473
      title = @html_title || []
tmaruyama's avatar
tmaruyama committed
474
      title << @project.name if @project
475
      title << Setting.app_title unless Setting.app_title == title.last
jplang's avatar
jplang committed
476
      title.select {|t| !t.blank? }.join(' - ')
477 478 479 480
    else
      @html_title ||= []
      @html_title += args
    end
481
  end
jplang's avatar
jplang committed
482

483 484 485 486 487 488 489 490
  # Returns the theme, controller name, and action as css classes for the
  # HTML body.
  def body_css_classes
    css = []
    if theme = Redmine::Themes.theme(Setting.ui_theme)
      css << 'theme-' + theme.name
    end

491 492
    css << 'controller-' + controller_name
    css << 'action-' + action_name
493 494 495
    css.join(' ')
  end

jplang's avatar
jplang committed
496
  def accesskey(s)
497
    Redmine::AccessKeys.key_for s
jplang's avatar
jplang committed
498 499
  end

500 501 502 503 504 505 506 507
  # Formats text according to system settings.
  # 2 ways to call this method:
  # * with a String: textilizable(text, options)
  # * with an object and one of its attribute: textilizable(issue, :description, options)
  def textilizable(*args)
    options = args.last.is_a?(Hash) ? args.pop : {}
    case args.size
    when 1
508
      obj = options[:object]
jplang's avatar
jplang committed
509
      text = args.shift
510 511
    when 2
      obj = args.shift
512 513
      attr = args.shift
      text = obj.send(attr).to_s
514 515 516
    else
      raise ArgumentError, 'invalid arguments to textilizable'
    end
jplang's avatar
jplang committed
517
    return '' if text.blank?
518 519
    project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
    only_path = options.delete(:only_path) == false ? false : true
520

521
    text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
522

523
    @parsed_headings = []
524
    @heading_anchors = {}
525
    @current_section = 0 if options[:edit_section_links]
526 527

    parse_sections(text, project, obj, attr, only_path, options)
528
    text = parse_non_pre_blocks(text) do |text|
529
      [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_macros].each do |method_name|
530 531
        send method_name, text, project, obj, attr, only_path, options
      end
532
    end
533
    parse_headings(text, project, obj, attr, only_path, options)
534

535 536 537
    if @parsed_headings.any?
      replace_toc(text, @parsed_headings)
    end
538

jplang's avatar
jplang committed
539
    text.html_safe
540
  end
541

542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563
  def parse_non_pre_blocks(text)
    s = StringScanner.new(text)
    tags = []
    parsed = ''
    while !s.eos?
      s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
      text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
      if tags.empty?
        yield text
      end
      parsed << text
      if tag
        if closing
          if tags.last == tag.downcase
            tags.pop
          end
        else
          tags << tag.downcase
        end
        parsed << full_tag
      end
    end
564 565 566 567
    # Close any non closing tags
    while tag = tags.pop
      parsed << "</#{tag}>"
    end
568
    parsed
569
  end
570

571
  def parse_inline_attachments(text, project, obj, attr, only_path, options)
572
    # when using an image link, try to use an attachment, if possible
573
    if options[:attachments] || (obj && obj.respond_to?(:attachments))
574
      attachments = options[:attachments] || obj.attachments
575
      text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
576
        filename, ext, alt, alttext = $1.downcase, $2, $3, $4
577
        # search for the picture in attachments
578 579 580
        if found = Attachment.latest_attach(attachments, filename)
          image_url = url_for :only_path => only_path, :controller => 'attachments',
                              :action => 'download', :id => found
581 582 583 584
          desc = found.description.to_s.gsub('"', '')
          if !desc.blank? && alttext.blank?
            alt = " title=\"#{desc}\" alt=\"#{desc}\""
          end
585
          "src=\"#{image_url}\"#{alt}"
586
        else
587
          m
588 589 590
        end
      end
    end
591
  end
592

593 594 595 596 597 598 599 600 601 602 603 604
  # Wiki links
  #
  # Examples:
  #   [[mypage]]
  #   [[mypage|mytext]]
  # wiki links can refer other project wikis, using project name or identifier:
  #   [[project:]] -> wiki starting page
  #   [[project:|mytext]]
  #   [[project:mypage]]
  #   [[project:mypage|mytext]]
  def parse_wiki_links(text, project, obj, attr, only_path, options)
    text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
605
      link_project = project
606 607 608
      esc, all, page, title = $1, $2, $3, $5
      if esc.nil?
        if page =~ /^([^\:]+)\:(.*)$/
609
          link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
610 611 612
          page = $2
          title ||= $1 if page.blank?
        end
613

614
        if link_project && link_project.wiki
615 616 617 618 619
          # extract anchor
          anchor = nil
          if page =~ /^(.+?)\#(.+)$/
            page, anchor = $1, $2
          end
620
          anchor = sanitize_anchor_name(anchor) if anchor.present?
621 622
          # check if page exists
          wiki_page = link_project.wiki.find_page(page)
623 624 625 626
          url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
            "##{anchor}"
          else
            case options[:wiki_links]
627
            when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
628
            when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
629
            else
630
              wiki_page_id = page.present? ? Wiki.titleize(page) : nil
631 632 633
              parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
              url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, 
               :id => wiki_page_id, :anchor => anchor, :parent => parent)
634
            end
635
          end
emassip's avatar
emassip committed
636
          link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
637 638
        else
          # project or wiki doesn't exist
639
          all
640
        end
jplang's avatar
jplang committed
641
      else
642
        all
jplang's avatar
jplang committed
643
      end
644
    end
645
  end
646

647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670
  # Redmine links
  #
  # Examples:
  #   Issues:
  #     #52 -> Link to issue #52
  #   Changesets:
  #     r52 -> Link to revision 52
  #     commit:a85130f -> Link to scmid starting with a85130f
  #   Documents:
  #     document#17 -> Link to document with id 17
  #     document:Greetings -> Link to the document with title "Greetings"
  #     document:"Some document" -> Link to the document with title "Some document"
  #   Versions:
  #     version#3 -> Link to version with id 3
  #     version:1.0.0 -> Link to version named "1.0.0"
  #     version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
  #   Attachments:
  #     attachment:file.zip -> Link to the attachment of the current object named file.zip
  #   Source files:
  #     source:some/file -> Link to the file located at /some/file in the project's repository
  #     source:some/file@52 -> Link to the file's revision 52
  #     source:some/file#L120 -> Link to line 120 of the file
  #     source:some/file@52#L120 -> Link to line 120 of the file's revision 52
  #     export:some/file -> Force the download of the file
671
  #   Forum messages:
672
  #     message#1218 -> Link to message with id 1218
673 674 675 676 677 678
  #
  #   Links can refer other objects from other projects, using project identifier:
  #     identifier:r52
  #     identifier:document:"Some document"
  #     identifier:version:1.0.0
  #     identifier:source:some/file
679
  def parse_redmine_links(text, project, obj, attr, only_path, options)
680 681
    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|
      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
682
      link = nil
683 684 685
      if project_identifier
        project = Project.visible.find_by_identifier(project_identifier)
      end
686 687
      if esc.nil?
        if prefix.nil? && sep == 'r'
688 689 690 691 692 693 694 695 696 697 698 699 700
          if project
            repository = nil
            if repo_identifier
              repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
            else
              repository = project.repository
            end
            # project.changesets.visible raises an SQL error because of a double join on repositories
            if repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(repository.id, identifier))
              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},
                                        :class => 'changeset',
                                        :title => truncate_single_line(changeset.comments, :length => 100))
            end
701 702
          end
        elsif sep == '#'
703
          oid = identifier.to_i
704 705
          case prefix
          when nil
706
            if oid.to_s == identifier && issue = Issue.visible.find_by_id(oid, :include => :status)
707 708
              anchor = comment_id ? "note-#{comment_id}" : nil
              link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid, :anchor => anchor},
jplang's avatar
jplang committed
709
                                        :class => issue.css_classes,
710
                                        :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
711 712
            end
          when 'document'
713
            if document = Document.visible.find_by_id(oid)
jplang's avatar
jplang committed
714 715
              link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
                                                :class => 'document'
716 717
            end
          when 'version'
718
            if version = Version.visible.find_by_id(oid)
jplang's avatar
jplang committed
719 720
              link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
                                              :class => 'version'
721
            end
722
          when 'message'
723
            if message = Message.visible.find_by_id(oid, :include => :parent)
724
              link = link_to_message(message, {:only_path => only_path}, :class => 'message')
725
            end
726 727 728 729 730 731 732 733 734 735
          when 'forum'
            if board = Board.visible.find_by_id(oid)
              link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
                                             :class => 'board'
            end
          when 'news'
            if news = News.visible.find_by_id(oid)
              link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
                                            :class => 'news'
            end
jplang's avatar
jplang committed
736 737
          when 'project'
            if p = Project.visible.find_by_id(oid)
738
              link = link_to_project(p, {:only_path => only_path}, :class => 'project')
jplang's avatar
jplang committed
739
            end
740 741 742
          end
        elsif sep == ':'
          # removes the double quotes if any
743
          name = identifier.gsub(%r{^"(.*)"$}, "\\1")
744 745
          case prefix
          when 'document'
746
            if project && document = project.documents.visible.find_by_title(name)
jplang's avatar
jplang committed
747 748
              link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
                                                :class => 'document'
749 750
            end
          when 'version'
751
            if project && version = project.versions.visible.find_by_name(name)
jplang's avatar
jplang committed
752 753
              link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
                                              :class => 'version'
754
            end
755 756 757 758 759 760 761 762 763 764
          when 'forum'
            if project && board = project.boards.visible.find_by_name(name)
              link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
                                             :class => 'board'
            end
          when 'news'
            if project && news = project.news.visible.find_by_title(name)
              link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
                                            :class => 'news'
            end
765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792
          when 'commit', 'source', 'export'
            if project
              repository = nil
              if name =~ %r{^(([a-z0-9\-]+)\|)(.+)$}
                repo_prefix, repo_identifier, name = $1, $2, $3
                repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
              else
                repository = project.repository
              end
              if prefix == 'commit'
                if repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%"]))
                  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},
                                               :class => 'changeset',
                                               :title => truncate_single_line(h(changeset.comments), :length => 100)
                end
              else
                if repository && User.current.allowed_to?(:browse_repository, project)
                  name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
                  path, rev, anchor = $1, $3, $5
                  link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => 'entry', :id => project, :repository_id => repository.identifier_param,
                                                          :path => to_path_param(path),
                                                          :rev => rev,
                                                          :anchor => anchor,
                                                          :format => (prefix == 'export' ? 'raw' : nil)},
                                                         :class => (prefix == 'export' ? 'source download' : 'source')
                end
              end
              repo_prefix = nil
793
            end
794
          when 'attachment'
795
            attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
796
            if attachments && attachment = attachments.detect {|a| a.filename == name }
jplang's avatar
jplang committed
797 798
              link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
                                                     :class => 'attachment'
799
            end
jplang's avatar
jplang committed
800 801
          when 'project'
            if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
802
              link = link_to_project(p, {:only_path => only_path}, :class => 'project')
jplang's avatar
jplang committed
803
            end
804
          end
jplang's avatar
jplang committed
805
        end
806
      end
807
      (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
808
    end
809
  end
810

811 812 813 814 815
  HEADING_RE = /(<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>)/i unless const_defined?(:HEADING_RE)

  def parse_sections(text, project, obj, attr, only_path, options)
    return unless options[:edit_section_links]
    text.gsub!(HEADING_RE) do
816
      heading = $1
817 818
      @current_section += 1
      if @current_section > 1
819
        content_tag('div',
820
          link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
821
          :class => 'contextual',
822
          :title => l(:button_edit_section)) + heading.html_safe
823
      else
824
        heading
825 826 827
      end
    end
  end
828

829
  # Headings and TOC
830
  # Adds ids and links to headings unless options[:headings] is set to false
831
  def parse_headings(text, project, obj, attr, only_path, options)
832
    return if options[:headings] == false
833

834
    text.gsub!(HEADING_RE) do
835
      level, attrs, content = $2.to_i, $3, $4
836
      item = strip_tags(content).strip
837
      anchor = sanitize_anchor_name(item)
838 839
      # used for single-file wiki export
      anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
840 841 842 843 844
      @heading_anchors[anchor] ||= 0
      idx = (@heading_anchors[anchor] += 1)
      if idx > 1
        anchor = "#{anchor}-#{idx}"
      end
845
      @parsed_headings << [level, anchor, item]
846
      "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
847 848
    end
  end
849

850 851 852 853 854
  MACROS_RE = /
                (!)?                        # escaping
                (
                \{\{                        # opening tag
                ([\w]+)                     # macro name
jplang's avatar
jplang committed
855
                (\((.*?)\))?                # optional arguments
856 857 858 859 860 861 862
                \}\}                        # closing tag
                )
              /x unless const_defined?(:MACROS_RE)

  # Macros substitution
  def parse_macros(text, project, obj, attr, only_path, options)
    text.gsub!(MACROS_RE) do
863
      esc, all, macro, args = $1, $2, $3.downcase, $5.to_s
864 865 866 867 868 869 870 871 872 873 874 875
      if esc.nil?
        begin
          exec_macro(macro, obj, args)
        rescue => e
          "<div class=\"flash error\">Error executing the <strong>#{macro}</strong> macro (#{e})</div>"
        end || all
      else
        all
      end
    end
  end

876
  TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
877

878 879
  # Renders the TOC with given headings
  def replace_toc(text, headings)
880 881 882 883 884 885 886
    text.gsub!(TOC_RE) do
      if headings.empty?
        ''
      else
        div_class = 'toc'
        div_class << ' right' if $1 == '>'
        div_class << ' left' if $1 == '<'
jplang's avatar
jplang committed
887 888 889 890
        out = "<ul class=\"#{div_class}\"><li>"
        root = headings.map(&:first).min
        current = root
        started = false
891
        headings.each do |level, anchor, item|
jplang's avatar
jplang committed
892 893 894 895 896 897 898 899 900 901
          if level > current
            out << '<ul><li>' * (level - current)
          elsif level < current
            out << "</li></ul>\n" * (current - level) + "</li><li>"
          elsif started
            out << '</li><li>'
          end
          out << "<a href=\"##{anchor}\">#{item}</a>"
          current = level
          started = true
902
        end
jplang's avatar
jplang committed
903 904
        out << '</li></ul>' * (current - root)
        out << '</li></ul>'
905 906 907
      end
    end
  end
908

jplang's avatar
jplang committed
909 910 911 912 913
  # Same as Rails' simple_format helper without using paragraphs
  def simple_format_without_paragraph(text)
    text.to_s.
      gsub(/\r\n?/, "\n").                    # \r\n and \r -> \n
      gsub(/\n\n+/, "<br /><br />").          # 2+ newline  -> 2 br