application_helper.rb 52 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.

jplang's avatar
jplang committed
20
require 'forwardable'
jplang's avatar
jplang committed
21
require 'cgi'
jplang's avatar
jplang committed
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

jplang's avatar
jplang committed
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

jplang's avatar
jplang committed
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)
jplang's avatar
jplang committed
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
jplang's avatar
jplang committed
57
58
59
      else
        name
      end
jplang's avatar
jplang committed
60
    else
jplang's avatar
jplang committed
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
jplang's avatar
jplang committed
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?
jplang's avatar
jplang committed
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
jplang's avatar
jplang committed
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
    link_to image_tag(thumbnail_path(attachment)),
      named_attachment_path(attachment, attachment.filename),
225
226
227
      :title => attachment.filename
  end

jplang's avatar
jplang committed
228
  def toggle_link(name, id, options={})
229
230
    onclick = "$('##{id}').toggle(); "
    onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
jplang's avatar
jplang committed
231
232
233
    onclick << "return false;"
    link_to(name, "#", :onclick => onclick)
  end
234

235
  # Used to format item titles on the activity view
236
  def format_activity_title(text)
237
    text
238
  end
239

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

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

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

257
258
259
260
261
  def format_changeset_comments(changeset, options={})
    method = options[:short] ? :short_comments : :comments
    textilizable changeset, method, :formatting => Setting.commit_logs_formatting?
  end

262
263
  def due_date_distance_in_words(date)
    if date
