application_helper.rb 44.8 KB
Newer Older
1 2
# encoding: utf-8
#
3
# Redmine - project management software
jplang's avatar
jplang committed
4
# Copyright (C) 2006-2012  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

28 29 30
  extend Forwardable
  def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter

31
  # Return true if user is authorized for controller/action, otherwise false
jplang's avatar
jplang committed
32 33
  def authorize_for(controller, action)
    User.current.allowed_to?({:controller => controller, :action => action}, @project)
34 35 36
  end

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

46 47 48 49
  # Display a link to remote if user is authorized
  def link_to_remote_if_authorized(name, options = {}, html_options = nil)
    url = options[:url] || {}
    link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
50 51
  end

52
  # Displays a link to user's account page if active
53
  def link_to_user(user, options={})
jplang's avatar
jplang committed
54
    if user.is_a?(User)
55 56 57 58 59 60
      name = h(user.name(options[:format]))
      if user.active?
        link_to name, :controller => 'users', :action => 'show', :id => user
      else
        name
      end
jplang's avatar
jplang committed
61
    else
62
      h(user.to_s)
jplang's avatar
jplang committed
63
    end
64
  end
65

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

93 94 95 96 97 98 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
    action = options.delete(:download) ? 'download' : 'show'
100 101
    opt_only_path = {}
    opt_only_path[:only_path] = (options[:only_path] == false ? false : true)
102
    options.delete(:only_path)
103 104
    link_to(h(text),
           {:controller => 'attachments', :action => action,
105
            :id => attachment, :filename => attachment.filename}.merge(opt_only_path),
106
           options)
107
  end
108

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

125 126 127 128 129 130 131 132 133 134 135 136 137
  # Generates a link to a message
  def link_to_message(message, options={}, html_options = nil)
    link_to(
      h(truncate(message.subject, :length => 60)),
      { :controller => 'messages', :action => 'show',
        :board_id => message.board_id,
        :id => message.root,
        :r => (message.parent_id && message.id),
        :anchor => (message.parent_id ? "message-#{message.id}" : nil)
      }.merge(options),
      html_options
    )
  end
138

139 140
  # Generates a link to a project if active
  # Examples:
141
  #
142 143 144 145 146 147 148 149 150 151 152 153 154 155
  #   link_to_project(project)                          # => link to the specified project overview
  #   link_to_project(project, :action=>'settings')     # => link to project settings
  #   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)
    if project.active?
      url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
      link_to(h(project), url, html_options)
    else
      h(project)
    end
  end

jplang's avatar
jplang committed
156 157 158 159 160 161
  def toggle_link(name, id, options={})
    onclick = "Element.toggle('#{id}'); "
    onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
    onclick << "return false;"
    link_to(name, "#", :onclick => onclick)
  end
162

163 164
  def image_to_function(name, function, html_options = {})
    html_options.symbolize_keys!
165 166 167
    tag(:input, html_options.merge({
        :type => "image", :src => image_path(name),
        :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
168 169
        }))
  end
170

171 172 173 174
  def prompt_to_remote(name, text, param, url, html_options = {})
    html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
    link_to name, {}, html_options
  end
175

176
  def format_activity_title(text)
177
    h(truncate_single_line(text, :length => 100))
178
  end
179

180 181 182
  def format_activity_day(date)
    date == Date.today ? l(:label_today).titleize : format_date(date)
  end
183

184
  def format_activity_description(text)
185 186
    h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
       ).gsub(/[\r\n]+/, "<br />").html_safe
187
  end
188

189 190 191 192 193 194 195
  def format_version_name(version)
    if version.project == @project
    	h(version)
    else
      h("#{version.project} - #{version}")
    end
  end
196

197 198 199 200 201
  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
202

203
  def render_page_hierarchy(pages, node=nil, options={})
204 205 206 207 208
    content = ''
    if pages[node]
      content << "<ul class=\"pages-hierarchy\">\n"
      pages[node].each do |page|
        content << "<li>"
