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

20
require 'forwardable'
21
require 'cgi'
22

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

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

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

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

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

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

90 91 92 93 94 95
  # Generates a link to an attachment.
  # Options:
  # * :text - Link text (default to attachment filename)
  # * :download - Force download (default: false)
  def link_to_attachment(attachment, options={})
    text = options.delete(:text) || attachment.filename
96 97 98 99
    route_method = options.delete(:download) ? :download_named_attachment_path : :named_attachment_path
    html_options = options.slice!(:only_path)
    url = send(route_method, attachment, attachment.filename, options)
    link_to text, url, html_options
100
  end
101

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

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

130 131
  # Generates a link to a project if active
  # Examples:
132
  #
133 134 135 136 137
  #   link_to_project(project)                          # => link to the specified project overview
  #   link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
  #   link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
  #
  def link_to_project(project, options={}, html_options = nil)
138
    if project.archived?
139 140 141
      h(project.name)
    elsif options.key?(:action)
      ActiveSupport::Deprecation.warn "#link_to_project with :action option is deprecated and will be removed in Redmine 3.0."
jplang's avatar
jplang committed
142
      url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
143 144 145 146 147 148 149 150 151 152 153 154 155 156
      link_to project.name, url, html_options
    else
      link_to project.name, project_path(project, options), html_options
    end
  end

  # Generates a link to a project settings if active
  def link_to_project_settings(project, options={}, html_options=nil)
    if project.active?
      link_to project.name, settings_project_path(project, options), html_options
    elsif project.archived?
      h(project.name)
    else
      link_to project.name, project_path(project, options), html_options
157 158 159
    end
  end

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

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

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

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

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

228 229
  def image_to_function(name, function, html_options = {})
    html_options.symbolize_keys!
230 231 232
    tag(:input, html_options.merge({
        :type => "image", :src => image_path(name),
        :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
233 234
        }))
  end
235

236
  def format_activity_title(text)
237
    h(truncate_single_line_raw(text, 100))
238
  end
239

240
  def format_activity_day(date)
241
    date == User.current.today ? l(:label_today).titleize : format_date(date)
242
  end
243

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

249
  def format_version_name(version)
250
    if !version.shared? || version.project == @project
251
      h(version)
252 253 254 255
    else
      h("#{version.project} - #{version}")
    end
  end
256

257 258 259 260 261
  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
262

263 264 265 266 267 268 269 270
  # Renders a tree of projects as a nested set of unordered lists
  # The given collection may be a subset of the whole project tree
  # (eg. some intermediate nodes are private and can not be seen)
  def render_project_nested_lists(projects)
    s = ''
    if projects.any?
      ancestors = []
      original_project = @project
jplang's avatar
jplang committed
271
      projects.sort_by(&:lft).each do |project|
272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295
        # set the project environment to please macros.
        @project = project
        if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
          s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
        else
          ancestors.pop
          s << "</li>"
          while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
            ancestors.pop
            s << "</ul></li>\n"
          end
        end
        classes = (ancestors.empty? ? 'root' : 'child')
        s << "<li class='#{classes}'><div class='#{classes}'>"
        s << h(block_given? ? yield(project) : project.name)
        s << "</div>\n"
        ancestors << project
      end
      s << ("</li></ul>\n" * ancestors.size)
      @project = original_project
    end
    s.html_safe
  end

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

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

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

334 335
  # Renders the project quick-jump box
  def render_project_jump_box
336
    return unless User.current.logged?
337
    projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq
338
    if projects.any?
emassip's avatar
emassip committed
339 340 341 342 343 344
      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) }
345
      end
emassip's avatar
emassip committed
346 347

      select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
348 349
    end
  end
350

351
  def project_tree_options_for_select(projects, options = {})
352 353 354 355
    s = ''.html_safe
    if options[:include_blank]
      s << content_tag('option', '&nbsp;'.html_safe, :value => '')
    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
  # Options for the new membership projects combo-box
  def options_for_membership_project_select(principal, projects)
    options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---")
    options << project_tree_options_for_select(projects) do |p|
406
      {:disabled => principal.projects.to_a.include?(p)}
407 408 409 410
    end
    options
  end

411 412 413 414
  def option_tag(name, text, value, selected=nil, options={})
    content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
  end

