GitLab steht Mittwoch, den 23. September, zwischen 10:00 und 12:00 Uhr aufgrund von Wartungsarbeiten nicht zur Verfügung.

application_helper.rb 46.1 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 94
  # 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'
95 96
    opt_only_path = {}
    opt_only_path[:only_path] = (options[:only_path] == false ? false : true)
97
    options.delete(:only_path)
98 99
    link_to(h(text),
           {:controller => 'attachments', :action => action,
100
            :id => attachment, :filename => attachment.filename}.merge(opt_only_path),
101
           options)
102
  end
103

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

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

132 133
  # Generates a link to a project if active
  # Examples:
134
  #
135 136 137 138 139
  #   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)
140
    if project.archived?
141 142 143
      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
144
      url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
145 146 147 148 149 150 151 152 153 154 155 156 157 158
      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
159 160 161
    end
  end

162 163 164 165
  def wiki_page_path(page, options={})
    url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
  end

166 167 168 169 170 171
  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
172
  def toggle_link(name, id, options={})
173 174
    onclick = "$('##{id}').toggle(); "
    onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
jplang's avatar
jplang committed
175 176 177
    onclick << "return false;"
    link_to(name, "#", :onclick => onclick)
  end
178

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

187
  def format_activity_title(text)
188
    h(truncate_single_line(text, :length => 100))
189
  end
190

191
  def format_activity_day(date)
192
    date == User.current.today ? l(:label_today).titleize : format_date(date)
193
  end
194

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

200 201 202 203 204 205 206
  def format_version_name(version)
    if version.project == @project
    	h(version)
    else
      h("#{version.project} - #{version}")
    end
  end
207

208 209 210 211 212
  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
213

214 215 216 217 218 219 220 221
  # 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
222
      projects.sort_by(&:lft).each do |project|
223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246
        # 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

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

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

jplang's avatar
jplang committed
272 273 274 275 276 277 278 279
  # 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
280

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

      select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
295 296
    end
  end
297

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

314
  # Yields the given block for each project with its level in the tree
315 316
  #
  # Wrapper for Project#project_tree
317
  def project_tree(projects, &block)
318
    Project.project_tree(projects, &block)
319
  end
320

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

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

346 347 348 349 350 351 352 353 354
  # 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

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

360 361 362 363 364 365 366 367 368
  # 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
369

jplang's avatar
jplang committed
370 371 372 373
  def anchor(text)
    text.to_s.gsub(' ', '_')
  end

374
  def html_hours(text)
375
    text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
376
  end
377

jplang's avatar
jplang committed
378
  def authoring(created, author, options={})
379
    l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
380
  end
381

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

391 392 393 394 395 396
  def syntax_highlight_lines(name, content)
    lines = []
    syntax_highlight(name, content).each_line { |line| lines << line }
    lines
  end

397
  def syntax_highlight(name, content)
398
    Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
399
  end
400

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

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

421
  def breadcrumb(*args)
422
    elements = args.flatten
423
    elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
424
  end
425

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

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

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

464 465 466 467 468 469 470 471
  # 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

472 473
    css << 'controller-' + controller_name
    css << 'action-' + action_name
474 475 476
    css.join(' ')
  end

jplang's avatar
jplang committed
477
  def accesskey(s)
478
    Redmine::AccessKeys.key_for s
jplang's avatar
jplang committed
479 480
  end

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

502 503
    text = text.dup
    macros = catch_macros(text)
504
    text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
505

506
    @parsed_headings = []
507
    @heading_anchors = {}
508
    @current_section = 0 if options[:edit_section_links]
509 510

    parse_sections(text, project, obj, attr, only_path, options)
511 512
    text = parse_non_pre_blocks(text, obj, macros) do |text|
      [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
513 514
        send method_name, text, project, obj, attr, only_path, options
      end
515
    end
516
    parse_headings(text, project, obj, attr, only_path, options)
517

518 519 520
    if @parsed_headings.any?
      replace_toc(text, @parsed_headings)
    end
521

jplang's avatar
jplang committed
522
    text.html_safe
523
  end
524

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

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

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

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

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

jplang's avatar
jplang committed
797
  HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
798 799 800 801

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

815
  # Headings and TOC
816
  # Adds ids and links to headings unless options[:headings] is set to false
817
  def parse_headings(text, project, obj, attr, only_path, options)
818
    return if options[:headings] == false
819

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

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

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

853 854 855
  # Extracts macros from text
  def catch_macros(text)
    macros = {}
856
    text.gsub!(MACROS_RE) do
857 858 859 860 861
      all, macro = $1, $4.downcase
      if macro_exists?(macro) || all =~ MACRO_SUB_RE
        index = macros.size
        macros[index] = all
        "{{macro(#{index})}}"
862 863 864 865
      else
        all
      end
    end
866 867 868 869 870 871 872 873 874
    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
875
        esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
876
        if esc.nil?
877
          h(exec_macro(macro, obj, args, block) || all)
878 879 880 881 882 883 884 885 886
        else
          h(all)
        end
      elsif orig
        h(orig)
      else
        h(all)
      end
    end
887 888
  end

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

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