application_helper.rb 49.4 KB
Newer Older
1 2
# encoding: utf-8
#
3
# Redmine - project management software
jplang's avatar
jplang committed
4
# Copyright (C) 2006-2016  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
  include Redmine::SudoMode::Helper
29
  include Redmine::Themes::Helper
30
  include Redmine::Hook::Helper
31

32 33 34
  extend Forwardable
  def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter

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

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

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

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

93 94 95 96 97 98
  # 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
99
    route_method = options.delete(:download) ? :download_named_attachment_url : :named_attachment_url
100
    html_options = options.slice!(:only_path)
jplang's avatar
jplang committed
101
    options[:only_path] = true unless options.key?(:only_path)
102 103
    url = send(route_method, attachment, attachment.filename, options)
    link_to text, url, html_options
104
  end
105

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

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

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

164 165 166 167 168 169 170
  # 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

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

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

219
  def thumbnail_tag(attachment)
220 221
    link_to image_tag(thumbnail_path(attachment)),
      named_attachment_path(attachment, attachment.filename),
222 223 224
      :title => attachment.filename
  end

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

232
  def format_activity_title(text)
233
    h(truncate_single_line_raw(text, 100))
234
  end
235

236
  def format_activity_day(date)
237
    date == User.current.today ? l(:label_today).titleize : format_date(date)
238
  end
239

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

245
  def format_version_name(version)
246
    if version.project == @project
247
      h(version)
248 249 250 251
    else
      h("#{version.project} - #{version}")
    end
  end
252

253 254 255 256 257
  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
258

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

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

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

jplang's avatar
jplang committed
317
  # Renders tabs and their content
318
  def render_tabs(tabs, selected=params[:tab])
jplang's avatar
jplang committed
319
    if tabs.any?
320 321 322 323 324
      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
325 326 327 328
    else
      content_tag 'p', l(:label_no_data), :class => "nodata"
    end
  end
329

330 331
  # Renders the project quick-jump box
  def render_project_jump_box
332
    return unless User.current.logged?
333
    projects = User.current.projects.active.select(:id, :name, :identifier, :lft, :rgt).to_a
334
    if projects.any?
emassip's avatar
emassip committed
335 336 337 338 339 340
      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) }
341
      end
emassip's avatar
emassip committed
342

343
      content_tag( :span, nil, :class => 'jump-box-arrow') +
emassip's avatar
emassip committed
344
      select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
345 346
    end
  end
347

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

370
  # Yields the given block for each project with its level in the tree
371 372
  #
  # Wrapper for Project#project_tree
373
  def project_tree(projects, &block)
374
    Project.project_tree(projects, &block)
375
  end
376

jplang's avatar
jplang committed
377 378
  def principals_check_box_tags(name, principals)
    s = ''
379
    principals.each do |principal|
jplang's avatar
jplang committed
380
      s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
jplang's avatar
jplang committed
381
    end
382
    s.html_safe
jplang's avatar
jplang committed
383
  end
384

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

402 403 404 405
  def option_tag(name, text, value, selected=nil, options={})
    content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
  end

406
  def truncate_single_line_raw(string, length)
jplang's avatar
jplang committed
407
    string.to_s.truncate(length).gsub(%r{[\r\n]+}m, ' ')
408 409
  end

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

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

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

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

432 433 434
  def time_tag(time)
    text = distance_of_time_in_words(Time.now, time)
    if @project
jplang's avatar
jplang committed
435
      link_to(text, project_activity_path(@project, :from => User.current.time_to_date(time)), :title => format_time(time))
436
    else
437
      content_tag('abbr', text, :title => format_time(time))
438
    end
439 440
  end

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

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

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

jplang's avatar
jplang committed
456
  def reorder_links(name, url, method = :post)
jplang's avatar
jplang committed
457 458 459
    # TODO: remove associated styles from application.css too
    ActiveSupport::Deprecation.warn "Application#reorder_links will be removed in Redmine 4."

