application_helper.rb 47.7 KB
Newer Older
1 2
# encoding: utf-8
#
3
# Redmine - project management software
jplang's avatar
jplang committed
4
# Copyright (C) 2006-2015  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]
jplang's avatar
jplang committed
83
    s = link_to(text, issue_url(issue, :only_path => only_path),
84
                :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
jplang's avatar
jplang committed
96
    route_method = options.delete(:download) ? :download_named_attachment_url : :named_attachment_url
97
    html_options = options.slice!(:only_path)
jplang's avatar
jplang committed
98
    options[:only_path] = true unless options.key?(:only_path)
99 100
    url = send(route_method, attachment, attachment.filename, options)
    link_to text, url, html_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)),
        :accesskey => options[:accesskey]
117
      )
118
  end
119

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

133 134
  # Generates a link to a project if active
  # Examples:
135
  #
136 137 138 139 140
  #   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)
141
    if project.archived?
142 143
      h(project.name)
    else
jplang's avatar
jplang committed
144 145 146
      link_to project.name,
        project_url(project, {:only_path => true}.merge(options)),
        html_options
147 148 149 150 151 152 153 154 155 156 157
    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
158 159 160
    end
  end

161 162 163 164 165 166 167
  # Generates a link to a version
  def link_to_version(version, options = {})
    return '' unless version && version.is_a?(Version)
    options = {:title => format_date(version.effective_date)}.merge(options)
    link_to_if version.visible?, format_version_name(version), version_path(version), options
  end

168
  # Helper that formats object for html or text rendering
169 170 171 172
  def format_object(object, html=true, &block)
    if block_given?
      object = yield object
    end
173
    case object.class.name
174 175
    when 'Array'
      object.map {|o| format_object(o, html)}.join(', ').html_safe
176 177 178 179 180 181 182 183 184 185 186 187 188
    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'
189
      html ? link_to_version(object) : object.to_s
190 191 192 193 194 195
    when 'TrueClass'
      l(:general_text_Yes)
    when 'FalseClass'
      l(:general_text_No)
    when 'Issue'
      object.visible? && html ? link_to_issue(object) : "##{object.id}"
196 197 198 199 200 201
    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
202
          format_object(f, html, &block)
203 204 205 206
        end
      else
        object.value.to_s
      end
207 208 209 210 211
    else
      html ? h(object) : object.to_s
    end
  end

212 213 214 215
  def wiki_page_path(page, options={})
    url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
  end

216
  def thumbnail_tag(attachment)
217 218
    link_to image_tag(thumbnail_path(attachment)),
      named_attachment_path(attachment, attachment.filename),
219 220 221
      :title => attachment.filename
  end

jplang's avatar
jplang committed
222
  def toggle_link(name, id, options={})
223 224
    onclick = "$('##{id}').toggle(); "
    onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
jplang's avatar
jplang committed
225 226 227
    onclick << "return false;"
    link_to(name, "#", :onclick => onclick)
  end
228

229
  def format_activity_title(text)
230
    h(truncate_single_line_raw(text, 100))
231
  end
232

233
  def format_activity_day(date)
234
    date == User.current.today ? l(:label_today).titleize : format_date(date)
235
  end
236

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

242
  def format_version_name(version)
243
    if !version.shared? || version.project == @project
244
      h(version)
245 246 247 248
    else
      h("#{version.project} - #{version}")
    end
  end
249

250 251 252 253 254
  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
255

256 257 258
  # 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)
259
  def render_project_nested_lists(projects, &block)
260 261 262 263
    s = ''
    if projects.any?
      ancestors = []
      original_project = @project
jplang's avatar
jplang committed
264
      projects.sort_by(&:lft).each do |project|
265 266 267 268 269 270 271 272 273 274 275 276 277 278
        # 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}'>"
279
        s << h(block_given? ? capture(project, &block) : project.name)
