application_helper.rb 49.3 KB
Newer Older
1 2
# encoding: utf-8
#
3
# Redmine - project management software
marutosijp's avatar
marutosijp committed
4
# Copyright (C) 2006-2014  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
    if options[:subject] == false
75
      title = issue.subject.truncate(60)
76 77
    else
      subject = issue.subject
78 79
      if truncate_length = options[:truncate]
        subject = subject.truncate(truncate_length)
80 81
      end
    end
82
    only_path = options[:only_path].nil? ? true : options[:only_path]
83 84
    s = link_to(text, issue_path(issue, :only_path => only_path),
                :class => issue.css_classes, :title => title)
jplang's avatar
jplang committed
85 86
    s << h(": #{subject}") if subject
    s = h("#{issue.project} - ") + s if options[:project]
87
    s
jplang's avatar
jplang committed
88
  end
89

90 91 92 93 94 95
  # 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
96 97 98 99
    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
100
  end
101

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

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

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

160 161 162
  # Helper that formats object for html or text rendering
  def format_object(object, html=true)
    case object.class.name
163 164
    when 'Array'
      object.map {|o| format_object(o, html)}.join(', ').html_safe
165 166 167 168 169 170 171 172 173 174 175 176 177
    when 'Time'
      format_time(object)
    when 'Date'
      format_date(object)
    when 'Fixnum'
      object.to_s
    when 'Float'
      sprintf "%.2f", object
    when 'User'
      html ? link_to_user(object) : object.to_s
    when 'Project'
      html ? link_to_project(object) : object.to_s
    when 'Version'
178
      html ? link_to(object.name, version_path(object)) : object.to_s
179 180 181 182 183 184
    when 'TrueClass'
      l(:general_text_Yes)
    when 'FalseClass'
      l(:general_text_No)
    when 'Issue'
      object.visible? && html ? link_to_issue(object) : "##{object.id}"
185 186 187 188 189 190 191 192 193 194 195
    when 'CustomValue', 'CustomFieldValue'
      if object.custom_field
        f = object.custom_field.format.formatted_custom_value(self, object, html)
        if f.nil? || f.is_a?(String)
          f
        else
          format_object(f, html)
        end
      else
        object.value.to_s
      end
196 197 198 199 200
    else
      html ? h(object) : object.to_s
    end
  end

201 202 203 204
  def wiki_page_path(page, options={})
    url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
  end

205
  def thumbnail_tag(attachment)
206 207
    link_to image_tag(thumbnail_path(attachment)),
      named_attachment_path(attachment, attachment.filename),
208 209 210
      :title => attachment.filename
  end

jplang's avatar
jplang committed
211
  def toggle_link(name, id, options={})
212 213
    onclick = "$('##{id}').toggle(); "
    onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
jplang's avatar
jplang committed
214 215 216
    onclick << "return false;"
    link_to(name, "#", :onclick => onclick)
  end
217

218 219
  def image_to_function(name, function, html_options = {})
    html_options.symbolize_keys!
220 221 222
    tag(:input, html_options.merge({
        :type => "image", :src => image_path(name),
        :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
223 224
        }))
  end
225

226
  def format_activity_title(text)
227
    h(truncate_single_line_raw(text, 100))
228
  end
229

230
  def format_activity_day(date)
231
    date == User.current.today ? l(:label_today).titleize : format_date(date)
232
  end
233

234
  def format_activity_description(text)
235 236
    h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
       ).gsub(/[\r\n]+/, "<br />").html_safe
237
  end
238

239 240
  def format_version_name(version)
    if version.project == @project
241
      h(version)
242 243 244 245
    else
      h("#{version.project} - #{version}")
    end
  end
246

247 248 249 250 251
  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
252

253 254 255 256 257 258 259 260
  # 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
261
      projects.sort_by(&:lft).each do |project|
262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285
        # 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

286
  def render_page_hierarchy(pages, node=nil, options={})
287 288 289 290 291
    content = ''
    if pages[node]
      content << "<ul class=\"pages-hierarchy\">\n"
      pages[node].each do |page|
        content << "<li>"