209
        content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
210 211
                           :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]
212 213 214 215
        content << "</li>\n"
      end
      content << "</ul>\n"
    end
216
    content.html_safe
217
  end
218

219 220 221 222
  # Renders flash messages
  def render_flash_messages
    s = ''
    flash.each do |k,v|
223
      s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
224
    end
225
    s.html_safe
226
  end
227

jplang's avatar
jplang committed
228 229 230 231 232 233 234 235
  # Renders tabs and their content
  def render_tabs(tabs)
    if tabs.any?
      render :partial => 'common/tabs', :locals => {:tabs => tabs}
    else
      content_tag 'p', l(:label_no_data), :class => "nodata"
    end
  end
236

237 238
  # Renders the project quick-jump box
  def render_project_jump_box
239
    return unless User.current.logged?
240
    projects = User.current.memberships.collect(&:project).compact.uniq
241 242
    if projects.any?
      s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
243
            "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
jplang's avatar
jplang committed
244
            '<option value="" disabled="disabled">---</option>'
245
      s << project_tree_options_for_select(projects, :selected => @project) do |p|
246
        { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
247 248
      end
      s << '</select>'
249
      s.html_safe
250 251
    end
  end
252

253 254 255
  def project_tree_options_for_select(projects, options = {})
    s = ''
    project_tree(projects) do |project, level|
256
      name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ').html_safe : '')
257 258 259 260 261 262
      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
263 264 265
      tag_options.merge!(yield(project)) if block_given?
      s << content_tag('option', name_prefix + h(project), tag_options)
    end
266
    s.html_safe
267
  end
268

269
  # Yields the given block for each project with its level in the tree
270 271
  #
  # Wrapper for Project#project_tree
272
  def project_tree(projects, &block)
273
    Project.project_tree(projects, &block)
274
  end
275

276 277 278 279 280 281 282 283 284 285
  def project_nested_ul(projects, &block)
    s = ''
    if projects.any?
      ancestors = []
      projects.sort_by(&:lft).each do |project|
        if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
          s << "<ul>\n"
        else
          ancestors.pop
          s << "</li>"
286
          while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
287 288 289 290 291 292 293 294 295 296
            ancestors.pop
            s << "</ul></li>\n"
          end
        end
        s << "<li>"
        s << yield(project).to_s
        ancestors << project
      end
      s << ("</li></ul>\n" * ancestors.size)
    end
297
    s.html_safe
298
  end
299

jplang's avatar
jplang committed
300 301
  def principals_check_box_tags(name, principals)
    s = ''
302
    principals.sort.each do |principal|
jplang's avatar
jplang committed
303 304
      s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
    end
305
    s.html_safe
jplang's avatar
jplang committed
306
  end
307

308 309 310
  # Returns a string for users/groups option tags
  def principals_options_for_select(collection, selected=nil)
    s = ''
311
    if collection.include?(User.current)
312
      s << content_tag('option', "<< #{l(:label_me)} >>".html_safe, :value => User.current.id)
313
    end
314 315 316 317 318 319 320 321
    groups = ''
    collection.sort.each do |element|
      selected_attribute = ' selected="selected"' if option_value_selected?(element, selected)
      (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
322
    s.html_safe
323
  end
324

325 326
  # Truncates and returns the string as a single line
  def truncate_single_line(string, *args)
327
    truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
328
  end
329

330 331 332 333 334 335 336 337 338
  # 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
339

jplang's avatar
jplang committed
340 341 342 343
  def anchor(text)
    text.to_s.gsub(' ', '_')
  end

344
  def html_hours(text)
345
    text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
346
  end
347

jplang's avatar
jplang committed
348
  def authoring(created, author, options={})
349
    l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
350
  end
351

352 353 354
  def time_tag(time)
    text = distance_of_time_in_words(Time.now, time)
    if @project
355
      link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
356 357 358
    else
      content_tag('acronym', text, :title => format_time(time))
    end
359 360
  end

361 362 363 364 365 366
  def syntax_highlight_lines(name, content)
    lines = []
    syntax_highlight(name, content).each_line { |line| lines << line }
    lines
  end

367
  def syntax_highlight(name, content)
368
    Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
369
  end
370

jplang's avatar
jplang committed
371
  def to_path_param(path)
372 373
    str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
    str.blank? ? nil : str
jplang's avatar
jplang committed
374 375
  end

376
  def pagination_links_full(paginator, count=nil, options={})
377
    page_param = options.delete(:page_param) || :page
378
    per_page_links = options.delete(:per_page_links)
379
    url_param = params.dup
380 381

    html = ''
382
    if paginator.current.previous
383 384 385 386
      # \xc2\xab(utf-8) = &#171;
      html << link_to_content_update(
                   "\xc2\xab " + l(:label_previous),
                   url_param.merge(page_param => paginator.current.previous)) + ' '
387
    end
388

389
    html << (pagination_links_each(paginator, options) do |n|
390
      link_to_content_update(n.to_s, url_param.merge(page_param => n))
391
    end || '')
392

393
    if paginator.current.next
394 395 396 397
      # \xc2\xbb(utf-8) = &#187;
      html << ' ' + link_to_content_update(
                      (l(:label_next) + " \xc2\xbb"),
                      url_param.merge(page_param => paginator.current.next))
398
    end
399

400
    unless count.nil?
401
      html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
402
      if per_page_links != false && links = per_page_links(paginator.items_per_page, count)
403 404
	      html << " | #{links}"
      end
405
    end
406

407
    html.html_safe
408
  end
409

410 411 412 413 414 415 416 417 418 419 420 421 422 423
  def per_page_links(selected=nil, item_count=nil)
    values = Setting.per_page_options_array
    if item_count && values.any?
      if item_count > values.first
        max = values.detect {|value| value >= item_count} || item_count
      else
        max = item_count
      end
      values = values.select {|value| value <= max || value == selected}
    end
    if values.empty? || (values.size == 1 && values.first == selected)
      return nil
    end
    links = values.collect do |n|
424
      n == selected ? n : link_to_content_update(n, params.merge(:per_page => n))
425
    end
426
    l(:label_display_per_page, links.join(', '))
427
  end
428

jplang's avatar
jplang committed
429
  def reorder_links(name, url, method = :post)
430 431
    link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
            url.merge({"#{name}[move_to]" => 'highest'}),
jplang's avatar
jplang committed
432
            :method => method, :title => l(:label_sort_highest)) +
433 434
    link_to(image_tag('1uparrow.png',   :alt => l(:label_sort_higher)),
            url.merge({"#{name}[move_to]" => 'higher'}),
jplang's avatar
jplang committed
435
           :method => method, :title => l(:label_sort_higher)) +
436 437
    link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
            url.merge({"#{name}[move_to]" => 'lower'}),
jplang's avatar
jplang committed
438
            :method => method, :title => l(:label_sort_lower)) +
