application_helper.rb 45.8 KB
Newer Older
1 2
# encoding: utf-8
#
3
# Redmine - project management software
jplang's avatar
jplang committed
4
# Copyright (C) 2006-2013  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
  include Redmine::Pagination::Helper
28

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

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

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

47
  # Displays a link to user's account page if active
48
  def link_to_user(user, options={})
jplang's avatar
jplang committed
49
    if user.is_a?(User)
50
      name = h(user.name(options[:format]))
51
      if user.active? || (User.current.admin? && user.logged?)
jplang's avatar
jplang committed
52
        link_to name, user_path(user), :class => user.css_classes
53 54 55
      else
        name
      end
jplang's avatar
jplang committed
56
    else
57
      h(user.to_s)
jplang's avatar
jplang committed
58
    end
59
  end
60

61 62
  # Displays a link to +issue+ with its subject.
  # Examples:
63
  #
64 65 66
  #   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
67
  #   link_to_issue(issue, :project => true)      # => Foo - Defect #6
68
  #   link_to_issue(issue, :subject => false, :tracker => false)     # => #6
69
  #
70
  def link_to_issue(issue, options={})
71 72
    title = nil
    subject = nil
73
    text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
74 75 76 77 78 79 80 81
    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
jplang's avatar
jplang committed
82
    s = link_to text, issue_path(issue), :class => issue.css_classes, :title => title
jplang's avatar
jplang committed
83 84
    s << h(": #{subject}") if subject
    s = h("#{issue.project} - ") + s if options[:project]
85
    s
jplang's avatar
jplang committed
86
  end
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
94 95 96 97
    route_method = options.delete(:download) ? :download_named_attachment_path : :named_attachment_path
    html_options = options.slice!(:only_path)
    url = send(route_method, attachment, attachment.filename, options)
    link_to text, url, html_options
98
  end
99

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

116 117 118
  # Generates a link to a message
  def link_to_message(message, options={}, html_options = nil)
    link_to(
119 120
      truncate(message.subject, :length => 60),
      board_message_path(message.board_id, message.parent_id || message.id, {
121 122
        :r => (message.parent_id && message.id),
        :anchor => (message.parent_id ? "message-#{message.id}" : nil)
123
      }.merge(options)),
124 125 126
      html_options
    )
  end
127

128 129
  # Generates a link to a project if active
  # Examples:
130
  #
131 132 133 134 135
  #   link_to_project(project)                          # => link to the specified project overview
  #   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)
136
    if project.archived?
137 138 139
      h(project.name)
    elsif options.key?(:action)
      ActiveSupport::Deprecation.warn "#link_to_project with :action option is deprecated and will be removed in Redmine 3.0."
jplang's avatar
jplang committed
140
      url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
141 142 143 144 145 146 147 148 149 150 151 152 153 154
      link_to project.name, url, html_options
    else
      link_to project.name, project_path(project, options), html_options
    end
  end

  # Generates a link to a project settings if active
  def link_to_project_settings(project, options={}, html_options=nil)
    if project.active?
      link_to project.name, settings_project_path(project, options), html_options
    elsif project.archived?
      h(project.name)
    else
      link_to project.name, project_path(project, options), html_options
155 156 157
    end
  end

158 159 160 161
  def wiki_page_path(page, options={})
    url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
  end

162
  def thumbnail_tag(attachment)
163 164
    link_to image_tag(thumbnail_path(attachment)),
      named_attachment_path(attachment, attachment.filename),
165 166 167
      :title => attachment.filename
  end

jplang's avatar
jplang committed
168
  def toggle_link(name, id, options={})
169 170
    onclick = "$('##{id}').toggle(); "
    onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
jplang's avatar
jplang committed
171 172 173
    onclick << "return false;"
    link_to(name, "#", :onclick => onclick)
  end
174

175 176
  def image_to_function(name, function, html_options = {})
    html_options.symbolize_keys!
177 178 179
    tag(:input, html_options.merge({
        :type => "image", :src => image_path(name),
        :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
180 181
        }))
  end
182

183
  def format_activity_title(text)
184
    h(truncate_single_line(text, :length => 100))
185
  end
186

187
  def format_activity_day(date)
188
    date == User.current.today ? l(:label_today).titleize : format_date(date)
189
  end
190

191
  def format_activity_description(text)
192 193
    h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
       ).gsub(/[\r\n]+/, "<br />").html_safe
194
  end
195

196 197 198 199 200 201 202
  def format_version_name(version)
    if version.project == @project
    	h(version)
    else
      h("#{version.project} - #{version}")
    end
  end
