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

application_helper.rb 47.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
  # 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
  def format_version_name(version)
    if version.project == @project
198
      h(version)
199 200 201 202
    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.each do |principal|
jplang's avatar
jplang committed
320
      s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
jplang's avatar
jplang committed
321
    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
    groups = ''
    collection.sort.each do |element|
333
      selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
334 335 336 337 338
      (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
  # 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|
346
      {:disabled => principal.projects.to_a.include?(p)}
347 348 349 350
    end
    options
  end

351 352 353 354
  def option_tag(name, text, value, selected=nil, options={})
    content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
  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 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470
	# Returns a h2 tag and sets the html title with the given arguments
  def title(*args)
    strings = args.map do |arg|
      if arg.is_a?(Array) && arg.size >= 2
        link_to(*arg)
      else
        h(arg.to_s)
      end
    end
    html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
    content_tag('h2', strings.join(' &#187; ').html_safe)
  end

  # Sets the html title
  # Returns the html title when called without arguments
  # Current project name and app_title and automatically appended
  # Exemples:
  #   html_title 'Foo', 'Bar'
  #   html_title # => 'Foo - Bar - My Project - Redmine'
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
476
      title.reject(&: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 498 499 500 501
    @used_accesskeys ||= []
    key = Redmine::AccessKeys.key_for(s)
    return nil if @used_accesskeys.include?(key)
    @used_accesskeys << key
    key
jplang's avatar
jplang committed
502 503
  end

504 505 506 507 508 509 510 511
  # 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
512
      obj = options[:object]
jplang's avatar
jplang committed
513
      text = args.shift
514 515
    when 2
      obj = args.shift
516 517
      attr = args.shift
      text = obj.send(attr).to_s
518 519 520
    else
      raise ArgumentError, 'invalid arguments to textilizable'
    end
jplang's avatar
jplang committed
521
    return '' if text.blank?
522 523
    project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
    only_path = options.delete(:only_path) == false ? false : true
524

525 526
    text = text.dup
    macros = catch_macros(text)
527
    text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
528

529
    @parsed_headings = []
530
    @heading_anchors = {}
531
    @current_section = 0 if options[:edit_section_links]
532 533

    parse_sections(text, project, obj, attr, only_path, options)
534 535
    text = parse_non_pre_blocks(text, obj, macros) do |text|
      [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
536 537
        send method_name, text, project, obj, attr, only_path, options
      end
538
    end
539
    parse_headings(text, project, obj, attr, only_path, options)
540

541 542 543
    if @parsed_headings.any?
      replace_toc(text, @parsed_headings)
    end
544

jplang's avatar
jplang committed
545
    text.html_safe
546
  end
547

548
  def parse_non_pre_blocks(text, obj, macros)
549 550 551 552 553 554 555 556
    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
557 558 559
        inject_macros(text, obj, macros) if macros.any?
      else
        inject_macros(text, obj, macros, false) if macros.any?
560 561 562 563 564 565 566 567 568 569 570 571 572
      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
573 574 575 576
    # Close any non closing tags
    while tag = tags.pop
      parsed << "</#{tag}>"
    end
577
    parsed
578
  end
579

580
  def parse_inline_attachments(text, project, obj, attr, only_path, options)
581
    # when using an image link, try to use an attachment, if possible
jplang's avatar
jplang committed
582 583 584
    attachments = options[:attachments] || []
    attachments += obj.attachments if obj.respond_to?(:attachments)
    if attachments.present?
585
      text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
586
        filename, ext, alt, alttext = $1.downcase, $2, $3, $4
587
        # search for the picture in attachments
588
        if found = Attachment.latest_attach(attachments, filename)
589
          image_url = download_named_attachment_path(found, found.filename, :only_path => only_path)
590 591 592 593
          desc = found.description.to_s.gsub('"', '')
          if !desc.blank? && alttext.blank?
            alt = " title=\"#{desc}\" alt=\"#{desc}\""
          end
594
          "src=\"#{image_url}\"#{alt}"
595
        else
596
          m
597 598 599
        end
      end
    end
600
  end
601

602 603 604 605 606 607 608 609 610 611 612 613
  # 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|
614
      link_project = project
615 616 617
      esc, all, page, title = $1, $2, $3, $5
      if esc.nil?
        if page =~ /^([^\:]+)\:(.*)$/
618 619 620
          identifier, page = $1, $2
          link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
          title ||= identifier if page.blank?
621
        end
622

623
        if link_project && link_project.wiki
624 625 626 627 628
          # extract anchor
          anchor = nil
          if page =~ /^(.+?)\#(.+)$/
            page, anchor = $1, $2
          end
629
          anchor = sanitize_anchor_name(anchor) if anchor.present?
630 631
          # check if page exists
          wiki_page = link_project.wiki.find_page(page)
632 633 634 635
          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]
636
            when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
637
            when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
638
            else
639
              wiki_page_id = page.present? ? Wiki.titleize(page) : nil
640 641
              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
642
               :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
643
            end
644
          end
emassip's avatar
emassip committed
645
          link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
646 647
        else
          # project or wiki doesn't exist
648
          all
649
        end
jplang's avatar
jplang committed
650
      else
651
        all
jplang's avatar
jplang committed
652
      end
653
    end
654
  end
655

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

jplang's avatar
jplang committed
822
  HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
823 824 825 826

  def parse_sections(text, project, obj, attr, only_path, options)
    return unless options[:edit_section_links]
    text.gsub!(HEADING_RE) do
827
      heading = $1
828 829
      @current_section += 1
      if @current_section > 1
830
        content_tag('div',
831
          link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
832
          :class => 'contextual',
833
          :title => l(:button_edit_section)) + heading.html_safe
834
      else
835
        heading
836 837 838
      end
    end
  end
839

840
  # Headings and TOC
841
  # Adds ids and links to headings unless options[:headings] is set to false
842
  def parse_headings(text, project, obj, attr, only_path, options)
843
    return if options[:headings] == false
844

845
    text.gsub!(HEADING_RE) do
846
      level, attrs, content = $2.to_i, $3, $4
847
      item = strip_tags(content).strip
848
      anchor = sanitize_anchor_name(item)
849 850
      # 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))
851 852 853 854 855
      @heading_anchors[anchor] ||= 0
      idx = (@heading_anchors[anchor] += 1)
      if idx > 1
        anchor = "#{anchor}-#{idx}"
      end
856
      @parsed_headings << [level, anchor, item]
857
      "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
858 859
    end
  end
860

861
  MACROS_RE = /(
862 863 864 865
                (!)?                        # escaping
                (
                \{\{                        # opening tag
                ([\w]+)                     # macro name
866
                (\(([^\n\r]*?)\))?          # optional arguments
867
                ([\n\r].*?[\n\r])?          # optional block of text
868 869
                \}\}                        # closing tag
                )
870
               )/mx unless const_defined?(:MACROS_RE)
871 872 873 874 875

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

878 879 880
  # Extracts macros from text
  def catch_macros(text)
    macros = {}
881
    text.gsub!(MACROS_RE) do
882 883 884 885 886
      all, macro = $1, $4.downcase
      if macro_exists?(macro) || all =~ MACRO_SUB_RE
        index = macros.size
        macros[index] = all
        "{{macro(#{index})}}"
887 888 889 890
      else
        all
      end
    end
891 892 893 894 895 896 897 898 899
    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