application_helper.rb 48.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)
457 458 459 460 461 462 463 464 465 466 467 468
    link_to('',
            url.merge({"#{name}[move_to]" => 'highest'}), :method => method,
            :title => l(:label_sort_highest), :class => 'icon-only icon-move-top') +
    link_to('',
            url.merge({"#{name}[move_to]" => 'higher'}), :method => method,
            :title => l(:label_sort_higher), :class => 'icon-only icon-move-up') +
    link_to('',
            url.merge({"#{name}[move_to]" => 'lower'}), :method => method,
            :title => l(:label_sort_lower), :class => 'icon-only icon-move-down') +
    link_to('',
            url.merge({"#{name}[move_to]" => 'lowest'}), :method => method,
            :title => l(:label_sort_lowest), :class => 'icon-only icon-move-bottom')
jplang's avatar
jplang committed
469
  end
470

471
  def breadcrumb(*args)
472
    elements = args.flatten
473
    elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
474
  end
475

476
  def other_formats_links(&block)
jplang's avatar
jplang committed
477
    concat('<p class="other-formats">'.html_safe + l(:label_export_to))
478
    yield Redmine::Views::OtherFormatsBuilder.new(self)
jplang's avatar
jplang committed
479
    concat('</p>'.html_safe)
480
  end
481

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

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

533 534 535 536 537 538 539 540
  # 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

541
    css << 'project-' + @project.identifier if @project && @project.identifier.present?
542 543
    css << 'controller-' + controller_name
    css << 'action-' + action_name
544 545 546
    css.join(' ')
  end

jplang's avatar
jplang committed
547
  def accesskey(s)
548 549 550 551 552
    @used_accesskeys ||= []
    key = Redmine::AccessKeys.key_for(s)
    return nil if @used_accesskeys.include?(key)
    @used_accesskeys << key
    key
jplang's avatar
jplang committed
553 554
  end

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

576 577
    text = text.dup
    macros = catch_macros(text)
578
    text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
579

580
    @parsed_headings = []
581
    @heading_anchors = {}
582
    @current_section = 0 if options[:edit_section_links]
583 584

    parse_sections(text, project, obj, attr, only_path, options)
585 586
    text = parse_non_pre_blocks(text, obj, macros) do |text|
      [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
587 588
        send method_name, text, project, obj, attr, only_path, options
      end
589
    end
590
    parse_headings(text, project, obj, attr, only_path, options)
591

592 593 594
    if @parsed_headings.any?
      replace_toc(text, @parsed_headings)
    end
595

jplang's avatar
jplang committed
596
    text.html_safe
597
  end
598

599
  def parse_non_pre_blocks(text, obj, macros)
600 601 602 603 604 605 606 607
    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
608 609 610
        inject_macros(text, obj, macros) if macros.any?
      else
        inject_macros(text, obj, macros, false) if macros.any?
611 612 613 614
      end
      parsed << text
      if tag
        if closing
615
          if tags.last && tags.last.casecmp(tag) == 0
616 617 618 619 620 621 622 623
            tags.pop
          end
        else
          tags << tag.downcase
        end
        parsed << full_tag
      end
    end
624 625 626 627
    # Close any non closing tags
    while tag = tags.pop
      parsed << "</#{tag}>"
    end
628
    parsed
629
  end
630

631
  def parse_inline_attachments(text, project, obj, attr, only_path, options)
632 633
    return if options[:inline_attachments] == false

634
    # when using an image link, try to use an attachment, if possible
jplang's avatar
jplang committed
635 636 637
    attachments = options[:attachments] || []
    attachments += obj.attachments if obj.respond_to?(:attachments)
    if attachments.present?
638
      text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
639
        filename, ext, alt, alttext = $1.downcase, $2, $3, $4
640
        # search for the picture in attachments
641
        if found = Attachment.latest_attach(attachments, CGI.unescape(filename))
jplang's avatar
jplang committed
642
          image_url = download_named_attachment_url(found, found.filename, :only_path => only_path)
643 644 645 646
          desc = found.description.to_s.gsub('"', '')
          if !desc.blank? && alttext.blank?
            alt = " title=\"#{desc}\" alt=\"#{desc}\""
          end
647
          "src=\"#{image_url}\"#{alt}"
648
        else
649
          m
650 651 652
        end
      end
    end
653
  end
654

655 656 657 658 659 660 661 662 663 664 665 666
  # 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|
667
      link_project = project
668 669 670
      esc, all, page, title = $1, $2, $3, $5
      if esc.nil?
        if page =~ /^([^\:]+)\:(.*)$/
671 672 673
          identifier, page = $1, $2
          link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
          title ||= identifier if page.blank?
674
        end
675

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

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

jplang's avatar
jplang committed
881
  HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
882 883 884 885

  def parse_sections(text, project, obj, attr, only_path, options)
    return unless options[:edit_section_links]
    text.gsub!(HEADING_RE) do
886
      heading, level = $1, $2
887 888
      @current_section += 1
      if @current_section > 1
889
        content_tag('div',
890 891
          link_to('', options[:edit_section_links].merge(:section => @current_section),
                  :class => 'icon-only icon-edit'),
892
          :class => "contextual heading-#{level}",
893 894
          :title => l(:button_edit_section),
          :id => "section-#{@current_section}") + heading.html_safe
895
      else
896
        heading
897 898 899
      end
    end
  end
900

901
  # Headings and TOC
902
  # Adds ids and links to headings unless options[:headings] is set to false
903
  def parse_headings(text, project, obj, attr, only_path, options)
904
    return if options[:headings] == false
905

906
    text.gsub!(HEADING_RE) do
907
      level, attrs, content = $2.to_i, $3, $4
908
      item = strip_tags(content).strip
909
      anchor = sanitize_anchor_name(item)
910 911
      # used for single-file wiki export
      anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
912 913 914 915 916
      @heading_anchors[anchor] ||= 0
      idx = (@heading_anchors[anchor] += 1)
      if idx > 1
        anchor = "#{anchor}-#{idx}"
      end
917
      @parsed_headings << [level, anchor, item]
918
      "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
919 920
    end
  end