439 440
    link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
            url.merge({"#{name}[move_to]" => 'lowest'}),
jplang's avatar
jplang committed
441
           :method => method, :title => l(:label_sort_lowest))
jplang's avatar
jplang committed
442
  end
443

444
  def breadcrumb(*args)
445
    elements = args.flatten
446
    elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
447
  end
448

449
  def other_formats_links(&block)
jplang's avatar
jplang committed
450
    concat('<p class="other-formats">'.html_safe + l(:label_export_to))
451
    yield Redmine::Views::OtherFormatsBuilder.new(self)
jplang's avatar
jplang committed
452
    concat('</p>'.html_safe)
453
  end
454

455 456 457 458 459
  def page_header_title
    if @project.nil? || @project.new_record?
      h(Setting.app_title)
    else
      b = []
jplang's avatar
jplang committed
460
      ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
461 462
      if ancestors.any?
        root = ancestors.shift
463
        b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
464
        if ancestors.size > 2
465
          b << "\xe2\x80\xa6"
466 467
          ancestors = ancestors[-2, 2]
        end
468
        b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
469 470
      end
      b << h(@project)
471
      b.join(" \xc2\xbb ").html_safe
472 473
    end
  end
474

475 476
  def html_title(*args)
    if args.empty?
477
      title = @html_title || []