203

204 205 206 207 208
  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
209

210 211 212 213 214 215 216 217
  # Renders a tree of projects as a nested set of unordered lists
  # The given collection may be a subset of the whole project tree
  # (eg. some intermediate nodes are private and can not be seen)
  def render_project_nested_lists(projects)
    s = ''
    if projects.any?
      ancestors = []
      original_project = @project
jplang's avatar
jplang committed
218
      projects.sort_by(&:lft).each do |project|
219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242
        # set the project environment to please macros.
        @project = project
        if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
          s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
        else
          ancestors.pop
          s << "</li>"
          while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
            ancestors.pop
            s << "</ul></li>\n"
          end
        end
        classes = (ancestors.empty? ? 'root' : 'child')
        s << "<li class='#{classes}'><div class='#{classes}'>"
        s << h(block_given? ? yield(project) : project.name)
        s << "</div>\n"
        ancestors << project
      end
      s << ("</li></ul>\n" * ancestors.size)
      @project = original_project
    end
    s.html_safe
  end

243
  def render_page_hierarchy(pages, node=nil, options={})
244 245 246 247 248
    content = ''
    if pages[node]
      content << "<ul class=\"pages-hierarchy\">\n"
      pages[node].each do |page|
        content << "<li>"
jplang's avatar
jplang committed
249
        content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
250 251
                           :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]
252 253 254 255
        content << "</li>\n"
      end
      content << "</ul>\n"
    end
256
    content.html_safe
257
  end
258

259 260 261 262
  # Renders flash messages
  def render_flash_messages
    s = ''
    flash.each do |k,v|
263
      s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
264
    end
265
    s.html_safe
266
  end
267

jplang's avatar
jplang committed
268 269 270 271 272 273 274 275
  # 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
276

277 278
  # Renders the project quick-jump box
  def render_project_jump_box
279
    return unless User.current.logged?
280
    projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq
281
    if projects.any?
emassip's avatar
emassip committed
282 283 284 285 286 287
      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) }
288
      end
emassip's avatar
emassip committed
289 290

      select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
291 292
    end
  end
293

294 295 296
  def project_tree_options_for_select(projects, options = {})
    s = ''
    project_tree(projects) do |project, level|
emassip's avatar
emassip committed
297
      name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
298 299 300 301 302 303
      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
304 305 306
      tag_options.merge!(yield(project)) if block_given?
      s << content_tag('option', name_prefix + h(project), tag_options)
    end
307
    s.html_safe
308
  end
309

310
  # Yields the given block for each project with its level in the tree
311 312
  #
  # Wrapper for Project#project_tree
313
  def project_tree(projects, &block)
314
    Project.project_tree(projects, &block)
315
  end
316

jplang's avatar
jplang committed
317 318
  def principals_check_box_tags(name, principals)
    s = ''
319
    principals.sort.each do |principal|
jplang's avatar
jplang committed
320 321
      s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
    end
322
    s.html_safe
jplang's avatar
jplang committed
323
  end
324

325 326 327
  # Returns a string for users/groups option tags
  def principals_options_for_select(collection, selected=nil)
    s = ''
328
    if collection.include?(User.current)
jplang's avatar
jplang committed
329
      s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
330
    end
331 332 333 334 335 336 337 338
    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
339
    s.html_safe
340
  end
341

342 343 344 345 346 347 348 349 350
  # Options for the new membership projects combo-box
  def options_for_membership_project_select(principal, projects)
    options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---")
    options << project_tree_options_for_select(projects) do |p|
      {:disabled => principal.projects.include?(p)}
    end
    options
  end

351 352
  # Truncates and returns the string as a single line
  def truncate_single_line(string, *args)
353
    truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
354
  end
355

356 357 358 359 360 361 362 363 364
  # 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
365

jplang's avatar
jplang committed
366 367 368 369
  def anchor(text)
    text.to_s.gsub(' ', '_')
  end

370
  def html_hours(text)
371
    text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
372
  end
373

jplang's avatar
jplang committed
374
  def authoring(created, author, options={})
375
    l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
376
  end
377

378 379 380
  def time_tag(time)
    text = distance_of_time_in_words(Time.now, time)
    if @project
381
      link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time))
382 383 384
    else
      content_tag('acronym', text, :title => format_time(time))
    end
385 386
  end

387 388 389 390 391 392
  def syntax_highlight_lines(name, content)
    lines = []
    syntax_highlight(name, content).each_line { |line| lines << line }
    lines
  end

393
  def syntax_highlight(name, content)
394
    Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
395
  end
396

