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

application_helper.rb 52.7 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
  include Redmine::Helpers::URL
32

33 34 35
  extend Forwardable
  def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter

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

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

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

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

94 95 96 97 98 99
  # 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
100
    route_method = options.delete(:download) ? :download_named_attachment_url : :named_attachment_url
101
    html_options = options.slice!(:only_path)
jplang's avatar
jplang committed
102
    options[:only_path] = true unless options.key?(:only_path)
103 104
    url = send(route_method, attachment, attachment.filename, options)
    link_to text, url, html_options
105
  end
106

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

124 125 126
  # Generates a link to a message
  def link_to_message(message, options={}, html_options = nil)
    link_to(
127
      message.subject.truncate(60),
jplang's avatar
jplang committed
128
      board_message_url(message.board_id, message.parent_id || message.id, {
129
        :r => (message.parent_id && message.id),
jplang's avatar
jplang committed
130 131
        :anchor => (message.parent_id ? "message-#{message.id}" : nil),
        :only_path => true
132
      }.merge(options)),
133 134 135
      html_options
    )
  end
136

137 138
  # Generates a link to a project if active
  # Examples:
139
  #
140 141 142 143 144
  #   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)
145
    if project.archived?
146 147
      h(project.name)
    else
jplang's avatar
jplang committed
148 149 150
      link_to project.name,
        project_url(project, {:only_path => true}.merge(options)),
        html_options
151 152 153 154 155 156 157 158 159 160 161
    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
162 163 164
    end
  end

165 166 167 168 169 170 171
  # 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

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

218 219 220 221
  def wiki_page_path(page, options={})
    url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
  end

222
  def thumbnail_tag(attachment)
223 224 225
    link_to(
      image_tag(
        thumbnail_path(attachment),
226 227
        :srcset => "#{thumbnail_path(attachment, :size => Setting.thumbnails_size.to_i * 2)} 2x",
        :width => Setting.thumbnails_size 
228 229 230 231 232
      ),
      named_attachment_path(
        attachment,
        attachment.filename
      ),
233
      :title => attachment.filename
234
    )
235 236
  end

jplang's avatar
jplang committed
237
  def toggle_link(name, id, options={})
238 239
    onclick = "$('##{id}').toggle(); "
    onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
jplang's avatar
jplang committed
240 241 242
    onclick << "return false;"
    link_to(name, "#", :onclick => onclick)
  end
243

244
  # Used to format item titles on the activity view
245
  def format_activity_title(text)
246
    text
247
  end
248

249
  def format_activity_day(date)
250
    date == User.current.today ? l(:label_today).titleize : format_date(date)
251
  end
252

253
  def format_activity_description(text)
254
    h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
255
       ).gsub(/[\r\n]+/, "<br />").html_safe
256
  end
257

258
  def format_version_name(version)
259
    if version.project == @project
260
      h(version)
261 262 263 264
    else
      h("#{version.project} - #{version}")
    end
  end
265

266 267 268 269 270
  def format_changeset_comments(changeset, options={})
    method = options[:short] ? :short_comments : :comments
    textilizable changeset, method, :formatting => Setting.commit_logs_formatting?
  end

271 272
  def due_date_distance_in_words(date)
    if date
273
      l((date < User.current.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(User.current.today, date))
274 275
    end
  end
276

277 278 279
  # 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)
280
  def render_project_nested_lists(projects, &block)
281 282 283 284
    s = ''
    if projects.any?
      ancestors = []
      original_project = @project
jplang's avatar
jplang committed
285
      projects.sort_by(&:lft).each do |project|
286 287 288 289 290 291 292 293 294 295 296 297 298 299
        # 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}'>"
300
        s << h(block_given? ? capture(project, &block) : project.name)
301 302 303 304 305 306 307 308 309
        s << "</div>\n"
        ancestors << project
      end
      s << ("</li></ul>\n" * ancestors.size)
      @project = original_project
    end
    s.html_safe
  end

310
  def render_page_hierarchy(pages, node=nil, options={})
311 312 313 314 315
    content = ''
    if pages[node]
      content << "<ul class=\"pages-hierarchy\">\n"
      pages[node].each do |page|
        content << "<li>"
jplang's avatar
jplang committed
316
        content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
317 318
                           :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]
319 320 321 322
        content << "</li>\n"
      end
      content << "</ul>\n"
    end
323
    content.html_safe
324
  end
325

326 327 328 329
  # Renders flash messages
  def render_flash_messages
    s = ''
    flash.each do |k,v|