jplang's avatar
jplang committed
292
        content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
293 294
                           :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]
295 296 297 298
        content << "</li>\n"
      end
      content << "</ul>\n"
    end
299
    content.html_safe
300
  end
301

302 303 304 305
  # Renders flash messages
  def render_flash_messages
    s = ''
    flash.each do |k,v|
306
      s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
307
    end
308
    s.html_safe
309
  end
310

jplang's avatar
jplang committed
311 312 313 314 315 316 317 318
  # 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
319

320 321
  # Renders the project quick-jump box
  def render_project_jump_box
322
    return unless User.current.logged?
323
    projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq
324
    if projects.any?
emassip's avatar
emassip committed
325 326 327 328 329 330
      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) }
331
      end
emassip's avatar
emassip committed
332 333

      select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
334 335
    end
  end
336

337 338 339
  def project_tree_options_for_select(projects, options = {})
    s = ''
    project_tree(projects) do |project, level|
emassip's avatar
emassip committed
340
      name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
341 342 343 344 345 346
      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
347 348 349
      tag_options.merge!(yield(project)) if block_given?
      s << content_tag('option', name_prefix + h(project), tag_options)
    end
350
    s.html_safe
351
  end
352

353
  # Yields the given block for each project with its level in the tree
354 355
  #
  # Wrapper for Project#project_tree
356
  def project_tree(projects, &block)
357
    Project.project_tree(projects, &block)
358
  end
359

jplang's avatar
jplang committed
360 361
  def principals_check_box_tags(name, principals)
    s = ''
362
    principals.each do |principal|
jplang's avatar
jplang committed
363
      s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
jplang's avatar
jplang committed
364
    end
365
    s.html_safe
jplang's avatar
jplang committed
366
  end
367

368 369 370
  # Returns a string for users/groups option tags
  def principals_options_for_select(collection, selected=nil)
    s = ''
371
    if collection.include?(User.current)
jplang's avatar
jplang committed
372
      s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
373
    end
374 375
    groups = ''
    collection.sort.each do |element|
376
      selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
377 378 379 380 381
      (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
382
    s.html_safe
383
  end
384

385 386 387 388
  # 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|
389
      {:disabled => principal.projects.to_a.include?(p)}
390 391 392 393
    end
    options
  end

394 395 396 397
  def option_tag(name, text, value, selected=nil, options={})
    content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
  end

398 399
  # Truncates and returns the string as a single line
  def truncate_single_line(string, *args)
400 401 402 403
    ActiveSupport::Deprecation.warn(
      "ApplicationHelper#truncate_single_line is deprecated and will be removed in Rails 4 poring")
    # Rails 4 ActionView::Helpers::TextHelper#truncate escapes.
    # So, result is broken.
404
    truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
405
  end
406

407 408 409 410
  def truncate_single_line_raw(string, length)
    string.truncate(length).gsub(%r{[\r\n]+}m, ' ')
  end

411 412 413 414 415 416 417 418 419
  # 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
420

jplang's avatar
jplang committed
421 422 423 424
  def anchor(text)
    text.to_s.gsub(' ', '_')
  end

425
  def html_hours(text)
426
    text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
427
  end
428

jplang's avatar
jplang committed
429
  def authoring(created, author, options={})
430
    l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
431
  end
432

433 434 435
  def time_tag(time)
    text = distance_of_time_in_words(Time.now, time)
    if @project
436
      link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time))
437
    else
438
      content_tag('abbr', text, :title => format_time(time))
439
    end
440 441
  end

442 443 444 445 446 447
  def syntax_highlight_lines(name, content)
    lines = []
    syntax_highlight(name, content).each_line { |line| lines << line }
    lines
  end

448
  def syntax_highlight(name, content)
449
    Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
450
  end
451

jplang's avatar
jplang committed
452
  def to_path_param(path)
453 454
    str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
    str.blank? ? nil : str
jplang's avatar
jplang committed
455 456
  end