264
      l((date < User.current.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(User.current.today, date))
265
266
    end
  end
267

268
269
270
  # Renders a tree of projects as a nested set of unordered lists
  # The given collection may be a subset of the whole project tree
  # (eg. some intermediate nodes are private and can not be seen)
271
  def render_project_nested_lists(projects, &block)
272
273
274
275
    s = ''
    if projects.any?
      ancestors = []
      original_project = @project
jplang's avatar
jplang committed
276
      projects.sort_by(&:lft).each do |project|
277
278
279
280
281
282
283
284
285
286
287
288
289
290
        # 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}'>"
291
        s << h(block_given? ? capture(project, &block) : project.name)
292
293
294
295
296
297
298
299
300
        s << "</div>\n"
        ancestors << project
      end
      s << ("</li></ul>\n" * ancestors.size)
      @project = original_project
    end
    s.html_safe
  end

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

317
318
319
320
  # Renders flash messages
  def render_flash_messages
    s = ''
    flash.each do |k,v|
321
      s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
322
    end
323
    s.html_safe
324
  end
325

jplang's avatar
jplang committed
326
  # Renders tabs and their content
327
  def render_tabs(tabs, selected=params[:tab])
jplang's avatar
jplang committed
328
    if tabs.any?
329
330
331
332
333
      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
334
335
336
337
    else
      content_tag 'p', l(:label_no_data), :class => "nodata"
    end
  end
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
 
  # 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
357

358
359
  # Renders the project quick-jump box
  def render_project_jump_box
360
    projects = projects_for_jump_box(User.current)
361
    if projects.any?
362
363
364
365
366
367
368
369
      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')})
      content = content_tag('div',
            content_tag('div', q, :class => 'quick-search') + 
            content_tag('div', render_projects_for_jump_box(projects, @project), :class => 'drdn-items selection'),
          :class => 'drdn-content'
        )
emassip's avatar
emassip committed
370

371
      content_tag('span', trigger + content, :id => "project-jump", :class => "drdn")
372
373
    end
  end
374

375
  def project_tree_options_for_select(projects, options = {})
376
    s = ''.html_safe
377
378
379
380
381
    if blank_text = options[:include_blank]
      if blank_text == true
        blank_text = '&nbsp;'.html_safe
      end
      s << content_tag('option', blank_text, :value => '')
382
    end
383
    project_tree(projects) do |project, level|
emassip's avatar
emassip committed
384
      name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
385
386
387
388
389
390
      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
391
392
393
      tag_options.merge!(yield(project)) if block_given?
      s << content_tag('option', name_prefix + h(project), tag_options)
    end
394
    s.html_safe
395
  end
396

397
  # Yields the given block for each project with its level in the tree
398
399
  #
  # Wrapper for Project#project_tree
400
401
  def project_tree(projects, options={}, &block)
    Project.project_tree(projects, options, &block)
402
  end
403

jplang's avatar
jplang committed
404
405
  def principals_check_box_tags(name, principals)
    s = ''
406
    principals.each do |principal|
jplang's avatar
jplang committed
407
      s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
jplang's avatar
jplang committed
408
    end
409
    s.html_safe
jplang's avatar
jplang committed
410
  end
411

412
413
414
  # Returns a string for users/groups option tags
  def principals_options_for_select(collection, selected=nil)
    s = ''
415
    if collection.include?(User.current)
jplang's avatar
jplang committed
416
      s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
417
    end
418
419
    groups = ''
    collection.sort.each do |element|
420
      selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
421
422
423
424
425
      (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
426
    s.html_safe
427
  end
428

429
430
431
432
  def option_tag(name, text, value, selected=nil, options={})
    content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
  end

433
  def truncate_single_line_raw(string, length)
jplang's avatar
jplang committed
434
    string.to_s.truncate(length).gsub(%r{[\r\n]+}m, ' ')
435
436
  end

437
438
439
440
441
442
443
444
445
  # 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
446

jplang's avatar
jplang committed
447
448
449
450
  def anchor(text)
    text.to_s.gsub(' ', '_')
  end

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

jplang's avatar
jplang committed
455
  def authoring(created, author, options={})
456
    l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
457
  end
458

459
460
461
  def time_tag(time)
    text = distance_of_time_in_words(Time.now, time)
    if @project
jplang's avatar
jplang committed
462
      link_to(text, project_activity_path(@project, :from => User.current.time_to_date(time)), :title => format_time(time))
463
    else
464
      content_tag('abbr', text, :title => format_time(time))
465
    end
466
467
  end

468
  def syntax_highlight_lines(name, content)
469
    syntax_highlight(name, content).each_line.to_a
470
471
  end

jplang's avatar
jplang committed
472
  def syntax_highlight(name, content)
473
    Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
jplang's avatar
jplang committed
474
  end
475

jplang's avatar
jplang committed
476
  def to_path_param(path)
477
478
    str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
    str.blank? ? nil : str
jplang's avatar
jplang committed
479
480
  end

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

485
    link_to(l(:label_sort_highest),
486
487
            url.merge({"#{name}[move_to]" => 'highest'}), :method => method,
            :title => l(:label_sort_highest), :class => 'icon-only icon-move-top') +
488
    link_to(l(:label_sort_higher),
489
490
            url.merge({"#{name}[move_to]" => 'higher'}), :method => method,
            :title => l(:label_sort_higher), :class => 'icon-only icon-move-up') +
491
    link_to(l(:label_sort_lower),
492
493
            url.merge({"#{name}[move_to]" => 'lower'}), :method => method,
            :title => l(:label_sort_lower), :class => 'icon-only icon-move-down') +
494
    link_to(l(:label_sort_lowest),
495
496
            url.merge({"#{name}[move_to]" => 'lowest'}), :method => method,
            :title => l(:label_sort_lowest), :class => 'icon-only icon-move-bottom')
jplang's avatar
jplang committed
497
  end
498

499
500
501
502
503
504
  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
505
      :class => "sort-handle",
506
507
      :data => data,
      :title => l(:button_sort))
508
509
  end

510
  def breadcrumb(*args)
511
    elements = args.flatten
512
    elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
513
  end
514

515
  def other_formats_links(&block)
jplang's avatar
jplang committed
516
    concat('<p class="other-formats">'.html_safe + l(:label_export_to))
517
    yield Redmine::Views::OtherFormatsBuilder.new(self)
jplang's avatar
jplang committed
518
    concat('</p>'.html_safe)
519
  end
520

521
522
523
524
525
  def page_header_title
    if @project.nil? || @project.new_record?
      h(Setting.app_title)
    else
      b = []
jplang's avatar
jplang committed
526
      ancestors = (@project.root? ? [] : @project.ancestors.visible.to_a)
527
528
      if ancestors.any?
        root = ancestors.shift
529
        b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
530
        if ancestors.size > 2
531
          b << "\xe2\x80\xa6"
532
533
          ancestors = ancestors[-2, 2]
        end
534
        b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
535
      end
536
537
538
539
540
541
542
      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
543
544
    end
  end
545

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

577
578
579
580
581
582
583
584
  # 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

585
    css << 'project-' + @project.identifier if @project && @project.identifier.present?
586
587
    css << 'controller-' + controller_name
    css << 'action-' + action_name
588
589
590
    if UserPreference::TEXTAREA_FONT_OPTIONS.include?(User.current.pref.textarea_font)
      css << "textarea-#{User.current.pref.textarea_font}"
    end
591
592
593
    css.join(' ')
  end

jplang's avatar
jplang committed
594
  def accesskey(s)
595
596
597
598
599
    @used_accesskeys ||= []
    key = Redmine::AccessKeys.key_for(s)
    return nil if @used_accesskeys.include?(key)
    @used_accesskeys << key
    key
jplang's avatar
jplang committed
600
601
  end

602
603
604
605
606
607
608
609
  # 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
610
      obj = options[:object]
jplang's avatar
Fixes:    
jplang committed
611
      text = args.shift
612
613
    when 2
      obj = args.shift
614
615
      attr = args.shift
      text = obj.send(attr).to_s
616
617
618
    else
      raise ArgumentError, 'invalid arguments to textilizable'
    end
jplang's avatar
Fixes:    
jplang committed
619
    return '' if text.blank?
620
    project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
621
    @only_path = only_path = options.delete(:only_path) == false ? false : true
622

623
624
    text = text.dup
    macros = catch_macros(text)
625
626
627
628
629
630
631

    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
632

633
    @parsed_headings = []
634
    @heading_anchors = {}
635
    @current_section = 0 if options[:edit_section_links]
636
637

    parse_sections(text, project, obj, attr, only_path, options)
638
639
    text = parse_non_pre_blocks(text, obj, macros) do |text|
      [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
640
641
        send method_name, text, project, obj, attr, only_path, options
      end
642
    end
643
    parse_headings(text, project, obj, attr, only_path, options)
644

645
646
647
    if @parsed_headings.any?
      replace_toc(text, @parsed_headings)
    end
648

jplang's avatar
jplang committed
649
    text.html_safe
650
  end
651

652
  def parse_non_pre_blocks(text, obj, macros)
653
654
655
656
657
658
659
660
    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
661
662
663
        inject_macros(text, obj, macros) if macros.any?
      else
        inject_macros(text, obj, macros, false) if macros.any?
664
665
666
667
      end
      parsed << text
      if tag
        if closing
668
          if tags.last && tags.last.casecmp(tag) == 0
669
670
671
672
673
674
675
676
            tags.pop
          end
        else
          tags << tag.downcase
        end
        parsed << full_tag
      end
    end
jplang's avatar
jplang committed
677
678
679
680
    # Close any non closing tags
    while tag = tags.pop
      parsed << "</#{tag}>"
    end
681
    parsed
682
  end
683

684
  def parse_inline_attachments(text, project, obj, attr, only_path, options)
685
686
    return if options[:inline_attachments] == false

687
    # when using an image link, try to use an attachment, if possible
jplang's avatar
jplang committed
688
689
690
    attachments = options[:attachments] || []
    attachments += obj.attachments if obj.respond_to?(:attachments)
    if attachments.present?
691
      text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
692
        filename, ext, alt, alttext = $1.downcase, $2, $3, $4
693
        # search for the picture in attachments
694
        if found = Attachment.latest_attach(attachments, CGI.unescape(filename))
jplang's avatar
jplang committed
695
          image_url = download_named_attachment_url(found, found.filename, :only_path => only_path)
696
697
698
699
          desc = found.description.to_s.gsub('"', '')
          if !desc.blank? && alttext.blank?
            alt = " title=\"#{desc}\" alt=\"#{desc}\""
          end
700
          "src=\"#{image_url}\"#{alt}"
701
        else
702
          m
703
704
705
        end
      end
    end
706
  end
707

708
709
710
711
712
713
714
715
716
717
718
719
  # 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|
720
      link_project = project
721
722
723
      esc, all, page, title = $1, $2, $3, $5
      if esc.nil?
        if page =~ /^([^\:]+)\:(.*)$/
724
725
726
          identifier, page = $1, $2
          link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
          title ||= identifier if page.blank?
727
        end
728

729
        if link_project && link_project.wiki
730
731
732
733
734
          # extract anchor
          anchor = nil
          if page =~ /^(.+?)\#(.+)$/
            page, anchor = $1, $2
          end
735
          anchor = sanitize_anchor_name(anchor) if anchor.present?
736
737
          # check if page exists
          wiki_page = link_project.wiki.find_page(page)
738
739
740
741
          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]
742
            when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
743
            when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
744
            else
745
              wiki_page_id = page.present? ? Wiki.titleize(page) : nil
746
              parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
747
              url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
jplang's avatar
jplang committed
748
               :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
749
            end
750
          end
emassip's avatar
emassip committed
751
          link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
752
753
        else
          # project or wiki doesn't exist
754
          all
755
        end
jplang's avatar
jplang committed
756
      else
757
        all
jplang's avatar
jplang committed
758
      end
759
    end
760
  end
761

762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
  # 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
786
  #   Forum messages:
787
  #     message#1218 -> Link to message with id 1218
788
789
790
  #  Projects:
  #     project:someproject -> Link to project named "someproject"
  #     project#3 -> Link to project with id 3
791
792
793
794
795
796
  #
  #   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
797
  def parse_redmine_links(text, default_project, obj, attr, only_path, options)
798
799
800
801
802
803
804
805
806
807
808
809
810
811
    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]

