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

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

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

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

399 400 401 402
  # 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|
403
      {:disabled => principal.projects.to_a.include?(p)}
404 405 406 407
    end
    options
  end

408 409 410 411
  def option_tag(name, text, value, selected=nil, options={})
    content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
  end

412 413
  # Truncates and returns the string as a single line
  def truncate_single_line(string, *args)
414 415 416 417
    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.
418
    truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
419
  end
420

421 422 423 424
  def truncate_single_line_raw(string, length)
    string.truncate(length).gsub(%r{[\r\n]+}m, ' ')
  end

425 426 427 428 429 430 431 432 433
  # 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
434

jplang's avatar
jplang committed
435 436 437 438
  def anchor(text)
    text.to_s.gsub(' ', '_')
  end

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

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

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

456 457 458 459 460 461
  def syntax_highlight_lines(name, content)
    lines = []
    syntax_highlight(name, content).each_line { |line| lines << line }
    lines
  end

462
  def syntax_highlight(name, content)
463
    Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
464
  end
465

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

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

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

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

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

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

548 549 550 551 552 553 554 555
  # 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

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

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

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

591 592
    text = text.dup
    macros = catch_macros(text)
593
    text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
594

595
    @parsed_headings = []
596
    @heading_anchors = {}
597
    @current_section = 0 if options[:edit_section_links]
598 599

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

607 608 609
    if @parsed_headings.any?
      replace_toc(text, @parsed_headings)
    end
610

jplang's avatar
jplang committed
611
    text.html_safe
612
  end
613

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

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

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

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

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