460
    link_to(l(:label_sort_highest),
461 462
            url.merge({"#{name}[move_to]" => 'highest'}), :method => method,
            :title => l(:label_sort_highest), :class => 'icon-only icon-move-top') +
463
    link_to(l(:label_sort_higher),
464 465
            url.merge({"#{name}[move_to]" => 'higher'}), :method => method,
            :title => l(:label_sort_higher), :class => 'icon-only icon-move-up') +
466
    link_to(l(:label_sort_lower),
467 468
            url.merge({"#{name}[move_to]" => 'lower'}), :method => method,
            :title => l(:label_sort_lower), :class => 'icon-only icon-move-down') +
469
    link_to(l(:label_sort_lowest),
470 471
            url.merge({"#{name}[move_to]" => 'lowest'}), :method => method,
            :title => l(:label_sort_lowest), :class => 'icon-only icon-move-bottom')
jplang's avatar
jplang committed
472
  end
473

474 475 476 477 478 479 480 481 482 483
  def reorder_handle(object, options={})
    data = {
      :reorder_url => options[:url] || url_for(object),
      :reorder_param => options[:param] || object.class.name.underscore
    }
    content_tag('span', '',
      :class => "sort-handle ui-icon ui-icon-arrowthick-2-n-s",
      :data => data)
  end

484
  def breadcrumb(*args)
485
    elements = args.flatten
486
    elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
487
  end
488

489
  def other_formats_links(&block)
jplang's avatar
jplang committed
490
    concat('<p class="other-formats">'.html_safe + l(:label_export_to))
491
    yield Redmine::Views::OtherFormatsBuilder.new(self)
jplang's avatar
jplang committed
492
    concat('</p>'.html_safe)
493
  end
494

495 496 497 498 499
  def page_header_title
    if @project.nil? || @project.new_record?
      h(Setting.app_title)
    else
      b = []
jplang's avatar
jplang committed
500
      ancestors = (@project.root? ? [] : @project.ancestors.visible.to_a)
501 502
      if ancestors.any?
        root = ancestors.shift
503
        b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
504
        if ancestors.size > 2
505
          b << "\xe2\x80\xa6"
506 507
          ancestors = ancestors[-2, 2]
        end
508
        b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
509
      end
510 511 512 513 514 515 516
      b << content_tag(:span, h(@project), class: 'current-project')
      if b.size > 1
        separator = content_tag(:span, ' &raquo; '.html_safe, class: 'separator')
        path = safe_join(b[0..-2], separator) + separator
        b = [content_tag(:span, path.html_safe, class: 'breadcrumbs'), b[-1]]
      end
      safe_join b
517 518
    end
  end
519

520
  # Returns a h2 tag and sets the html title with the given arguments
521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538
  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'
539 540
  def html_title(*args)
    if args.empty?
541
      title = @html_title || []
tmaruyama's avatar
tmaruyama committed
542
      title << @project.name if @project
543
      title << Setting.app_title unless Setting.app_title == title.last
544
      title.reject(&:blank?).join(' - ')
545 546 547 548
    else
      @html_title ||= []
      @html_title += args
    end
549
  end
jplang's avatar
jplang committed
550

551 552 553 554 555 556 557 558
  # 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

559
    css << 'project-' + @project.identifier if @project && @project.identifier.present?
560 561
    css << 'controller-' + controller_name
    css << 'action-' + action_name
562 563 564
    css.join(' ')
  end

jplang's avatar
jplang committed
565
  def accesskey(s)
566 567 568 569 570
    @used_accesskeys ||= []
    key = Redmine::AccessKeys.key_for(s)
    return nil if @used_accesskeys.include?(key)
    @used_accesskeys << key
    key
jplang's avatar
jplang committed
571 572
  end

573 574 575 576 577 578 579 580
  # 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
581
      obj = options[:object]
jplang's avatar
jplang committed
582
      text = args.shift
583 584
    when 2
      obj = args.shift
