application_helper.rb 49.6 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
  def reorder_handle(object, options={})
    data = {
      :reorder_url => options[:url] || url_for(object),
      :reorder_param => options[:param] || object.class.name.underscore
    }
    content_tag('span', '',
jplang's avatar
jplang committed
480
      :class => "sort-handle",
481 482
      :data => data,
      :title => l(:button_sort))
483 484
  end

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

920
  # Headings and TOC
921
  # Adds ids and links to headings unless options[:headings] is set to false