280 281 282 283 284 285 286 287 288
        s << "</div>\n"
        ancestors << project
      end
      s << ("</li></ul>\n" * ancestors.size)
      @project = original_project
    end
    s.html_safe
  end

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

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

jplang's avatar
jplang committed
314
  # Renders tabs and their content
315
  def render_tabs(tabs, selected=params[:tab])
jplang's avatar
jplang committed
316
    if tabs.any?
317 318 319 320 321
      unless tabs.detect {|tab| tab[:name] == selected}
        selected = nil
      end
      selected ||= tabs.first[:name]
      render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected}
jplang's avatar
jplang committed
322 323 324 325
    else
      content_tag 'p', l(:label_no_data), :class => "nodata"
    end
  end
326

327 328
  # Renders the project quick-jump box
  def render_project_jump_box
329
    return unless User.current.logged?
330
    projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq
331
    if projects.any?
emassip's avatar
emassip committed
332 333 334 335 336 337
      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) }
338
      end
emassip's avatar
emassip committed
339 340

      select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
341 342
    end
  end
343

344
  def project_tree_options_for_select(projects, options = {})
345 346 347 348
    s = ''.html_safe
    if options[:include_blank]
      s << content_tag('option', '&nbsp;'.html_safe, :value => '')
    end
349
    project_tree(projects) do |project, level|
emassip's avatar
emassip committed
350
      name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
351 352 353 354 355 356
      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
357 358 359
      tag_options.merge!(yield(project)) if block_given?
      s << content_tag('option', name_prefix + h(project), tag_options)
    end
360
    s.html_safe
361
  end
362

363
  # Yields the given block for each project with its level in the tree
364 365
  #
  # Wrapper for Project#project_tree
366
  def project_tree(projects, &block)
367
    Project.project_tree(projects, &block)
368
  end
369

jplang's avatar
jplang committed
370 371
  def principals_check_box_tags(name, principals)
    s = ''
372
    principals.each do |principal|
jplang's avatar
jplang committed
373
      s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
jplang's avatar
jplang committed
374
    end
375
    s.html_safe
jplang's avatar
jplang committed
376
  end
377

378 379 380
  # Returns a string for users/groups option tags
  def principals_options_for_select(collection, selected=nil)
    s = ''
381
    if collection.include?(User.current)
jplang's avatar
jplang committed
382
      s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
383
    end
384 385
    groups = ''
    collection.sort.each do |element|
386
      selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