585 586
      attr = args.shift
      text = obj.send(attr).to_s
587 588 589
    else
      raise ArgumentError, 'invalid arguments to textilizable'
    end
jplang's avatar
jplang committed
590
    return '' if text.blank?
591
    project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
592
    @only_path = only_path = options.delete(:only_path) == false ? false : true
593

594 595
    text = text.dup
    macros = catch_macros(text)
596
    text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
597

598
    @parsed_headings = []
599
    @heading_anchors = {}
600
    @current_section = 0 if options[:edit_section_links]
601 602

    parse_sections(text, project, obj, attr, only_path, options)
603 604
    text = parse_non_pre_blocks(text, obj, macros) do |text|
      [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
605 606
        send method_name, text, project, obj, attr, only_path, options
      end
607
    end
608
    parse_headings(text, project, obj, attr, only_path, options)
609

610 611 612
    if @parsed_headings.any?
      replace_toc(text, @parsed_headings)
    end
613

jplang's avatar
jplang committed
614
    text.html_safe
615
  end
616

617
  def parse_non_pre_blocks(text, obj, macros)
618 619 620 621 622 623 624 625
    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
626 627 628
        inject_macros(text, obj, macros) if macros.any?
      else
        inject_macros(text, obj, macros, false) if macros.any?
629 630 631 632
      end
      parsed << text
      if tag
        if closing
633
          if tags.last && tags.last.casecmp(tag) == 0
634 635 636 637 638 639 640 641
            tags.pop
          end
        else
          tags << tag.downcase
        end
        parsed << full_tag
      end
    end
642 643 644 645
    # Close any non closing tags
    while tag = tags.pop
      parsed << "</#{tag}>"
    end
646
    parsed
647
  end
648

649
  def parse_inline_attachments(text, project, obj, attr, only_path, options)
650 651
    return if options[:inline_attachments] == false

652
    # when using an image link, try to use an attachment, if possible
jplang's avatar
jplang committed
653 654 655
    attachments = options[:attachments] || []
    attachments += obj.attachments if obj.respond_to?(:attachments)
    if attachments.present?
656
      text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
657
        filename, ext, alt, alttext = $1.downcase, $2, $3, $4
658
        # search for the picture in attachments
659
        if found = Attachment.latest_attach(attachments, CGI.unescape(filename))
jplang's avatar
jplang committed
660
          image_url = download_named_attachment_url(found, found.filename, :only_path => only_path)
661 662 663 664
          desc = found.description.to_s.gsub('"', '')
          if !desc.blank? && alttext.blank?
            alt = " title=\"#{desc}\" alt=\"#{desc}\""
          end
665
          "src=\"#{image_url}\"#{alt}"
666
        else
667
          m
668 669 670
        end
      end
    end
671
  end
672

673 674 675 676 677 678 679 680 681 682 683 684
  # 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|
685
      link_project = project
686 687 688
      esc, all, page, title = $1, $2, $3, $5
      if esc.nil?
        if page =~ /^([^\:]+)\:(.*)$/
689 690 691
          identifier, page = $1, $2
          link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
          title ||= identifier if page.blank?
692
        end
693

694
        if link_project && link_project.wiki
695 696 697 698 699
          # extract anchor
          anchor = nil
          if page =~ /^(.+?)\#(.+)$/
            page, anchor = $1, $2
          end
700
          anchor = sanitize_anchor_name(anchor) if anchor.present?
701 702
          # check if page exists
          wiki_page = link_project.wiki.find_page(page)
703 704 705 706
          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]
707
            when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
708
            when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
709
            else
710
              wiki_page_id = page.present? ? Wiki.titleize(page) : nil
711
              parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
712
              url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
jplang's avatar
jplang committed
713
               :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
714
            end
715
          end
emassip's avatar
emassip committed
716
          link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
717 718
        else
          # project or wiki doesn't exist
719
          all
720
        end
jplang's avatar
jplang committed
721
      else
722
        all