415 416
  # Truncates and returns the string as a single line
  def truncate_single_line(string, *args)
417 418 419 420
    ActiveSupport::Deprecation.warn(
      "ApplicationHelper#truncate_single_line is deprecated and will be removed in Rails 4 poring")
    # Rails 4 ActionView::Helpers::TextHelper#truncate escapes.
    # So, result is broken.
421
    truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
422
  end
423

424 425 426 427
  def truncate_single_line_raw(string, length)
    string.truncate(length).gsub(%r{[\r\n]+}m, ' ')
  end

428 429 430 431 432 433 434 435 436
  # 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
437

jplang's avatar
jplang committed
438 439 440 441
  def anchor(text)
    text.to_s.gsub(' ', '_')
  end

442
  def html_hours(text)
443
    text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
444
  end
445

jplang's avatar
jplang committed
446
  def authoring(created, author, options={})
447
    l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
448
  end
449

450 451 452
  def time_tag(time)
    text = distance_of_time_in_words(Time.now, time)
    if @project
453
      link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time))
454
    else
455
      content_tag('abbr', text, :title => format_time(time))
456
    end
457 458
  end

459 460 461 462 463 464
  def syntax_highlight_lines(name, content)
    lines = []
    syntax_highlight(name, content).each_line { |line| lines << line }
    lines
  end

465
  def syntax_highlight(name, content)
466
    Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
467
  end
468

jplang's avatar
jplang committed
469
  def to_path_param(path)
470 471
    str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
    str.blank? ? nil : str
jplang's avatar
jplang committed
472 473
  end

jplang's avatar
jplang committed
474
  def reorder_links(name, url, method = :post)
475 476
    link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
            url.merge({"#{name}[move_to]" => 'highest'}),
jplang's avatar
jplang committed
477
            :method => method, :title => l(:label_sort_highest)) +
478 479
    link_to(image_tag('1uparrow.png',   :alt => l(:label_sort_higher)),
            url.merge({"#{name}[move_to]" => 'higher'}),
jplang's avatar
jplang committed
480
           :method => method, :title => l(:label_sort_higher)) +
481 482
    link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
            url.merge({"#{name}[move_to]" => 'lower'}),
jplang's avatar
jplang committed
483
            :method => method, :title => l(:label_sort_lower)) +
484 485
    link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
            url.merge({"#{name}[move_to]" => 'lowest'}),
jplang's avatar
jplang committed
486
           :method => method, :title => l(:label_sort_lowest))
jplang's avatar
jplang committed
487
  end
488

489
  def breadcrumb(*args)
490
    elements = args.flatten
491
    elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
492
  end
493

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

500 501 502 503 504
  def page_header_title
    if @project.nil? || @project.new_record?
      h(Setting.app_title)
    else
      b = []
jplang's avatar
jplang committed
505
      ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
506 507
      if ancestors.any?
        root = ancestors.shift
508
        b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
509
        if ancestors.size > 2
510
          b << "\xe2\x80\xa6"
511 512
          ancestors = ancestors[-2, 2]
        end
513
        b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
514 515
      end
      b << h(@project)
516
      b.join(" \xc2\xbb ").html_safe
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 592
    project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
    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 633 634 635 636 637 638 639 640 641
      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
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
    # when using an image link, try to use an attachment, if possible
jplang's avatar
jplang committed
651 652 653
    attachments = options[:attachments] || []
    attachments += obj.attachments if obj.respond_to?(:attachments)
    if attachments.present?
654
      text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
655
        filename, ext, alt, alttext = $1.downcase, $2, $3, $4
656
        # search for the picture in attachments
657
        if found = Attachment.latest_attach(attachments, filename)
658
          image_url = download_named_attachment_path(found, found.filename, :only_path => only_path)
659 660 661 662
          desc = found.description.to_s.gsub('"', '')
          if !desc.blank? && alttext.blank?
            alt = " title=\"#{desc}\" alt=\"#{desc}\""
          end
663
          "src=\"#{image_url}\"#{alt}"
664
        else
665
          m
666 667 668
        end
      end
    end
669
  end
670

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

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

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

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

  def parse_sections(text, project, obj, attr, only_path, options)
    return unless options[:edit_section_links]
    text.gsub!(HEADING_RE) do