tmaruyama's avatar
tmaruyama committed
478
      title << @project.name if @project
479
      title << Setting.app_title unless Setting.app_title == title.last
jplang's avatar
jplang committed
480
      title.select {|t| !t.blank? }.join(' - ')
481 482 483 484
    else
      @html_title ||= []
      @html_title += args
    end
485
  end
jplang's avatar
jplang committed
486

487 488 489 490 491 492 493 494
  # 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

495 496
    css << 'controller-' + controller_name
    css << 'action-' + action_name
497 498 499
    css.join(' ')
  end

jplang's avatar
jplang committed
500
  def accesskey(s)
501
    Redmine::AccessKeys.key_for s
jplang's avatar
jplang committed
502 503
  end

504 505 506 507 508 509 510 511
  # 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
512
      obj = options[:object]
jplang's avatar
jplang committed
513
      text = args.shift
514 515
    when 2
      obj = args.shift
516 517
      attr = args.shift
      text = obj.send(attr).to_s
518 519 520
    else
      raise ArgumentError, 'invalid arguments to textilizable'
    end
jplang's avatar
jplang committed
521
    return '' if text.blank?
522 523
    project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
    only_path = options.delete(:only_path) == false ? false : true
524

525
    text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
526

527
    @parsed_headings = []
528
    @heading_anchors = {}
529
    @current_section = 0 if options[:edit_section_links]
530 531

    parse_sections(text, project, obj, attr, only_path, options)
532
    text = parse_non_pre_blocks(text) do |text|
533
      [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_macros].each do |method_name|
534 535
        send method_name, text, project, obj, attr, only_path, options
      end
536
    end
537
    parse_headings(text, project, obj, attr, only_path, options)
538

539 540 541
    if @parsed_headings.any?
      replace_toc(text, @parsed_headings)
    end
542

jplang's avatar
jplang committed
543
    text.html_safe
544
  end
545

546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567
  def parse_non_pre_blocks(text)
    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
      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
568 569 570 571
    # Close any non closing tags
    while tag = tags.pop
      parsed << "</#{tag}>"
    end
572
    parsed
573
  end
574

575
  def parse_inline_attachments(text, project, obj, attr, only_path, options)
576
    # when using an image link, try to use an attachment, if possible
577
    if options[:attachments] || (obj && obj.respond_to?(:attachments))
578
      attachments = options[:attachments] || obj.attachments
579
      text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
580
        filename, ext, alt, alttext = $1.downcase, $2, $3, $4
581
        # search for the picture in attachments
582 583 584
        if found = Attachment.latest_attach(attachments, filename)
          image_url = url_for :only_path => only_path, :controller => 'attachments',
                              :action => 'download', :id => found
585 586 587 588
          desc = found.description.to_s.gsub('"', '')
          if !desc.blank? && alttext.blank?
            alt = " title=\"#{desc}\" alt=\"#{desc}\""
          end
589
          "src=\"#{image_url}\"#{alt}"
590
        else
591
          m
592 593 594
        end
      end
    end
595
  end
596

597 598 599 600 601 602 603 604 605 606 607 608
  # 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|
609
      link_project = project
610 611 612
      esc, all, page, title = $1, $2, $3, $5
      if esc.nil?
        if page =~ /^([^\:]+)\:(.*)$/
613
          link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
614 615 616
          page = $2
          title ||= $1 if page.blank?
        end
617

618
        if link_project && link_project.wiki
619 620 621 622 623
          # extract anchor
          anchor = nil
          if page =~ /^(.+?)\#(.+)$/
            page, anchor = $1, $2
          end
624
          anchor = sanitize_anchor_name(anchor) if anchor.present?