812
813
814
815
816
817
818
819
820
821
      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'
822
823
            if project
              repository = nil
824
              if repo_identifier
825
826
827
828
                repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
              else
                repository = project.repository
              end
829
830
831
832
833
834
835
836
837
838
839
              # 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))
840
              end
841
            end
842
843
844
845
846
          elsif sep == '#'
            oid = identifier.to_i
            case prefix
            when nil
              if oid.to_s == identifier &&
jplang's avatar
jplang committed
847
                issue = Issue.visible.find_by_id(oid)
848
                anchor = comment_id ? "note-#{comment_id}" : nil
jplang's avatar
jplang committed
849
                link = link_to("##{oid}#{comment_suffix}",
jplang's avatar
jplang committed
850
                               issue_url(issue, :only_path => only_path, :anchor => anchor),
851
                               :class => issue.css_classes,
852
                               :title => "#{issue.tracker.name}: #{issue.subject.truncate(100)} (#{issue.status.name})")
853
854
855
              end
            when 'document'
              if document = Document.visible.find_by_id(oid)
jplang's avatar
jplang committed
856
                link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
857
858
859
              end
            when 'version'
              if version = Version.visible.find_by_id(oid)
jplang's avatar
jplang committed
860
                link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
861
862
              end
            when 'message'
jplang's avatar
jplang committed
863
              if message = Message.visible.find_by_id(oid)
864
865
866
867
                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
868
                link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
869
870
871
              end
            when 'news'
              if news = News.visible.find_by_id(oid)
jplang's avatar
jplang committed
872
                link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
873
874
875
876
877
              end
            when 'project'
              if p = Project.visible.find_by_id(oid)
                link = link_to_project(p, {:only_path => only_path}, :class => 'project')
              end
878
            end
879
880
881
882
883
884
885
          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
886
                link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
887
888
889
              end
            when 'version'
              if project && version = project.versions.visible.find_by_name(name)
jplang's avatar
jplang committed
890
                link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
891
892
893
              end
            when 'forum'
              if project && board = project.boards.visible.find_by_name(name)
jplang's avatar
jplang committed
894
                link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
895
896
897
              end
            when 'news'
              if project && news = project.news.visible.find_by_title(name)
jplang's avatar
jplang committed
898
                link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
              end
            when 'commit', 'source', 'export'
              if project
                repository = nil
                if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
                  repo_prefix, repo_identifier