jplang's avatar
jplang committed
723
      end
724
    end
725
  end
726

727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750
  # 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
751
  #   Forum messages:
752
  #     message#1218 -> Link to message with id 1218
753 754 755
  #  Projects:
  #     project:someproject -> Link to project named "someproject"
  #     project#3 -> Link to project with id 3
756 757 758 759 760 761
  #
  #   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
762
  def parse_redmine_links(text, default_project, obj, attr, only_path, options)
763
    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|
764
      tag_content, leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $2, $3, $4, $5, $6, $7, $12, $13, $10 || $14 || $20, $16 || $21, $17, $19
765 766 767 768 769 770 771 772 773 774
      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'
775 776
            if project
              repository = nil
777
              if repo_identifier
778 779 780 781
                repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
              else
                repository = project.repository
              end
782 783 784 785 786 787 788 789 790 791 792
              # 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))
793
              end
794
            end
795 796 797 798 799
          elsif sep == '#'
            oid = identifier.to_i
            case prefix
            when nil
              if oid.to_s == identifier &&
jplang's avatar
jplang committed
800
                issue = Issue.visible.find_by_id(oid)
801
                anchor = comment_id ? "note-#{comment_id}" : nil
jplang's avatar
jplang committed
802
                link = link_to("##{oid}#{comment_suffix}",
jplang's avatar
jplang committed
803
                               issue_url(issue, :only_path => only_path, :anchor => anchor),
804
                               :class => issue.css_classes,
805
                               :title => "#{issue.tracker.name}: #{issue.subject.truncate(100)} (#{issue.status.name})")
806 807 808
              end
            when 'document'
              if document = Document.visible.find_by_id(oid)
jplang's avatar
jplang committed
809
                link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
810 811 812
              end
            when 'version'
              if version = Version.visible.find_by_id(oid)
jplang's avatar
jplang committed
813
                link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
814 815
              end
            when 'message'
jplang's avatar
jplang committed
816
              if message = Message.visible.find_by_id(oid)
817 818 819 820
                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
821
                link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
822 823 824
              end
            when 'news'
              if news = News.visible.find_by_id(oid)
jplang's avatar
jplang committed
825
                link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
826 827 828 829 830
              end
            when 'project'
              if p = Project.visible.find_by_id(oid)
                link = link_to_project(p, {:only_path => only_path}, :class => 'project')
              end
831
            end
832 833 834 835 836 837 838
          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
839
                link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
840 841 842
              end
            when 'version'
              if project && version = project.versions.visible.find_by_name(name)
jplang's avatar
jplang committed
843
                link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
844 845 846
              end
            when 'forum'
              if project && board = project.boards.visible.find_by_name(name)
jplang's avatar
jplang committed
847
                link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
848 849 850
              end
            when 'news'
              if project && news = project.news.visible.find_by_title(name)
jplang's avatar
jplang committed
851
                link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890
              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
891
            end
892
          end
jplang's avatar
jplang committed
893
        end
894
        (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
895 896
      end
    end
897
  end
898

jplang's avatar
jplang committed
899
  HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
900 901 902 903

  def parse_sections(text, project, obj, attr, only_path, options)
    return unless options[:edit_section_links]
    text.gsub!(HEADING_RE) do
904
      heading, level = $1, $2
905 906
      @current_section += 1
      if @current_section > 1
907
        content_tag('div',
908
          link_to(l(:button_edit_section), options[:edit_section_links].merge(:section => @current_section),
909
                  :class => 'icon-only icon-edit'),
910
          :class => "contextual heading-#{level}",
911 912
          :title => l(:button_edit_section),
          :id => "section-#{@current_section}") + heading.html_safe
913
      else
914
        heading
915 916 917
      end
    end
  end
918

919
  # Headings and TOC
920
  # Adds ids and links to headings unless options[:headings] is set to false
921
  def parse_headings(text, project, obj, attr, only_path, options)
922
    return if options[:headings] == false
923