625 626
          # check if page exists
          wiki_page = link_project.wiki.find_page(page)
627 628 629 630
          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]
631
            when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
632
            when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
633
            else
634
              wiki_page_id = page.present? ? Wiki.titleize(page) : nil
635 636 637
              parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
              url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, 
               :id => wiki_page_id, :anchor => anchor, :parent => parent)
638
            end
639
          end
emassip's avatar
emassip committed
640
          link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
641 642
        else
          # project or wiki doesn't exist
643
          all
644
        end
jplang's avatar
jplang committed
645
      else
646
        all
jplang's avatar
jplang committed
647
      end
648
    end
649
  end
650

651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674
  # 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
675
  #   Forum messages:
676
  #     message#1218 -> Link to message with id 1218
677 678 679 680 681 682
  #
  #   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
683
  def parse_redmine_links(text, project, obj, attr, only_path, options)
684 685
    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|
      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
686
      link = nil
687 688 689
      if project_identifier
        project = Project.visible.find_by_identifier(project_identifier)
      end
690 691
      if esc.nil?
        if prefix.nil? && sep == 'r'
692 693 694 695 696 697 698 699 700 701 702 703 704
          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
            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(changeset.comments, :length => 100))
            end
705 706
          end
        elsif sep == '#'
707
          oid = identifier.to_i
708 709
          case prefix
          when nil
jplang's avatar
jplang committed
710
            if issue = Issue.visible.find_by_id(oid, :include => :status)
711 712
              anchor = comment_id ? "note-#{comment_id}" : nil
              link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid, :anchor => anchor},
jplang's avatar
jplang committed
713
                                        :class => issue.css_classes,
714
                                        :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
715 716
            end
          when 'document'
717
            if document = Document.visible.find_by_id(oid)
jplang's avatar
jplang committed
718 719
              link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
                                                :class => 'document'
720 721
            end
          when 'version'
722
            if version = Version.visible.find_by_id(oid)
jplang's avatar
jplang committed
723 724
              link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
                                              :class => 'version'
725
            end
726
          when 'message'
727
            if message = Message.visible.find_by_id(oid, :include => :parent)
728
              link = link_to_message(message, {:only_path => only_path}, :class => 'message')
729
            end
730 731 732 733 734 735 736 737 738 739
          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
740 741
          when 'project'
            if p = Project.visible.find_by_id(oid)
742
              link = link_to_project(p, {:only_path => only_path}, :class => 'project')
jplang's avatar
jplang committed
743
            end
744 745 746
          end
        elsif sep == ':'
          # removes the double quotes if any
747
          name = identifier.gsub(%r{^"(.*)"$}, "\\1")
748 749
          case prefix
          when 'document'
750
            if project && document = project.documents.visible.find_by_title(name)
jplang's avatar
jplang committed
751 752
              link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
                                                :class => 'document'
753 754
            end
          when 'version'
755
            if project && version = project.versions.visible.find_by_name(name)
jplang's avatar
jplang committed
756 757
              link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
                                              :class => 'version'
758
            end
759 760 761 762 763 764 765 766 767 768
          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
769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796
          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.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%"]))
                  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(h(changeset.comments), :length => 100)
                end
              else
                if repository && User.current.allowed_to?(:browse_repository, project)
                  name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
                  path, rev, anchor = $1, $3, $5
                  link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => 'entry', :id => project, :repository_id => repository.identifier_param,
                                                          :path => to_path_param(path),
                                                          :rev => rev,
                                                          :anchor => anchor,
                                                          :format => (prefix == 'export' ? 'raw' : nil)},
                                                         :class => (prefix == 'export' ? 'source download' : 'source')
                end
              end
              repo_prefix = nil
797
            end
798
          when 'attachment'
799
            attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
800
            if attachments && attachment = attachments.detect {|a| a.filename == name }
jplang's avatar
jplang committed
801 802
              link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
                                                     :class => 'attachment'
803
            end
jplang's avatar
jplang committed
804 805
          when 'project'
            if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