jplang's avatar
jplang committed
457
  def reorder_links(name, url, method = :post)
458 459
    link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
            url.merge({"#{name}[move_to]" => 'highest'}),
jplang's avatar
jplang committed
460
            :method => method, :title => l(:label_sort_highest)) +
461 462
    link_to(image_tag('1uparrow.png',   :alt => l(:label_sort_higher)),
            url.merge({"#{name}[move_to]" => 'higher'}),
jplang's avatar
jplang committed
463
           :method => method, :title => l(:label_sort_higher)) +
464 465
    link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
            url.merge({"#{name}[move_to]" => 'lower'}),
jplang's avatar
jplang committed
466
            :method => method, :title => l(:label_sort_lower)) +
467 468
    link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
            url.merge({"#{name}[move_to]" => 'lowest'}),
jplang's avatar
jplang committed
469
           :method => method, :title => l(:label_sort_lowest))
jplang's avatar
jplang committed
470
  end
471

472
  def breadcrumb(*args)
473
    elements = args.flatten
474
    elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
475
  end
476

477
  def other_formats_links(&block)
jplang's avatar
jplang committed
478
    concat('<p class="other-formats">'.html_safe + l(:label_export_to))
479
    yield Redmine::Views::OtherFormatsBuilder.new(self)
jplang's avatar
jplang committed
480
    concat('</p>'.html_safe)
481
  end
482

483 484 485 486 487
  def page_header_title
    if @project.nil? || @project.new_record?
      h(Setting.app_title)
    else
      b = []
jplang's avatar
jplang committed
488
      ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
489 490
      if ancestors.any?
        root = ancestors.shift
491
        b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
492
        if ancestors.size > 2
493
          b << "\xe2\x80\xa6"
494 495
          ancestors = ancestors[-2, 2]
        end
496
        b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
497 498
      end
      b << h(@project)
499
      b.join(" \xc2\xbb ").html_safe
500 501
    end
  end
502

503
  # Returns a h2 tag and sets the html title with the given arguments
504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521
  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'
522 523
  def html_title(*args)
    if args.empty?
524
      title = @html_title || []
tmaruyama's avatar
tmaruyama committed
525
      title << @project.name if @project
526
      title << Setting.app_title unless Setting.app_title == title.last
527
      title.reject(&:blank?).join(' - ')
528 529 530 531
    else
      @html_title ||= []
      @html_title += args
    end
532
  end
jplang's avatar
jplang committed
533

534 535 536 537 538 539 540 541
  # 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

542
    css << 'project-' + @project.identifier if @project && @project.identifier.present?
543 544
    css << 'controller-' + controller_name
    css << 'action-' + action_name
545 546 547
    css.join(' ')
  end

jplang's avatar
jplang committed
548
  def accesskey(s)
549 550 551 552 553
    @used_accesskeys ||= []
    key = Redmine::AccessKeys.key_for(s)
    return nil if @used_accesskeys.include?(key)
    @used_accesskeys << key
    key
jplang's avatar
jplang committed
554 555
  end

556 557 558 559 560 561 562 563
  # 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
564
      obj = options[:object]
jplang's avatar
jplang committed
565
      text = args.shift
566 567
    when 2
      obj = args.shift
568 569
      attr = args.shift
      text = obj.send(attr).to_s
570 571 572
    else
      raise ArgumentError, 'invalid arguments to textilizable'
    end
jplang's avatar
jplang committed
573
    return '' if text.blank?
574 575
    project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
    only_path = options.delete(:only_path) == false ? false : true
576

577 578
    text = text.dup
    macros = catch_macros(text)
579
    text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
580

581
    @parsed_headings = []
582
    @heading_anchors = {}
583
    @current_section = 0 if options[:edit_section_links]
584 585

    parse_sections(text, project, obj, attr, only_path, options)
