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

20
require 'forwardable'
21
require 'cgi'
22

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

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

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

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

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

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

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

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

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

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

150 151 152 153
  def wiki_page_path(page, options={})
    url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
  end

154 155 156 157 158 159
  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
160
  def toggle_link(name, id, options={})
161 162
    onclick = "$('##{id}').toggle(); "
    onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
jplang's avatar
jplang committed
163 164 165
    onclick << "return false;"
    link_to(name, "#", :onclick => onclick)
  end
166

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

175
  def format_activity_title(text)
176
    h(truncate_single_line(text, :length => 100))
177
  end
178

179
  def format_activity_day(date)
180
    date == User.current.today ? l(:label_today).titleize : format_date(date)
181
  end
182

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

188 189 190 191 192 193 194
  def format_version_name(version)
    if version.project == @project
    	h(version)
    else
      h("#{version.project} - #{version}")
    end
  end
195

196 197 198 199 200
  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
201

202 203 204 205 206 207 208 209
  # 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
210
      projects.sort_by(&:lft).each do |project|
211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234
        # 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

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

251 252 253 254
  # Renders flash messages
  def render_flash_messages
    s = ''
    flash.each do |k,v|
255
      s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
256
    end
257
    s.html_safe
258
  end
259

jplang's avatar
jplang committed
260 261 262 263 264 265 266 267
  # 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
268

269 270
  # Renders the project quick-jump box
  def render_project_jump_box
271
    return unless User.current.logged?
272
    projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq
273
    if projects.any?
emassip's avatar
emassip committed
274 275 276 277 278 279
      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) }
280
      end
emassip's avatar
emassip committed
281 282

      select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
283 284
    end
  end
285

286 287 288
  def project_tree_options_for_select(projects, options = {})
    s = ''
    project_tree(projects) do |project, level|
emassip's avatar
emassip committed
289
      name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
290 291 292 293 294 295
      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
296 297 298
      tag_options.merge!(yield(project)) if block_given?
      s << content_tag('option', name_prefix + h(project), tag_options)
    end
299
    s.html_safe
300
  end
301

302
  # Yields the given block for each project with its level in the tree
303 304
  #
  # Wrapper for Project#project_tree
305
  def project_tree(projects, &block)
306
    Project.project_tree(projects, &block)
307
  end
308

jplang's avatar
jplang committed
309 310
  def principals_check_box_tags(name, principals)
    s = ''
311
    principals.sort.each do |principal|
jplang's avatar
jplang committed
312 313
      s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
    end
314
    s.html_safe
jplang's avatar
jplang committed
315
  end
316

317 318 319
  # Returns a string for users/groups option tags
  def principals_options_for_select(collection, selected=nil)
    s = ''
320
    if collection.include?(User.current)
jplang's avatar
jplang committed
321
      s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
322
    end
323 324 325 326 327 328 329 330
    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
331
    s.html_safe
332
  end
333

334 335 336 337 338 339 340 341 342
  # 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

343 344
  # Truncates and returns the string as a single line
  def truncate_single_line(string, *args)
345
    truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
346
  end
347

348 349 350 351 352 353 354 355 356
  # 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
357

jplang's avatar
jplang committed
358 359 360 361
  def anchor(text)
    text.to_s.gsub(' ', '_')
  end

362
  def html_hours(text)
363
    text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
364
  end
365

jplang's avatar
jplang committed
366
  def authoring(created, author, options={})
367
    l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
368
  end
369

370 371 372
  def time_tag(time)
    text = distance_of_time_in_words(Time.now, time)
    if @project
373
      link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time))
374 375 376
    else
      content_tag('acronym', text, :title => format_time(time))
    end
377 378
  end

379 380 381 382 383 384
  def syntax_highlight_lines(name, content)
    lines = []
    syntax_highlight(name, content).each_line { |line| lines << line }
    lines
  end

385
  def syntax_highlight(name, content)
386
    Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
387
  end
388

jplang's avatar
jplang committed
389
  def to_path_param(path)
390 391
    str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
    str.blank? ? nil : str
jplang's avatar
jplang committed
392 393
  end

394
  def pagination_links_full(paginator, count=nil, options={})
395
    page_param = options.delete(:page_param) || :page
396
    per_page_links = options.delete(:per_page_links)
397
    url_param = params.dup
398 399

    html = ''
400
    if paginator.current.previous
401 402 403 404
      # \xc2\xab(utf-8) = &#171;
      html << link_to_content_update(
                   "\xc2\xab " + l(:label_previous),
                   url_param.merge(page_param => paginator.current.previous)) + ' '
405
    end
406

407
    html << (pagination_links_each(paginator, options) do |n|
408
      link_to_content_update(n.to_s, url_param.merge(page_param => n))
409
    end || '')