806
              link = link_to_project(p, {:only_path => only_path}, :class => 'project')
jplang's avatar
jplang committed
807
            end
808
          end
jplang's avatar
jplang committed
809
        end
810
      end
811
      (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
812
    end
813
  end
814

815 816 817 818 819
  HEADING_RE = /(<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>)/i unless const_defined?(:HEADING_RE)

  def parse_sections(text, project, obj, attr, only_path, options)
    return unless options[:edit_section_links]
    text.gsub!(HEADING_RE) do
820
      heading = $1
821 822
      @current_section += 1
      if @current_section > 1
823
        content_tag('div',
824
          link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
825
          :class => 'contextual',
826
          :title => l(:button_edit_section)) + heading.html_safe
827
      else
828
        heading
829 830 831
      end
    end
  end
832

833
  # Headings and TOC
834
  # Adds ids and links to headings unless options[:headings] is set to false
835
  def parse_headings(text, project, obj, attr, only_path, options)
836
    return if options[:headings] == false
837

838
    text.gsub!(HEADING_RE) do
839
      level, attrs, content = $2.to_i, $3, $4
840
      item = strip_tags(content).strip
841
      anchor = sanitize_anchor_name(item)
842 843
      # used for single-file wiki export
      anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
844 845 846 847 848
      @heading_anchors[anchor] ||= 0
      idx = (@heading_anchors[anchor] += 1)
      if idx > 1
        anchor = "#{anchor}-#{idx}"
      end
849
      @parsed_headings << [level, anchor, item]
850
      "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
851 852
    end
  end
853

854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880
  MACROS_RE = /
                (!)?                        # escaping
                (
                \{\{                        # opening tag
                ([\w]+)                     # macro name
                (\(([^\}]*)\))?             # optional arguments
                \}\}                        # closing tag
                )
              /x unless const_defined?(:MACROS_RE)

  # Macros substitution
  def parse_macros(text, project, obj, attr, only_path, options)
    text.gsub!(MACROS_RE) do
      esc, all, macro = $1, $2, $3.downcase
      args = ($5 || '').split(',').each(&:strip)
      if esc.nil?
        begin
          exec_macro(macro, obj, args)
        rescue => e
          "<div class=\"flash error\">Error executing the <strong>#{macro}</strong> macro (#{e})</div>"
        end || all
      else
        all
      end
    end
  end

881
  TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
882

883 884
  # Renders the TOC with given headings
  def replace_toc(text, headings)
885 886 887 888 889 890 891
    text.gsub!(TOC_RE) do
      if headings.empty?
        ''
      else
        div_class = 'toc'
        div_class << ' right' if $1 == '>'
        div_class << ' left' if $1 == '<'
jplang's avatar
jplang committed
892 893 894 895
        out = "<ul class=\"#{div_class}\"><li>"
        root = headings.map(&:first).min
        current = root
        started = false
896
        headings.each do |level, anchor, item|
jplang's avatar
jplang committed
897 898 899 900 901 902 903 904 905 906
          if level > current
            out << '<ul><li>' * (level - current)
          elsif level < current
            out << "</li></ul>\n" * (current - level) + "</li><li>"
          elsif started
            out << '</li><li>'
          end
          out << "<a href=\"##{anchor}\">#{item}</a>"
          current = level
          started = true
907
        end
jplang's avatar
jplang committed
908 909
        out << '</li></ul>' * (current - root)
        out << '</li></ul>'
910 911 912
      end
    end
  end
913

jplang's avatar
jplang committed
914 915 916 917 918
  # Same as Rails' simple_format helper without using paragraphs
  def simple_format_without_paragraph(text)
    text.to_s.
      gsub(/\r\n?/, "\n").                    # \r\n and \r -> \n
      gsub(/\n\n+/, "<br /><br />").          # 2+ newline  -> 2 br
919 920
      gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline   -> br
      html_safe
jplang's avatar
jplang committed
921
  end
922

923
  def lang_options_for_select(blank=true)