586 587
    text = parse_non_pre_blocks(text, obj, macros) do |text|
      [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
588 589
        send method_name, text, project, obj, attr, only_path, options
      end
590
    end
591
    parse_headings(text, project, obj, attr, only_path, options)
592

593 594 595
    if @parsed_headings.any?
      replace_toc(text, @parsed_headings)
    end
596

jplang's avatar
jplang committed
597
    text.html_safe
598
  end
599

600
  def parse_non_pre_blocks(text, obj, macros)
601 602 603 604 605 606 607 608
    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
609 610 611
        inject_macros(text, obj, macros) if macros.any?
      else
        inject_macros(text, obj, macros, false) if macros.any?
612 613 614 615 616 617 618 619 620 621 622 623 624
      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
625 626 627 628
    # Close any non closing tags
    while tag = tags.pop
      parsed << "</#{tag}>"
    end
629
    parsed
630
  end
631

632
  def parse_inline_attachments(text, project, obj, attr, only_path, options)
633
    # when using an image link, try to use an attachment, if possible
jplang's avatar
jplang committed
634 635 636
    attachments = options[:attachments] || []
    attachments += obj.attachments if obj.respond_to?(:attachments)
    if attachments.present?
637
      text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
638
        filename, ext, alt, alttext = $1.downcase, $2, $3, $4
639
        # search for the picture in attachments
640
        if found = Attachment.latest_attach(attachments, filename)
641
          image_url = download_named_attachment_path(found, found.filename, :only_path => only_path)
642 643 644 645
          desc = found.description.to_s.gsub('"', '')
          if !desc.blank? && alttext.blank?
            alt = " title=\"#{desc}\" alt=\"#{desc}\""
          end
646
          "src=\"#{image_url}\"#{alt}"
647
        else
648
          m
649 650 651
        end
      end
    end
652
  end
653

654 655 656 657 658 659 660 661 662 663 664 665
  # 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|
666
      link_project = project
667 668 669
      esc, all, page, title = $1, $2, $3, $5
      if esc.nil?
        if page =~ /^([^\:]+)\:(.*)$/
670 671 672
          identifier, page = $1, $2
          link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
          title ||= identifier if page.blank?
673
        end
674

675
        if link_project && link_project.wiki
676 677 678 679 680
          # extract anchor
          anchor = nil
          if page =~ /^(.+?)\#(.+)$/
            page, anchor = $1, $2
          end
681
          anchor = sanitize_anchor_name(anchor) if anchor.present?
682 683
          # check if page exists
          wiki_page = link_project.wiki.find_page(page)
684 685 686 687
          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]
688
            when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
689
            when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
690
            else
691
              wiki_page_id = page.present? ? Wiki.titleize(page) : nil
692
              parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
693
              url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
jplang's avatar
jplang committed
694
               :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
695
            end
696
          end
emassip's avatar
emassip committed
697
          link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
698 699
        else
          # project or wiki doesn't exist
700
          all
701
        end
jplang's avatar
jplang committed
702
      else
703
        all
jplang's avatar
jplang committed
704
      end
705
    end
706
  end
707

708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731
  # 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
732
  #   Forum messages:
733
  #     message#1218 -> Link to message with id 1218
734 735 736
  #  Projects:
  #     project:someproject -> Link to project named "someproject"
  #     project#3 -> Link to project with id 3
737 738 739 740 741 742
  #
  #   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
743
  def parse_redmine_links(text, default_project, obj, attr, only_path, options)
744
    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|
745
      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
746
      link = nil
747
      project = default_project
748 749 750
      if project_identifier
        project = Project.visible.find_by_identifier(project_identifier)
      end
751 752
      if esc.nil?
        if prefix.nil? && sep == 'r'
753 754 755 756 757 758 759 760
          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
761 762 763 764 765 766 767 768 769
            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',
770
                             :title => truncate_single_line_raw(changeset.comments, 100))
771
            end
772 773
          end
        elsif sep == '#'
774
          oid = identifier.to_i
775 776
          case prefix
          when nil
777 778
            if oid.to_s == identifier &&
                  issue = Issue.visible.includes(:status).find_by_id(oid)
779
              anchor = comment_id ? "note-#{comment_id}" : nil