410

411
    if paginator.current.next
412 413 414 415
      # \xc2\xbb(utf-8) = &#187;
      html << ' ' + link_to_content_update(
                      (l(:label_next) + " \xc2\xbb"),
                      url_param.merge(page_param => paginator.current.next))
416
    end
417

418
    unless count.nil?
419
      html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
420
      if per_page_links != false && links = per_page_links(paginator.items_per_page, count)
421 422
	      html << " | #{links}"
      end
423
    end
424

425
    html.html_safe
426
  end
427

428 429 430 431 432 433 434 435 436 437 438 439 440 441
  def per_page_links(selected=nil, item_count=nil)
    values = Setting.per_page_options_array
    if item_count && values.any?
      if item_count > values.first
        max = values.detect {|value| value >= item_count} || item_count
      else
        max = item_count
      end
      values = values.select {|value| value <= max || value == selected}
    end
    if values.empty? || (values.size == 1 && values.first == selected)
      return nil
    end
    links = values.collect do |n|
442
      n == selected ? n : link_to_content_update(n, params.merge(:per_page => n))
443
    end
444
    l(:label_display_per_page, links.join(', '))
445
  end
446

jplang's avatar
jplang committed
447
  def reorder_links(name, url, method = :post)
448 449
    link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
            url.merge({"#{name}[move_to]" => 'highest'}),
jplang's avatar
jplang committed
450
            :method => method, :title => l(:label_sort_highest)) +
451 452
    link_to(image_tag('1uparrow.png',   :alt => l(:label_sort_higher)),
            url.merge({"#{name}[move_to]" => 'higher'}),
jplang's avatar
jplang committed
453
           :method => method, :title => l(:label_sort_higher)) +
454 455
    link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
            url.merge({"#{name}[move_to]" => 'lower'}),
jplang's avatar
jplang committed
456
            :method => method, :title => l(:label_sort_lower)) +
457 458
    link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
            url.merge({"#{name}[move_to]" => 'lowest'}),
jplang's avatar
jplang committed
459
           :method => method, :title => l(:label_sort_lowest))
jplang's avatar
jplang committed
460
  end
461

462
  def breadcrumb(*args)
463
    elements = args.flatten
464
    elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
465
  end
466

467
  def other_formats_links(&block)
jplang's avatar
jplang committed
468
    concat('<p class="other-formats">'.html_safe + l(:label_export_to))
469
    yield Redmine::Views::OtherFormatsBuilder.new(self)
jplang's avatar
jplang committed
470
    concat('</p>'.html_safe)
471
  end
472

473 474 475 476 477
  def page_header_title
    if @project.nil? || @project.new_record?
      h(Setting.app_title)
    else
      b = []
jplang's avatar
jplang committed
478
      ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
479 480
      if ancestors.any?
        root = ancestors.shift
481
        b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
482
        if ancestors.size > 2
483
          b << "\xe2\x80\xa6"
484 485
          ancestors = ancestors[-2, 2]
        end
486
        b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
487 488
      end
      b << h(@project)
489
      b.join(" \xc2\xbb ").html_safe
490 491
    end
  end
492

493 494
  def html_title(*args)
    if args.empty?
495
      title = @html_title || []
tmaruyama's avatar
tmaruyama committed
496
      title << @project.name if @project
497
      title << Setting.app_title unless Setting.app_title == title.last
jplang's avatar
jplang committed
498
      title.select {|t| !t.blank? }.join(' - ')
499 500 501 502
    else
      @html_title ||= []
      @html_title += args
    end
503
  end
jplang's avatar
jplang committed
504

505 506 507 508 509 510 511 512
  # 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

513 514
    css << 'controller-' + controller_name
    css << 'action-' + action_name
515 516 517
    css.join(' ')
  end

jplang's avatar
jplang committed
518
  def accesskey(s)
519
    Redmine::AccessKeys.key_for s
jplang's avatar
jplang committed
520 521
  end

522 523 524 525 526 527 528 529
  # 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
530
      obj = options[:object]
jplang's avatar
jplang committed
531
      text = args.shift
532 533
    when 2
      obj = args.shift
534 535
      attr = args.shift
      text = obj.send(attr).to_s
536 537 538
    else
      raise ArgumentError, 'invalid arguments to textilizable'
    end
jplang's avatar
jplang committed
539
    return '' if text.blank?
540 541
    project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
    only_path = options.delete(:only_path) == false ? false : true
542

543 544
    text = text.dup
    macros = catch_macros(text)
545
    text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
546

547
    @parsed_headings = []
548
    @heading_anchors = {}
549
    @current_section = 0 if options[:edit_section_links]
550 551

    parse_sections(text, project, obj, attr, only_path, options)