387 388 389 390 391
      (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
392
    s.html_safe
393
  end
394

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

399
  def truncate_single_line_raw(string, length)
jplang's avatar
jplang committed
400
    string.to_s.truncate(length).gsub(%r{[\r\n]+}m, ' ')
401 402
  end

403 404 405 406 407 408 409 410 411
  # 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
412

jplang's avatar
jplang committed
413 414 415 416
  def anchor(text)
    text.to_s.gsub(' ', '_')
  end

417
  def html_hours(text)
418
    text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
419
  end
420

jplang's avatar
jplang committed
421
  def authoring(created, author, options={})
422
    l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
423
  end
424

425 426 427
  def time_tag(time)
    text = distance_of_time_in_words(Time.now, time)
    if @project
jplang's avatar
jplang committed
428
      link_to(text, project_activity_path(@project, :from => User.current.time_to_date(time)), :title => format_time(time))
429
    else
430
      content_tag('abbr', text, :title => format_time(time))
431
    end
432 433
  end

434 435 436 437 438 439
  def syntax_highlight_lines(name, content)
    lines = []
    syntax_highlight(name, content).each_line { |line| lines << line }
    lines
  end

440
  def syntax_highlight(name, content)
441
    Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
442
  end
443

jplang's avatar
jplang committed
444
  def to_path_param(path)
445 446
    str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
    str.blank? ? nil : str
jplang's avatar
jplang committed
447 448
  end

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

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

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

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

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

526 527 528 529 530 531 532 533
  # 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

534
    css << 'project-' + @project.identifier if @project && @project.identifier.present?
535 536
    css << 'controller-' + controller_name
    css << 'action-' + action_name
537 538 539
    css.join(' ')
  end

jplang's avatar
jplang committed
540
  def accesskey(s)
541 542 543 544 545
    @used_accesskeys ||= []
    key = Redmine::AccessKeys.key_for(s)
    return nil if @used_accesskeys.include?(key)
    @used_accesskeys << key
    key
jplang's avatar
jplang committed
546 547
  end

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

569 570
    text = text.dup
    macros = catch_macros(text)
571
    text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
572

573
    @parsed_headings = []
574
    @heading_anchors = {}
575
    @current_section = 0 if options[:edit_section_links]
576 577

    parse_sections(text, project, obj, attr, only_path, options)
578 579
    text = parse_non_pre_blocks(text, obj, macros) do |text|
      [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
580 581
        send method_name, text, project, obj, attr, only_path, options
      end
582
    end
583
    parse_headings(text, project, obj, attr, only_path, options)
584

585 586 587
    if @parsed_headings.any?
      replace_toc(text, @parsed_headings)
    end
588

jplang's avatar
jplang committed
589
    text.html_safe
590
  end
591

592
  def parse_non_pre_blocks(text, obj, macros)
593 594 595 596 597 598 599 600
    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
601 602 603
        inject_macros(text, obj, macros) if macros.any?
      else
        inject_macros(text, obj, macros, false) if macros.any?
604 605 606 607 608 609 610 611 612 613 614 615 616
      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
617 618 619 620
    # Close any non closing tags
    while tag = tags.pop
      parsed << "</#{tag}>"
    end
621
    parsed
622
  end
623

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

646 647 648 649 650 651 652 653 654 655 656 657
  # 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|
658
      link_project = project
659 660 661
      esc, all, page, title = $1, $2, $3, $5
      if esc.nil?
        if page =~ /^([^\:]+)\:(.*)$/
662 663 664
          identifier, page = $1, $2
          link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
          title ||= identifier if page.blank?
665
        end
666

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

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

jplang's avatar
jplang committed
872
  HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
873 874 875 876

  def parse_sections(text, project, obj, attr, only_path, options)
    return unless options[:edit_section_links]
    text.gsub!(HEADING_RE) do
877
      heading = $1
878 879
      @current_section += 1
      if @current_section > 1
880
        content_tag('div',
881
          link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
882
          :class => 'contextual',
883 884
          :title => l(:button_edit_section),
          :id => "section-#{@current_section}") + heading.html_safe
885
      else
886
        heading
887 888 889
      end
    end
  end
890

891
  # Headings and TOC
892
  # Adds ids and links to headings unless options[:headings] is set to false
893
  def parse_headings(text, project, obj, attr, only_path, options)
894
    return if options[:headings] == false
895

896
    text.gsub!(HEADING_RE) do
897
      level, attrs, content = $2.to_i, $3, $4
898
      item = strip_tags(content).strip
899
      anchor = sanitize_anchor_name(item)
900 901
      # 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))
902 903 904 905 906
      @heading_anchors[anchor] ||= 0
      idx = (@heading_anchors[anchor] += 1)
      if idx > 1
        anchor = "#{anchor}-#{idx}"
      end
907
      @parsed_headings << [level, anchor, item]
908
      "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
909 910
    end
  end
911

912
  MACROS_RE = /(
913 914 915 916
                (!)?                        # escaping
                (
                \{\{                        # opening tag
                ([\w]+)                     # macro name
917
                (\(([^\n\r]*?)\))?          # optional arguments
918
                ([\n\r].*?[\n\r])?          # optional block of text
919 920
                \}\}                        # closing tag
                )
921
               )/mx unless const_defined?(:MACROS_RE)
922 923 924 925 926

  MACRO_SUB_RE