780 781 782 783
              link = link_to(h("##{oid}#{comment_suffix}"),
                             {:only_path => only_path, :controller => 'issues',
                              :action => 'show', :id => oid, :anchor => anchor},
                             :class => issue.css_classes,
784
                             :title => "#{issue.subject.truncate(100)} (#{issue.status.name})")
785 786
            end
          when 'document'
787
            if document = Document.visible.find_by_id(oid)
jplang's avatar
jplang committed
788 789
              link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
                                                :class => 'document'
790 791
            end
          when 'version'
792
            if version = Version.visible.find_by_id(oid)
jplang's avatar
jplang committed
793 794
              link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
                                              :class => 'version'
795
            end
796
          when 'message'
797
            if message = Message.visible.includes(:parent).find_by_id(oid)
798
              link = link_to_message(message, {:only_path => only_path}, :class => 'message')
799
            end
800 801 802 803 804 805 806 807 808 809
          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
810 811
          when 'project'
            if p = Project.visible.find_by_id(oid)
812
              link = link_to_project(p, {:only_path => only_path}, :class => 'project')
jplang's avatar
jplang committed
813
            end
814 815 816
          end
        elsif sep == ':'
          # removes the double quotes if any
817
          name = identifier.gsub(%r{^"(.*)"$}, "\\1")
818 819
          case prefix
          when 'document'
820
            if project && document = project.documents.visible.find_by_title(name)
jplang's avatar
jplang committed
821 822
              link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
                                                :class => 'document'
823 824
            end
          when 'version'
825
            if project && version = project.versions.visible.find_by_name(name)
jplang's avatar
jplang committed
826 827
              link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
                                              :class => 'version'
828
            end
829 830 831 832 833 834 835 836 837 838
          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
839 840 841
          when 'commit', 'source', 'export'
            if project
              repository = nil
842
              if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
843 844 845 846 847 848
                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
849
                if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
850 851
                  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',
852
                                               :title => truncate_single_line_raw(changeset.comments, 100)
853 854 855
                end
              else
                if repository && User.current.allowed_to?(:browse_repository, project)
856
                  name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
857
                  path, rev, anchor = $1, $3, $5
858
                  link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
859 860
                                                          :path => to_path_param(path),
                                                          :rev => rev,
861
                                                          :anchor => anchor},
862 863 864 865
                                                         :class => (prefix == 'export' ? 'source download' : 'source')
                end
              end
              repo_prefix = nil
866
            end
867
          when 'attachment'
868 869
            attachments = options[:attachments] || []
            attachments += obj.attachments if obj.respond_to?(:attachments)
870
            if attachments && attachment = Attachment.latest_attach(attachments, name)
jplang's avatar
jplang committed
871
              link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
872
            end
jplang's avatar
jplang committed
873
          when 'project'
jplang's avatar
jplang committed
874
            if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
875
              link = link_to_project(p, {:only_path => only_path}, :class => 'project')
jplang's avatar
jplang committed
876
            end
877
          end
jplang's avatar
jplang committed
878
        end
879
      end
880
      (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
881
    end
882
  end
883

jplang's avatar
jplang committed
884
  HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
885 886 887 888

  def parse_sections(text, project, obj, attr, only_path, options)
    return unless options[:edit_section_links]
    text.gsub!(HEADING_RE) do
889
      heading = $1
890 891
      @current_section += 1
      if @current_section > 1
892
        content_tag('div',
893
          link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
894
          :class => 'contextual',
895 896
          :title => l(:button_edit_section),
          :id => "section-#{@current_section}") + heading.html_safe
897
      else
898
        heading
899 900 901
      end
    end
  end
902

903
  # Headings and TOC
904
  # Adds ids and links to headings unless options[:headings] is set to false
905
  def parse_headings(text, project, obj, attr, only_path, options)
906
    return if options[:headings] == false
907

908
    text.gsub!(HEADING_RE) do
909
      level, attrs, content = $2.to_i, $3, $4
jplang's avatar