jplang's avatar
jplang committed
397
  def to_path_param(path)
398 399
    str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
    str.blank? ? nil : str
jplang's avatar
jplang committed
400 401
  end

jplang's avatar
jplang committed
402
  def reorder_links(name, url, method = :post)
403 404
    link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
            url.merge({"#{name}[move_to]" => 'highest'}),
jplang's avatar
jplang committed
405
            :method => method, :title => l(:label_sort_highest)) +
406 407
    link_to(image_tag('1uparrow.png',   :alt => l(:label_sort_higher)),
            url.merge({"#{name}[move_to]" => 'higher'}),
jplang's avatar
jplang committed
408
           :method => method, :title => l(:label_sort_higher)) +
409 410
    link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
            url.merge({"#{name}[move_to]" => 'lower'}),
jplang's avatar
jplang committed
411
            :method => method, :title => l(:label_sort_lower)) +
412 413
    link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
            url.merge({"#{name}[move_to]" => 'lowest'}),
jplang's avatar
jplang committed
414
           :method => method, :title => l(:label_sort_lowest))
jplang's avatar
jplang committed
415
  end
416

417
  def breadcrumb(*args)
418
    elements = args.flatten
419
    elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
420
  end
421

422
  def other_formats_links(&block)
jplang's avatar
jplang committed
423
    concat('<p class="other-formats">'.html_safe + l(:label_export_to))
424
    yield Redmine::Views::OtherFormatsBuilder.new(self)
jplang's avatar
jplang committed
425
    concat('</p>'.html_safe)
426
  end
427

428 429 430 431 432
  def page_header_title
    if @project.nil? || @project.new_record?
      h(Setting.app_title)
    else
      b = []
jplang's avatar
jplang committed
433
      ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
434 435
      if ancestors.any?
        root = ancestors.shift
436
        b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
437
        if ancestors.size > 2
438
          b << "\xe2\x80\xa6"
439 440
          ancestors = ancestors[-2, 2]
        end
441
        b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
442 443
      end
      b << h(@project)
444
      b.join(" \xc2\xbb ").html_safe
445 446
    end
  end
447

448 449
  def html_title(*args)
    if args.empty?
450
      title = @html_title || []
tmaruyama's avatar
tmaruyama committed
451
      title << @project.name if @project
452
      title << Setting.app_title unless Setting.app_title == title.last
jplang's avatar
jplang committed
453
      title.select {|t| !t.blank? }.join(' - ')
454 455 456 457
    else
      @html_title ||= []
      @html_title += args
    end
458
  end
jplang's avatar
jplang committed
459

460 461 462 463 464 465 466 467
  # 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

468 469
    css << 'controller-' + controller_name
    css << 'action-' + action_name
470 471 472
    css.join(' ')
  end

jplang's avatar
jplang committed
473
  def accesskey(s)
474
    Redmine::AccessKeys.key_for s
jplang's avatar
jplang committed
475 476
  end

477 478 479 480 481 482 483 484
  # 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
485
      obj = options[:object]
jplang's avatar
jplang committed
486
      text = args.shift
487 488
    when 2
      obj = args.shift
489 490
      attr = args.shift
      text = obj.send(attr).to_s
491 492 493
    else
      raise ArgumentError, 'invalid arguments to textilizable'
    end
jplang's avatar
jplang committed
494
    return '' if text.blank?
495 496
    project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
    only_path = options.delete(:only_path) == false ? false : true
497

498 499
    text = text.dup
    macros = catch_macros(text)
500
    text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
501

502
    @parsed_headings = []
503
    @heading_anchors = {}
504
    @current_section = 0 if options[:edit_section_links]
505 506

    parse_sections(text, project, obj, attr, only_path, options)
