GitLab steht Mittwoch, den 23. September, zwischen 10:00 und 12:00 Uhr aufgrund von Wartungsarbeiten nicht zur Verfügung.

application_helper.rb 49.5 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
  # Helper that formats object for html or text rendering
  def format_object(object, html=true)
    case object.class.name
163 164
    when 'Array'
      object.map {|o| format_object(o, html)}.join(', ').html_safe
165 166 167 168 169 170 171 172 173 174 175 176 177
    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'
178
      html ? link_to(object.name, version_path(object)) : object.to_s
179 180 181 182 183 184
    when 'TrueClass'
      l(:general_text_Yes)
    when 'FalseClass'
      l(:general_text_No)
    when 'Issue'
      object.visible? && html ? link_to_issue(object) : "##{object.id}"
185 186 187 188 189 190 191 192 193 194 195
    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
          format_object(f, html)
        end
      else
        object.value.to_s
      end
196 197 198 199 200
    else
      html ? h(object) : object.to_s
    end
  end

201 202 203 204
  def wiki_page_path(page, options={})
    url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
  end

205
  def thumbnail_tag(attachment)
206 207
    link_to image_tag(thumbnail_path(attachment)),
      named_attachment_path(attachment, attachment.filename),
208 209 210
      :title => attachment.filename
  end

jplang's avatar
jplang committed
211
  def toggle_link(name, id, options={})
212 213
    onclick = "$('##{id}').toggle(); "
    onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
jplang's avatar
jplang committed
214 215 216
    onclick << "return false;"
    link_to(name, "#", :onclick => onclick)
  end
217

218 219
  def image_to_function(name, function, html_options = {})
    html_options.symbolize_keys!
220 221 222
    tag(:input, html_options.merge({
        :type => "image", :src => image_path(name),
        :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
223 224
        }))
  end
225

226
  def format_activity_title(text)
227
    h(truncate_single_line_raw(text, 100))
228
  end
229

230
  def format_activity_day(date)
231
    date == User.current.today ? l(:label_today).titleize : format_date(date)
232
  end
233

234
  def format_activity_description(text)
235
    h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
236
       ).gsub(/[\r\n]+/, "<br />").html_safe
237
  end
238

239 240
  def format_version_name(version)
    if version.project == @project
241
      h(version)
242 243 244 245
    else
      h("#{version.project} - #{version}")
    end
  end
246

247 248 249 250 251
  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
252

253 254 255 256 257 258 259 260
  # 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
261
      projects.sort_by(&:lft).each do |project|
262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285
        # 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

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

302 303 304 305
  # Renders flash messages
  def render_flash_messages
    s = ''
    flash.each do |k,v|
306
      s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
307
    end
308
    s.html_safe
309
  end
310

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

324 325
  # Renders the project quick-jump box
  def render_project_jump_box
326
    return unless User.current.logged?
327
    projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq
328
    if projects.any?
emassip's avatar
emassip committed
329 330 331 332 333 334
      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) }
335
      end
emassip's avatar
emassip committed
336 337

      select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
338 339
    end
  end
340

341 342 343
  def project_tree_options_for_select(projects, options = {})
    s = ''
    project_tree(projects) do |project, level|
emassip's avatar
emassip committed
344
      name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
345 346 347 348 349 350
      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
351 352 353
      tag_options.merge!(yield(project)) if block_given?
      s << content_tag('option', name_prefix + h(project), tag_options)
    end
354
    s.html_safe
355
  end
356

357
  # Yields the given block for each project with its level in the tree
358 359
  #
  # Wrapper for Project#project_tree
360
  def project_tree(projects, &block)
361
    Project.project_tree(projects, &block)
362
  end
363

jplang's avatar
jplang committed
364 365
  def principals_check_box_tags(name, principals)
    s = ''
366
    principals.each do |principal|
jplang's avatar
jplang committed
367
      s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
jplang's avatar
jplang committed
368
    end
369
    s.html_safe
jplang's avatar
jplang committed
370
  end
371

372 373 374
  # Returns a string for users/groups option tags
  def principals_options_for_select(collection, selected=nil)
    s = ''
375
    if collection.include?(User.current)
jplang's avatar
jplang committed
376
      s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
377
    end
378 379
    groups = ''
    collection.sort.each do |element|
380
      selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