330
      s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
331
    end
332
    s.html_safe
333
  end
334

jplang's avatar
jplang committed
335
  # Renders tabs and their content
336
  def render_tabs(tabs, selected=params[:tab])
jplang's avatar
jplang committed
337
    if tabs.any?
338 339 340 341 342
      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
343 344 345 346
    else
      content_tag 'p', l(:label_no_data), :class => "nodata"
    end
  end
347

348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365
  # Returns an array of projects that are displayed in the quick-jump box
  def projects_for_jump_box(user=User.current)
    if user.logged?
      user.projects.active.select(:id, :name, :identifier, :lft, :rgt).to_a
    else
      []
    end
  end

  def render_projects_for_jump_box(projects, selected=nil)
    s = ''.html_safe
    project_tree(projects) do |project, level|
      padding = level * 16
      text = content_tag('span', project.name, :style => "padding-left:#{padding}px;")
      s << link_to(text, project_path(project, :jump => current_menu_item), :title => project.name, :class => (project == selected ? 'selected' : nil))
    end
    s
  end
366

367 368
  # Renders the project quick-jump box
  def render_project_jump_box
369
    projects = projects_for_jump_box(User.current)
370 371 372 373 374 375 376 377 378 379
    text = @project.try(:name) || l(:label_jump_to_a_project)
    trigger = content_tag('span', text, :class => 'drdn-trigger')
    q = text_field_tag('q', '', :id => 'projects-quick-search', :class => 'autocomplete', :data => {:automcomplete_url => projects_path(:format => 'js')})
    all = link_to(l(:label_project_all), projects_path(:jump => current_menu_item), :class => (@project.nil? && controller.class.main_menu ? 'selected' : nil))
    content = content_tag('div',
          content_tag('div', q, :class => 'quick-search') +
          content_tag('div', render_projects_for_jump_box(projects, @project), :class => 'drdn-items projects selection') +
          content_tag('div', all, :class => 'drdn-items all-projects selection'),
        :class => 'drdn-content'
      )
emassip's avatar
emassip committed
380

381
    content_tag('span', trigger + content, :id => "project-jump", :class => "drdn")
382
  end
383

384
  def project_tree_options_for_select(projects, options = {})
385
    s = ''.html_safe
386 387 388 389 390
    if blank_text = options[:include_blank]
      if blank_text == true
        blank_text = '&nbsp;'.html_safe
      end
      s << content_tag('option', blank_text, :value => '')
391
    end
392
    project_tree(projects) do |project, level|
emassip's avatar
emassip committed
393
      name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
394 395 396 397 398 399
      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
400 401 402
      tag_options.merge!(yield(project)) if block_given?
      s << content_tag('option', name_prefix + h(project), tag_options)
    end
403
    s.html_safe
404
  end
405

406
  # Yields the given block for each project with its level in the tree
407 408
  #
  # Wrapper for Project#project_tree
409 410
  def project_tree(projects, options={}, &block)
    Project.project_tree(projects, options, &block)
411
  end
412

jplang's avatar
jplang committed
413 414
  def principals_check_box_tags(name, principals)
    s = ''
415
    principals.each do |principal|
jplang's avatar
jplang committed
416
      s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
jplang's avatar
jplang committed
417
    end
418
    s.html_safe
jplang's avatar
jplang committed
419
  end
420

421 422 423
  # Returns a string for users/groups option tags
  def principals_options_for_select(collection, selected=nil)
    s = ''
424
    if collection.include?(User.current)
jplang's avatar
jplang committed
425
      s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
426
    end
427 428
    groups = ''
    collection.sort.each do |element|
429
      selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