507 508
    text = parse_non_pre_blocks(text, obj, macros) do |text|
      [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
509 510
        send method_name, text, project, obj, attr, only_path, options
      end
511
    end
512
    parse_headings(text, project, obj, attr, only_path, options)
513

514 515 516
    if @parsed_headings.any?
      replace_toc(text, @parsed_headings)
    end
517

jplang's avatar
jplang committed
518
    text.html_safe
519
  end
520

521
  def parse_non_pre_blocks(text, obj, macros)
522 523 524 525 526 527 528 529
    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
530 531 532
        inject_macros(text, obj, macros) if macros.any?
      else
        inject_macros(text, obj, macros, false) if macros.any?
533 534 535 536 537 538 539 540 541 542 543 544 545
      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
546 547 548 549
    # Close any non closing tags
    while tag = tags.pop
      parsed << "</#{tag}>"
    end
550
    parsed
551
  end
552

553
  def parse_inline_attachments(text, project, obj, attr, only_path, options)
554
    # when using an image link, try to use an attachment, if possible
jplang's avatar
jplang committed
555 556 557
    attachments = options[:attachments] || []
    attachments += obj.attachments if obj.respond_to?(:attachments)
    if attachments.present?
558
      text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
559
        filename, ext, alt, alttext = $1.downcase, $2, $3, $4
560
        # search for the picture in attachments
561
        if found = Attachment.latest_attach(attachments, filename)
562
          image_url = download_named_attachment_path(found, found.filename, :only_path => only_path)
563 564 565 566
          desc = found.description.to_s.gsub('"', '')
          if !desc.blank? && alttext.blank?
            alt = " title=\"#{desc}\" alt=\"#{desc}\""
          end
567
          "src=\"#{image_url}\"#{alt}"
568
        else
569
          m
570 571 572
        end
      end
    end
573
  end
574

575 576 577 578 579 580 581 582 583 584 585 586
  # 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|
587
      link_project = project
588 589 590
      esc, all, page, title = $1, $2, $3, $5
      if esc.nil?
        if page =~ /^([^\:]+)\:(.*)$/
591
          link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
592 593 594
          page = $2
          title ||= $1 if page.blank?
        end
595

596
        if link_project && link_project.wiki
597 598 599 600 601
          # extract anchor
          anchor = nil
          if page =~ /^(.+?)\#(.+)$/
            page, anchor = $1, $2
          end
602
          anchor = sanitize_anchor_name(anchor) if anchor.present?
603 604
          # check if page exists
          wiki_page = link_project.wiki.find_page(page)
605 606 607 608
          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]
609
            when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
610
            when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
611
            else
612
              wiki_page_id = page.present? ? Wiki.titleize(page) : nil
613 614
              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, 
jplang's avatar
jplang committed
615
               :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
616
            end
617
          end
emassip's avatar
emassip committed
618
          link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
619 620
        else
          # project or wiki doesn't exist
621
          all
622
        end
jplang's avatar
jplang committed
623
      else
624
        all
jplang's avatar
jplang committed
625
      end
626
    end
627
  end
628

629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652
  # 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
653
  #   Forum messages:
654
  #     message#1218 -> Link to message with id 1218
655 656 657 658 659 660
  #
  #   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
661
  def parse_redmine_links(text, default_project, obj, attr, only_path, options)
662 663
    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
664
      link = nil
665
      project = default_project
666 667 668
      if project_identifier
        project = Project.visible.find_by_identifier(project_identifier)
      end
669 670
      if esc.nil?
        if prefix.nil? && sep == 'r'
671 672 673 674 675 676 677 678 679 680 681 682 683
          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
684 685
          end
        elsif sep == '#'
686
          oid = identifier.to_i
687 688
          case prefix
          when nil
689
            if oid.to_s == identifier && issue = Issue.visible.find_by_id(oid, :include => :status)
690 691
              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
692
                                        :class => issue.css_classes,
693
                                        :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
694 695
            end
          when 'document'
696
            if document = Document.visible.find_by_id(oid)
jplang's avatar
jplang committed
697 698
              link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
                                                :class => 'document'
699 700
            end
          when 'version'
701
            if version = Version.visible.find_by_id(oid)
jplang's avatar
jplang committed
702 703
              link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
                                              :class => 'version'
704
            end
705
          when 'message'
706
            if message = Message.visible.find_by_id(oid, :include => :parent)
707
              link = link_to_message(message, {:only_path => only_path}, :class => 'message')
708
            end
709 710 711 712 713 714 715 716 717 718
          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
719 720
          when 'project'
            if p = Project.visible.find_by_id(oid)
721
              link = link_to_project(p, {:only_path => only_path}, :class => 'project')
jplang's avatar
jplang committed
722
            end
723 724 725
          end
        elsif sep == ':'
          # removes the double quotes if any
726
          name = identifier.gsub(%r{^"(.*)"$}, "\\1")
727 728
          case prefix
          when 'document'
729
            if project && document = project.documents.visible.find_by_title(name)
jplang's avatar
jplang committed
730 731
              link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
                                                :class => 'document'
732 733
            end
          when 'version'
734
            if project && version = project.versions.visible.find_by_name(name)
jplang's avatar
jplang committed
735 736
              link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
                                              :class => 'version'
737
            end
738 739 740 741 742 743 744 745 746 747
          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
748 749 750 751 752 753 754 755 756 757
          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'
jplang's avatar
jplang committed
758
                if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
759 760 761 762 763 764
                  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)