552 553
    text = parse_non_pre_blocks(text, obj, macros) do |text|
      [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
554 555
        send method_name, text, project, obj, attr, only_path, options
      end
556
    end
557
    parse_headings(text, project, obj, attr, only_path, options)
558

559 560 561
    if @parsed_headings.any?
      replace_toc(text, @parsed_headings)
    end
562

jplang's avatar
jplang committed
563
    text.html_safe
564
  end
565

566
  def parse_non_pre_blocks(text, obj, macros)
567 568 569 570 571 572 573 574
    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
575 576 577
        inject_macros(text, obj, macros) if macros.any?
      else
        inject_macros(text, obj, macros, false) if macros.any?
578 579 580 581 582 583 584 585 586 587 588 589 590
      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
591 592 593 594
    # Close any non closing tags
    while tag = tags.pop
      parsed << "</#{tag}>"
    end
595
    parsed
596
  end
597

598
  def parse_inline_attachments(text, project, obj, attr, only_path, options)
599
    # when using an image link, try to use an attachment, if possible
jplang's avatar
jplang committed
600 601 602
    if options[:attachments].present? || (obj && obj.respond_to?(:attachments))
      attachments = options[:attachments] || []
      attachments += obj.attachments if obj
603
      text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
604
        filename, ext, alt, alttext = $1.downcase, $2, $3, $4
605
        # search for the picture in attachments
606 607 608
        if found = Attachment.latest_attach(attachments, filename)
          image_url = url_for :only_path => only_path, :controller => 'attachments',
                              :action => 'download', :id => found
609 610 611 612
          desc = found.description.to_s.gsub('"', '')
          if !desc.blank? && alttext.blank?
            alt = " title=\"#{desc}\" alt=\"#{desc}\""
          end
613
          "src=\"#{image_url}\"#{alt}"
614
        else
615
          m
616 617 618
        end
      end
    end
619
  end
620

621 622 623 624 625 626 627 628 629 630 631 632
  # 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|
633
      link_project = project
634 635 636
      esc, all, page, title = $1, $2, $3, $5
      if esc.nil?
        if page =~ /^([^\:]+)\:(.*)$/
637
          link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
638 639 640
          page = $2
          title ||= $1 if page.blank?
        end
641

642
        if link_project && link_project.wiki
643 644 645 646 647
          # extract anchor
          anchor = nil
          if page =~ /^(.+?)\#(.+)$/
            page, anchor = $1, $2
          end
648
          anchor = sanitize_anchor_name(anchor) if anchor.present?
649 650
          # check if page exists
          wiki_page = link_project.wiki.find_page(page)
651 652 653 654
          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]
655
            when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
656
            when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
657
            else
658
              wiki_page_id = page.present? ? Wiki.titleize(page) : nil
659 660
              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
661
               :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
662
            end
663
          end
emassip's avatar
emassip committed
664
          link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
665 666
        else
          # project or wiki doesn't exist
667
          all
668
        end
jplang's avatar
jplang committed
669
      else
670
        all
jplang's avatar
jplang committed
671
      end
672
    end
673
  end
674

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

jplang's avatar
jplang committed
838
  HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
839 840 841 842

  def parse_sections(text, project, obj, attr, only_path, options)
    return unless options[:edit_section_links]
    text.gsub!(HEADING_RE) do
843
      heading = $1
844 845
      @current_section += 1
      if @current_section > 1
846
        content_tag('div',
847
          link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
848
          :class => 'contextual',
849
          :title => l(:button_edit_section)) + heading.html_safe
850
      else
851
        heading
852 853 854
      end
    end
  end
855

856
  # Headings and TOC
857
  # Adds ids and links to headings unless options[:headings] is set to false
858
  def parse_headings(text, project, obj, attr, only_path, options)
859
    return if options[:headings] == false
860

861
    text.gsub!(HEADING_RE) do
862
      level, attrs, content = $2.to_i, $3, $4
863
      item = strip_tags(content).strip
864
      anchor = sanitize_anchor_name(item)
865 866
      # 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))
867 868 869 870 871
      @heading_anchors[anchor] ||= 0
      idx = (@heading_anchors[anchor] += 1)
      if idx > 1
        anchor = "#{anchor}-#{idx}"
      end
872
      @parsed_headings << [level, anchor, item]
873
      "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
874 875
    end
  end
876

877
  MACROS_RE = /(
878 879 880 881
                (!)?                        # escaping
                (
                \{\{                        # opening tag
                ([\w]+)                     # macro name
882
                (\(([^\n\r]*?)\))?          # optional arguments
883
                ([\n\r].*?[\n\r])?          # optional block of text
884 885
                \}\}                        # closing tag
                )
886
               )/mx unless const_defined?(:MACROS_RE)