430 431 432 433 434
      (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
435
    s.html_safe
436
  end
437

438 439 440 441
  def option_tag(name, text, value, selected=nil, options={})
    content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
  end

442
  def truncate_single_line_raw(string, length)
jplang's avatar
jplang committed
443
    string.to_s.truncate(length).gsub(%r{[\r\n]+}m, ' ')
444 445
  end

446 447 448 449 450 451 452 453 454
  # 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
455

jplang's avatar
jplang committed
456 457 458 459
  def anchor(text)
    text.to_s.gsub(' ', '_')
  end

460
  def html_hours(text)
461
    text.gsub(%r{(\d+)([\.:])(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">\2\3</span>').html_safe
462
  end
463

jplang's avatar
jplang committed
464
  def authoring(created, author, options={})
465
    l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
466
  end
467

468 469 470
  def time_tag(time)
    text = distance_of_time_in_words(Time.now, time)
    if @project
jplang's avatar
jplang committed
471
      link_to(text, project_activity_path(@project, :from => User.current.time_to_date(time)), :title => format_time(time))
472
    else
473
      content_tag('abbr', text, :title => format_time(time))
474
    end
475 476
  end

477
  def syntax_highlight_lines(name, content)
478
    syntax_highlight(name, content).each_line.to_a
479 480
  end

481
  def syntax_highlight(name, content)
482
    Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
483
  end
484

jplang's avatar
jplang committed
485
  def to_path_param(path)
486 487
    str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
    str.blank? ? nil : str
jplang's avatar
jplang committed
488 489
  end

jplang's avatar
jplang committed
490
  def reorder_links(name, url, method = :post)
jplang's avatar
jplang committed
491 492 493
    # TODO: remove associated styles from application.css too
    ActiveSupport::Deprecation.warn "Application#reorder_links will be removed in Redmine 4."

494
    link_to(l(:label_sort_highest),
495 496
            url.merge({"#{name}[move_to]" => 'highest'}), :method => method,
            :title => l(:label_sort_highest), :class => 'icon-only icon-move-top') +
497
    link_to(l(:label_sort_higher),
498 499
            url.merge({"#{name}[move_to]" => 'higher'}), :method => method,
            :title => l(:label_sort_higher), :class => 'icon-only icon-move-up') +
500
    link_to(l(:label_sort_lower),
501 502
            url.merge({"#{name}[move_to]" => 'lower'}), :method => method,
            :title => l(:label_sort_lower), :class => 'icon-only icon-move-down') +
503
    link_to(l(:label_sort_lowest),
504 505
            url.merge({"#{name}[move_to]" => 'lowest'}), :method => method,
            :title => l(:label_sort_lowest), :class => 'icon-only icon-move-bottom')
jplang's avatar
jplang committed
506
  end
507

508 509 510 511 512 513
  def reorder_handle(object, options={})
    data = {
      :reorder_url => options[:url] || url_for(object),
      :reorder_param => options[:param] || object.class.name.underscore
    }
    content_tag('span', '',
jplang's avatar
jplang committed
514
      :class => "sort-handle",
515 516
      :data => data,
      :title => l(:button_sort))
517 518
  end

519
  def breadcrumb(*args)
520
    elements = args.flatten
521
    elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
522
  end
523

524
  def other_formats_links(&block)
jplang's avatar
jplang committed
525
    concat('<p class="other-formats">'.html_safe + l(:label_export_to))
526
    yield Redmine::Views::OtherFormatsBuilder.new(self)
jplang's avatar
jplang committed
527
    concat('</p>'.html_safe)
528
  end
529

530 531 532 533 534
  def page_header_title
    if @project.nil? || @project.new_record?
      h(Setting.app_title)
    else
      b = []
jplang's avatar
jplang committed
535
      ancestors = (@project.root? ? [] : @project.ancestors.visible.to_a)
536 537
      if ancestors.any?
        root = ancestors.shift
538
        b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
539
        if ancestors.size > 2
540
          b << "\xe2\x80\xa6"
541 542
          ancestors = ancestors[-2, 2]
        end
543
        b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
544
      end
545 546 547 548 549 550 551
      b << content_tag(:span, h(@project), class: 'current-project')
      if b.size > 1
        separator = content_tag(:span, ' &raquo; '.html_safe, class: 'separator')
        path = safe_join(b[0..-2], separator) + separator
        b = [content_tag(:span, path.html_safe, class: 'breadcrumbs'), b[-1]]
      end
      safe_join b
552 553
    end
  end
554

555
  # Returns a h2 tag and sets the html title with the given arguments
556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573
  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'
574 575
  def html_title(*args)
    if args.empty?
576
      title = @html_title || []
tmaruyama's avatar
tmaruyama committed
577
      title << @project.name if @project
578
      title << Setting.app_title unless Setting.app_title == title.last
579
      title.reject(&:blank?).join(' - ')
580 581 582 583
    else
      @html_title ||= []
      @html_title += args
    end
584
  end
jplang's avatar
jplang committed
585

586 587 588 589 590 591 592 593
  # 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

594
    css << 'project-' + @project.identifier if @project && @project.identifier.present?
595 596
    css << 'controller-' + controller_name
    css << 'action-' + action_name
597 598 599
    if UserPreference::TEXTAREA_FONT_OPTIONS.include?(User.current.pref.textarea_font)
      css << "textarea-#{User.current.pref.textarea_font}"
    end
600 601 602
    css.join(' ')
  end

jplang's avatar
jplang committed
603
  def accesskey(s)
604 605 606 607 608
    @used_accesskeys ||= []
    key = Redmine::AccessKeys.key_for(s)
    return nil if @used_accesskeys.include?(key)
    @used_accesskeys << key
    key
jplang's avatar
jplang committed
609 610
  end

611 612 613 614 615 616 617 618
  # 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
619
      obj = options[:object]
jplang's avatar
jplang committed
620
      text = args.shift
621 622
    when 2
      obj = args.shift
623 624
      attr = args.shift
      text = obj.send(attr).to_s
625 626 627
    else
      raise ArgumentError, 'invalid arguments to textilizable'
    end
jplang's avatar
jplang committed
628
    return '' if text.blank?
629
    project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
630
    @only_path = only_path = options.delete(:only_path) == false ? false : true
631

632 633
    text = text.dup
    macros = catch_macros(text)
634 635 636 637 638 639 640

    if options[:formatting] == false
      text = h(text)
    else
      formatting = options[:formatting] || Setting.text_formatting
      text = Redmine::WikiFormatting.to_html(formatting, text, :object => obj, :attribute => attr)
    end
641

642
    @parsed_headings = []
643
    @heading_anchors = {}
644
    @current_section = 0 if options[:edit_section_links]
645 646

    parse_sections(text, project, obj, attr, only_path, options)
647
    text = parse_non_pre_blocks(text, obj, macros) do |text|
648
      [:parse_inline_attachments, :parse_hires_images, :parse_wiki_links, :parse_redmine_links].each do |method_name|
649 650
        send method_name, text, project, obj, attr, only_path, options
      end
651
    end
652
    parse_headings(text, project, obj, attr, only_path, options)
653

654 655 656
    if @parsed_headings.any?
      replace_toc(text, @parsed_headings)
    end
657

jplang's avatar
jplang committed
658
    text.html_safe
659
  end
660

661
  def parse_non_pre_blocks(text, obj, macros)
662 663 664 665 666 667 668 669
    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
670 671 672
        inject_macros(text, obj, macros) if macros.any?
      else
        inject_macros(text, obj, macros, false) if macros.any?
673 674 675 676
      end
      parsed << text
      if tag
        if closing
677
          if tags.last && tags.last.casecmp(tag) == 0
678 679 680 681 682 683 684 685
            tags.pop
          end
        else
          tags << tag.downcase
        end
        parsed << full_tag
      end
    end
686 687 688 689
    # Close any non closing tags
    while tag = tags.pop
      parsed << "</#{tag}>"
    end
690
    parsed
691
  end
692

693 694 695 696 697 698 699 700 701
  # add srcset attribute to img tags if filename includes @2x, @3x, etc.
  # to support hires displays
  def parse_hires_images(text, project, obj, attr, only_path, options)
    text.gsub!(/src="([^"]+@(\dx)\.(bmp|gif|jpg|jpe|jpeg|png))"/i) do |m|
      filename, dpr = $1, $2
      m + " srcset=\"#{filename} #{dpr}\""
    end
  end

702
  def parse_inline_attachments(text, project, obj, attr, only_path, options)
703 704
    return if options[:inline_attachments] == false

705
    # when using an image link, try to use an attachment, if possible
jplang's avatar
jplang committed
706 707 708
    attachments = options[:attachments] || []
    attachments += obj.attachments if obj.respond_to?(:attachments)
    if attachments.present?
709
      text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
710
        filename, ext, alt, alttext = $1.downcase, $2, $3, $4
711
        # search for the picture in attachments
712
        if found = Attachment.latest_attach(attachments, CGI.unescape(filename))
jplang's avatar
jplang committed
713
          image_url = download_named_attachment_url(found, found.filename, :only_path => only_path)
714 715 716 717
          desc = found.description.to_s.gsub('"', '')
          if !desc.blank? && alttext.blank?
            alt = " title=\"#{desc}\" alt=\"#{desc}\""
          end
718
          "src=\"#{image_url}\"#{alt}"
719
        else
720
          m
721 722 723
        end
      end
    end
724
  end
725

726 727 728 729 730 731 732 733 734 735 736 737
  # 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|
738
      link_project = project
739 740 741
      esc, all, page, title = $1, $2, $3, $5
      if esc.nil?
        if page =~ /^([^\:]+)\:(.*)$/
742 743 744
          identifier, page = $1, $2
          link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
          title ||= identifier if page.blank?
745
        end
746

747
        if link_project && link_project.wiki && User.current.allowed_to?(:view_wiki_pages, link_project)
748 749 750 751 752
          # extract anchor
          anchor = nil
          if page =~ /^(.+?)\#(.+)$/
            page, anchor = $1, $2
          end
753
          anchor = sanitize_anchor_name(anchor) if anchor.present?
754 755
          # check if page exists
          wiki_page = link_project.wiki.find_page(page)
756 757 758 759
          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]
760
            when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
761
            when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
762
            else
763
              wiki_page_id = page.present? ? Wiki.titleize(page) : nil
764
              parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
765
              url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
jplang's avatar
jplang committed
766
               :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
767
            end
768
          end
emassip's avatar
emassip committed
769
          link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
770 771
        else
          # project or wiki doesn't exist
772
          all
773
        end
jplang's avatar
jplang committed
774
      else
775
        all
jplang's avatar
jplang committed
776
      end
777
    end
778
  end
779

780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803
  # 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
804
  #   Forum messages:
805
  #     message#1218 -> Link to message with id 1218
806 807 808
  #  Projects:
  #     project:someproject -> Link to project named "someproject"
  #     project#3 -> Link to project with id 3
809 810 811 812 813 814
  #
  #   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
815
  def parse_redmine_links(text, default_project, obj, attr, only_path, options)
816 817 818 819 820 821 822 823 824 825 826 827 828 829
    text.gsub!(LINKS_RE) do |_|
      tag_content = $~[:tag_content]
      leading = $~[:leading]
      esc = $~[:esc]
      project_prefix = $~[:project_prefix]
      project_identifier = $~[:project_identifier]
      prefix = $~[:prefix]
      repo_prefix = $~[:repo_prefix]
      repo_identifier = $~[:repo_identifier]
      sep = $~[:sep1] || $~[:sep2] || $~[:sep3]
      identifier = $~[:identifier1] || $~[:identifier2]
      comment_suffix = $~[:comment_suffix]
      comment_id = $~[:comment_id]

830 831 832 833 834 835 836 837 838 839
      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'
840 841
            if project
              repository = nil
842
              if repo_identifier
843 844 845 846
                repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
              else
                repository = project.repository
              end
847 848 849 850 851 852 853 854 855 856 857
              # 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))
858
              end
859
            end
860 861 862 863 864
          elsif sep == '#'
            oid = identifier.to_i
            case prefix
            when nil
              if oid.to_s == identifier &&
jplang's avatar
jplang committed
865
                issue = Issue.visible.find_by_id(oid)
866
                anchor = comment_id ? "note-#{comment_id}" : nil
jplang's avatar
jplang committed
867
                link = link_to("##{oid}#{comment_suffix}",
jplang's avatar
jplang committed
868
                               issue_url(issue, :only_path => only_path, :anchor => anchor),
869
                               :class => issue.css_classes,
870
                               :title => "#{issue.tracker.name}: #{issue.subject.truncate(100)} (#{issue.status.name})")
871 872 873
              end
            when 'document'
              if document = Document.visible.find_by_id(oid)
jplang's avatar
jplang committed
874
                link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
875 876 877
              end
            when 'version'
              if version = Version.visible.find_by_id(oid)
jplang's avatar
jplang committed
878
                link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
879 880
              end
            when 'message'
jplang's avatar
jplang committed
881
              if message = Message.visible.find_by_id(oid)
882 883 884 885
                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
886
                link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
887 888 889
              end
            when 'news'
              if news = News.visible.find_by_id(oid)
jplang's avatar
jplang committed
890
                link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
891 892 893 894 895
              end
            when 'project'
              if p = Project.visible.find_by_id(oid)
                link = link_to_project(p, {:only_path => only_path}, :class => 'project')
              end
896
            end
897 898 899 900 901 902 903
          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
904
                link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
905 906 907
              end
            when 'version'
              if project && version = project.versions.visible.find_by_name(name)
jplang's avatar
jplang committed
908
                link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
909 910 911
              end
            when 'forum'
              if project && board = project.boards.visible.find_by_name(name)
jplang's avatar
jplang committed
912
                link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
913 914 915
              end
            when 'news'
              if project && news = project.news.visible.find_by_title(name)
jplang's avatar
jplang committed
916
                link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955
              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
956
            end
957
          end
jplang's avatar