381 382 383 384 385
      (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
386
    s.html_safe
387
  end
388

389 390 391 392
  # 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|
393
      {:disabled => principal.projects.to_a.include?(p)}
394 395 396 397
    end
    options
  end

398 399 400 401
  def option_tag(name, text, value, selected=nil, options={})
    content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
  end

402 403
  # Truncates and returns the string as a single line
  def truncate_single_line(string, *args)
404 405 406 407
    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.
408
    truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
409
  end
410

411 412 413 414
  def truncate_single_line_raw(string, length)
    string.truncate(length).gsub(%r{[\r\n]+}m, ' ')
  end

415 416 417 418 419 420 421 422 423
  # 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
424

jplang's avatar
jplang committed
425 426 427 428
  def anchor(text)
    text.to_s.gsub(' ', '_')
  end

429
  def html_hours(text)
430
    text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
431
  end
432

jplang's avatar
jplang committed
433
  def authoring(created, author, options={})
434
    l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
435
  end
436

437 438 439
  def time_tag(time)
    text = distance_of_time_in_words(Time.now, time)
    if @project
440
      link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time))
441
    else
442
      content_tag('abbr', text, :title => format_time(time))
443
    end
444 445
  end

446 447 448 449 450 451
  def syntax_highlight_lines(name, content)
    lines = []
    syntax_highlight(name, content).each_line { |line| lines << line }
    lines
  end

452
  def syntax_highlight(name, content)
453
    Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
454
  end
455

jplang's avatar
jplang committed
456
  def to_path_param(path)
457 458
    str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
    str.blank? ? nil : str
jplang's avatar
jplang committed
459 460
  end

jplang's avatar
jplang committed
461
  def reorder_links(name, url, method = :post)
462 463
    link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
            url.merge({"#{name}[move_to]" => 'highest'}),
jplang's avatar
jplang committed
464
            :method => method, :title => l(:label_sort_highest)) +
465 466
    link_to(image_tag('1uparrow.png',   :alt => l(:label_sort_higher)),
            url.merge({"#{name}[move_to]" => 'higher'}),
jplang's avatar
jplang committed
467
           :method => method, :title => l(:label_sort_higher)) +
468 469
    link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
            url.merge({"#{name}[move_to]" => 'lower'}),
jplang's avatar
jplang committed
470
            :method => method, :title => l(:label_sort_lower)) +
471 472
    link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
            url.merge({"#{name}[move_to]" => 'lowest'}),
jplang's avatar
jplang committed
473
           :method => method, :title => l(:label_sort_lowest))
jplang's avatar
jplang committed
474
  end
475

476
  def breadcrumb(*args)
477
    elements = args.flatten
478
    elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
479
  end
480

481
  def other_formats_links(&block)
jplang's avatar
jplang committed
482
    concat('<p class="other-formats">'.html_safe + l(:label_export_to))
483
    yield Redmine::Views::OtherFormatsBuilder.new(self)
jplang's avatar
jplang committed
484
    concat('</p>'.html_safe)
485
  end
486

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

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

538 539 540 541 542 543 544 545
  # 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

546
    css << 'project-' + @project.identifier if @project && @project.identifier.present?
547 548
    css << 'controller-' + controller_name
    css << 'action-' + action_name
549 550 551
    css.join(' ')
  end

jplang's avatar
jplang committed
552
  def accesskey(s)
553 554 555 556 557
    @used_accesskeys ||= []
    key = Redmine::AccessKeys.key_for(s)
    return nil if @used_accesskeys.include?(key)
    @used_accesskeys << key
    key
jplang's avatar
jplang committed
558 559
  end

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

581 582
    text = text.dup
    macros = catch_macros(text)
583
    text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
584

585
    @parsed_headings = []
586
    @heading_anchors = {}
587
    @current_section = 0 if options[:edit_section_links]
588 589

    parse_sections(text, project, obj, attr, only_path, options)
590 591
    text = parse_non_pre_blocks(text, obj, macros) do |text|
      [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
592 593
        send method_name, text, project, obj, attr, only_path, options
      end
594
    end
595
    parse_headings(text, project, obj, attr, only_path, options)
596

597 598 599
    if @parsed_headings.any?
      replace_toc(text, @parsed_headings)
    end
600

jplang's avatar
jplang committed
601
    text.html_safe
602
  end
603

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

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

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

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

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

jplang's avatar
jplang committed
888
  HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
889 890 891 892

  def parse_sections(text, project, obj, attr, only_path, options)
    return unless options[:edit_section_links]
    text.gsub!(HEADING_RE) do
893
      heading = $1
894 895
      @current_section += 1
      if @current_section > 1
896
        content_tag('div',
897
          link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
898
          :class => 'contextual',
899 900
          :title => l(:button_edit_section),
          :id => "section-#{@current_section}") + heading.html_safe
901
      else
902
        heading
903 904 905
      end
    end
  end
906

907
  # Headings and TOC
jplang's avatar