765
                  name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
766
                  path, rev, anchor = $1, $3, $5
767
                  link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
768 769
                                                          :path => to_path_param(path),
                                                          :rev => rev,
770
                                                          :anchor => anchor},
771 772 773 774
                                                         :class => (prefix == 'export' ? 'source download' : 'source')
                end
              end
              repo_prefix = nil
775
            end
776
          when 'attachment'
777
            attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
778
            if attachments && attachment = Attachment.latest_attach(attachments, name)
jplang's avatar
jplang committed
779
              link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
780
            end
jplang's avatar
jplang committed
781
          when 'project'
jplang's avatar
jplang committed
782
            if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
783
              link = link_to_project(p, {:only_path => only_path}, :class => 'project')
jplang's avatar
jplang committed
784
            end
785
          end
jplang's avatar
jplang committed
786
        end
787
      end
788
      (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
789
    end
790
  end
791

jplang's avatar
jplang committed
792
  HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
793 794 795 796

  def parse_sections(text, project, obj, attr, only_path, options)
    return unless options[:edit_section_links]
    text.gsub!(HEADING_RE) do
797
      heading = $1
798 799
      @current_section += 1
      if @current_section > 1
800
        content_tag('div',
801
          link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
802
          :class => 'contextual',
803
          :title => l(:button_edit_section)) + heading.html_safe
804
      else
805
        heading
806 807 808
      end
    end
  end
809

810
  # Headings and TOC
811
  # Adds ids and links to headings unless options[:headings] is set to false
812
  def parse_headings(text, project, obj, attr, only_path, options)
813
    return if options[:headings] == false
814

815
    text.gsub!(HEADING_RE) do
816
      level, attrs, content = $2.to_i, $3, $4
817
      item = strip_tags(content).strip
818
      anchor = sanitize_anchor_name(item)
819 820
      # 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))
821 822 823 824 825
      @heading_anchors[anchor] ||= 0
      idx = (@heading_anchors[anchor] += 1)
      if idx > 1
        anchor = "#{anchor}-#{idx}"
      end
826
      @parsed_headings << [level, anchor, item]
827
      "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
828 829
    end
  end
830

831
  MACROS_RE = /(
832 833 834 835
                (!)?                        # escaping
                (
                \{\{                        # opening tag
                ([\w]+)                     # macro name
836
                (\(([^\n\r]*?)\))?          # optional arguments
837
                ([\n\r].*?[\n\r])?          # optional block of text
838 839
                \}\}                        # closing tag
                )
840
               )/mx unless const_defined?(:MACROS_RE)
841 842 843 844 845

  MACRO_SUB_RE = /(
                  \{\{
                  macro\((\d+)\)
                  \}\}
846
                  )/x unless const_defined?(:MACRO_SUB_RE)
847

848 849 850
  # Extracts macros from text
  def catch_macros(text)
    macros = {}
851
    text.gsub!(MACROS_RE) do
852 853 854 855 856
      all, macro = $1, $4.downcase
      if macro_exists?(macro) || all =~ MACRO_SUB_RE
        index = macros.size
        macros[index] = all
        "{{macro(#{index})}}"
857 858 859 860
      else
        all
      end
    end
861 862 863 864 865 866 867 868 869
    macros
  end

  # Executes and replaces macros in text
  def inject_macros(text, obj, macros, execute=true)
    text.gsub!(MACRO_SUB_RE) do
      all, index = $1, $2.to_i
      orig = macros.delete(index)
      if execute && orig && orig =~ MACROS_RE
870
        esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
871
        if esc.nil?
872
          h(exec_macro(macro, obj, args, block) || all)
873 874 875 876 877 878 879 880 881
        else
          h(all)
        end
      elsif orig
        h(orig)
      else
        h(all)
      end
    end
882 883
  end

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

886 887
  # Renders the TOC with given headings
  def replace_toc(text, headings)
888
    text.gsub!(TOC_RE) do
jplang's avatar
jplang committed
889 890
      # Keep only the 4 first levels
      headings = headings.select{|level, anchor, item| level <= 4}
891 892 893 894 895 896
      if headings.empty?
        ''
      else
        div_class = 'toc'
        div_class << ' right' if $1 == '>'
        div_class << ' left' if $1 == '<'
jplang's avatar
jplang committed
897 898 899 900
        out = "<ul class=\"#{div_class}\"><li>"
        root = headings.map(&:first).min
        current = root
        started = false
901
        headings.each do |level, anchor, item|
jplang's avatar
jplang committed
902 903 904 905 906 907 908 909 910 911
          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