application_helper.rb 41.5 KB
Newer Older
1 2
# encoding: utf-8
#
3
# Redmine - project management software
4
# Copyright (C) 2006-2011  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 102 103
    link_to(h(text),
           {:controller => 'attachments', :action => action,
            :id => attachment, :filename => attachment.filename },
           options)
104
  end
105

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

122 123 124 125 126 127 128 129 130 131 132 133 134
  # 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
135

136 137
  # Generates a link to a project if active
  # Examples:
138
  #
139 140 141 142 143 144 145 146 147 148 149 150 151 152
  #   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
153 154 155 156 157 158
  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
159

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

168 169 170 171
  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
172

173
  def format_activity_title(text)
174
    h(truncate_single_line(text, :length => 100))
175
  end
176

177 178 179
  def format_activity_day(date)
    date == Date.today ? l(:label_today).titleize : format_date(date)
  end
180

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

186 187 188 189 190 191 192
  def format_version_name(version)
    if version.project == @project
    	h(version)
    else
      h("#{version.project} - #{version}")
    end
  end
193

194 195 196 197 198
  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
199

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

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

jplang's avatar
jplang committed
225 226 227 228 229 230 231 232
  # 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
233

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

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

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

273 274 275 276 277 278 279 280 281 282
  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>"
283
          while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
284 285 286 287 288 289 290 291 292 293
            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
294
    s.html_safe
295
  end
296

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

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

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

327 328 329 330 331 332 333 334 335
  # 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
336

jplang's avatar
jplang committed
337 338 339 340
  def anchor(text)
    text.to_s.gsub(' ', '_')
  end

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

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

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

358
  def syntax_highlight(name, content)
359
    Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
360
  end
361

jplang's avatar
jplang committed
362 363 364 365
  def to_path_param(path)
    path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
  end

366
  def pagination_links_full(paginator, count=nil, options={})
367
    page_param = options.delete(:page_param) || :page
368
    per_page_links = options.delete(:per_page_links)
369
    url_param = params.dup
370 371

    html = ''
372
    if paginator.current.previous
373 374 375 376
      # \xc2\xab(utf-8) = &#171;
      html << link_to_content_update(
                   "\xc2\xab " + l(:label_previous),
                   url_param.merge(page_param => paginator.current.previous)) + ' '
377
    end
378

379
    html << (pagination_links_each(paginator, options) do |n|
380
      link_to_content_update(n.to_s, url_param.merge(page_param => n))
381
    end || '')
382

383
    if paginator.current.next
384 385 386 387
      # \xc2\xbb(utf-8) = &#187;
      html << ' ' + link_to_content_update(
                      (l(:label_next) + " \xc2\xbb"),
                      url_param.merge(page_param => paginator.current.next))
388
    end
389

390
    unless count.nil?
391 392 393 394
      html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
      if per_page_links != false && links = per_page_links(paginator.items_per_page)
	      html << " | #{links}"
      end
395
    end
396

397
    html.html_safe
398
  end
399

400 401
  def per_page_links(selected=nil)
    links = Setting.per_page_options_array.collect do |n|
402
      n == selected ? n : link_to_content_update(n, params.merge(:per_page => n))
403 404 405
    end
    links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
  end
406

jplang's avatar
jplang committed
407
  def reorder_links(name, url, method = :post)
408 409
    link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
            url.merge({"#{name}[move_to]" => 'highest'}),
jplang's avatar
jplang committed
410
            :method => method, :title => l(:label_sort_highest)) +
411 412
    link_to(image_tag('1uparrow.png',   :alt => l(:label_sort_higher)),
            url.merge({"#{name}[move_to]" => 'higher'}),
jplang's avatar
jplang committed
413
           :method => method, :title => l(:label_sort_higher)) +
414 415
    link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
            url.merge({"#{name}[move_to]" => 'lower'}),
jplang's avatar
jplang committed
416
            :method => method, :title => l(:label_sort_lower)) +
417 418
    link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
            url.merge({"#{name}[move_to]" => 'lowest'}),
jplang's avatar
jplang committed
419
           :method => method, :title => l(:label_sort_lowest))
jplang's avatar
jplang committed
420
  end
421

422
  def breadcrumb(*args)
423
    elements = args.flatten
424
    elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
425
  end
426

427
  def other_formats_links(&block)
jplang's avatar
jplang committed
428
    concat('<p class="other-formats">'.html_safe + l(:label_export_to))
429
    yield Redmine::Views::OtherFormatsBuilder.new(self)
jplang's avatar
jplang committed
430
    concat('</p>'.html_safe)
431
  end
432

433 434 435 436 437
  def page_header_title
    if @project.nil? || @project.new_record?
      h(Setting.app_title)
    else
      b = []
jplang's avatar
jplang committed
438
      ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
439 440
      if ancestors.any?
        root = ancestors.shift
441
        b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
442
        if ancestors.size > 2
443
          b << "\xe2\x80\xa6"
444 445
          ancestors = ancestors[-2, 2]
        end
446
        b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
447 448
      end
      b << h(@project)
449
      b.join(" \xc2\xbb ").html_safe
450 451
    end
  end
452

453 454
  def html_title(*args)
    if args.empty?
455
      title = @html_title || []
tmaruyama's avatar
tmaruyama committed
456
      title << @project.name if @project
457
      title << Setting.app_title unless Setting.app_title == title.last
jplang's avatar
jplang committed
458
      title.select {|t| !t.blank? }.join(' - ')
459 460 461 462
    else
      @html_title ||= []
      @html_title += args
    end
463
  end
jplang's avatar
jplang committed
464

465 466 467 468 469 470 471 472 473 474 475 476 477
  # 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

    css << 'controller-' + params[:controller]
    css << 'action-' + params[:action]
    css.join(' ')
  end

jplang's avatar
jplang committed
478
  def accesskey(s)
479
    Redmine::AccessKeys.key_for s
jplang's avatar
jplang committed
480 481
  end

482 483 484 485 486 487 488 489
  # 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
490
      obj = options[:object]
jplang's avatar
jplang committed
491
      text = args.shift
492 493
    when 2
      obj = args.shift
494 495
      attr = args.shift
      text = obj.send(attr).to_s
496 497 498
    else
      raise ArgumentError, 'invalid arguments to textilizable'
    end
jplang's avatar
jplang committed
499
    return '' if text.blank?
500 501
    project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
    only_path = options.delete(:only_path) == false ? false : true
502

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

505
    @parsed_headings = []
506
    @heading_anchors = {}
507
    @current_section = 0 if options[:edit_section_links]
508 509

    parse_sections(text, project, obj, attr, only_path, options)
510
    text = parse_non_pre_blocks(text) do |text|
511
      [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_macros].each do |method_name|
512 513
        send method_name, text, project, obj, attr, only_path, options
      end
514
    end
515
    parse_headings(text, project, obj, attr, only_path, options)
516

517 518 519
    if @parsed_headings.any?
      replace_toc(text, @parsed_headings)
    end
520

jplang's avatar
jplang committed
521
    text.html_safe
522
  end
523

524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545
  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
546 547 548 549
    # Close any non closing tags
    while tag = tags.pop
      parsed << "</#{tag}>"
    end
550
    parsed.html_safe
551
  end
552

553
  def parse_inline_attachments(text, project, obj, attr, only_path, options)
554
    # when using an image link, try to use an attachment, if possible
555
    if options[:attachments] || (obj && obj.respond_to?(:attachments))
556
      attachments = options[:attachments] || obj.attachments
557
      text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
558
        filename, ext, alt, alttext = $1.downcase, $2, $3, $4
559
        # search for the picture in attachments
560 561 562
        if found = Attachment.latest_attach(attachments, filename)
          image_url = url_for :only_path => only_path, :controller => 'attachments',
                              :action => 'download', :id => found
563 564 565 566
          desc = found.description.to_s.gsub('"', '')
          if !desc.blank? && alttext.blank?
            alt = " title=\"#{desc}\" alt=\"#{desc}\""
          end
567
          "src=\"#{image_url}\"#{alt}".html_safe
568
        else
569
          m.html_safe
570 571 572
        end
      end
    end
573
  end
574

575 576 577 578 579 580 581 582 583 584 585 586
  # 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|
587
      link_project = project
588 589 590
      esc, all, page, title = $1, $2, $3, $5
      if esc.nil?
        if page =~ /^([^\:]+)\:(.*)$/
591
          link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
592 593 594
          page = $2
          title ||= $1 if page.blank?
        end
595

596
        if link_project && link_project.wiki
597 598 599 600 601
          # extract anchor
          anchor = nil
          if page =~ /^(.+?)\#(.+)$/
            page, anchor = $1, $2
          end
602
          anchor = sanitize_anchor_name(anchor) if anchor.present?
603 604
          # check if page exists
          wiki_page = link_project.wiki.find_page(page)
605 606 607 608
          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]
609
            when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
610
            when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
611
            else
612
              wiki_page_id = page.present? ? Wiki.titleize(page) : nil
613 614 615
              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)
616
            end
617
          end
emassip's avatar
emassip committed
618
          link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
619 620
        else
          # project or wiki doesn't exist
621
          all.html_safe
622
        end
jplang's avatar
jplang committed
623
      else
624
        all.html_safe
jplang's avatar
jplang committed
625
      end
626
    end
627
  end
628

629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652
  # 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
653
  #   Forum messages:
654
  #     message#1218 -> Link to message with id 1218
655 656 657 658 659 660
  #
  #   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
661
  def parse_redmine_links(text, project, obj, attr, only_path, options)
662 663
    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
664
      link = nil
665 666 667
      if project_identifier
        project = Project.visible.find_by_identifier(project_identifier)
      end
668 669
      if esc.nil?
        if prefix.nil? && sep == 'r'
670 671 672 673 674 675 676 677 678 679 680 681 682
          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
683 684
          end
        elsif sep == '#'
685
          oid = identifier.to_i
686 687
          case prefix
          when nil
jplang's avatar
jplang committed
688
            if issue = Issue.visible.find_by_id(oid, :include => :status)
689 690
              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
691
                                        :class => issue.css_classes,
692
                                        :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
693 694
            end
          when 'document'
695
            if document = Document.visible.find_by_id(oid)
jplang's avatar
jplang committed
696 697
              link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
                                                :class => 'document'
698 699
            end
          when 'version'
700
            if version = Version.visible.find_by_id(oid)
jplang's avatar
jplang committed
701 702
              link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
                                              :class => 'version'
703
            end
704
          when 'message'
705
            if message = Message.visible.find_by_id(oid, :include => :parent)
706
              link = link_to_message(message, {:only_path => only_path}, :class => 'message')
707
            end
708 709 710 711 712 713 714 715 716 717
          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
718 719
          when 'project'
            if p = Project.visible.find_by_id(oid)
720
              link = link_to_project(p, {:only_path => only_path}, :class => 'project')
jplang's avatar
jplang committed
721
            end
722 723 724
          end
        elsif sep == ':'
          # removes the double quotes if any
725
          name = identifier.gsub(%r{^"(.*)"$}, "\\1")
726 727
          case prefix
          when 'document'
728
            if project && document = project.documents.visible.find_by_title(name)
jplang's avatar
jplang committed
729 730
              link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
                                                :class => 'document'
731 732
            end
          when 'version'
733
            if project && version = project.versions.visible.find_by_name(name)
jplang's avatar
jplang committed
734 735
              link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
                                              :class => 'version'
736
            end
737 738 739 740 741 742 743 744 745 746
          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
747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774
          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
775
            end
776
          when 'attachment'
777
            attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
778
            if attachments && attachment = attachments.detect {|a| a.filename == name }
jplang's avatar
jplang committed
779 780
              link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
                                                     :class => 'attachment'
781
            end
jplang's avatar
jplang committed
782 783
          when 'project'
            if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
784
              link = link_to_project(p, {:only_path => only_path}, :class => 'project')
jplang's avatar
jplang committed
785
            end
786
          end
jplang's avatar
jplang committed
787
        end
788
      end
789
      (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}")).html_safe
790
    end
791
  end
792

793 794 795 796 797
  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
798 799
      @current_section += 1
      if @current_section > 1
800
        content_tag('div',
801
          link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
802 803 804 805 806 807 808
          :class => 'contextual',
          :title => l(:button_edit_section)) + $1
      else
        $1
      end
    end
  end
809

810
  # Headings and TOC
811
  # Adds ids and links to headings unless options[:headings] is set to false
812
  def parse_headings(text, project, obj, attr, only_path, options)
813
    return if options[:headings] == false
814

815
    text.gsub!(HEADING_RE) do
816
      level, attrs, content = $2.to_i, $3, $4
817
      item = strip_tags(content).strip
818
      anchor = sanitize_anchor_name(item)
819 820
      # 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))
821 822 823 824 825
      @heading_anchors[anchor] ||= 0
      idx = (@heading_anchors[anchor] += 1)
      if idx > 1
        anchor = "#{anchor}-#{idx}"
      end
826
      @parsed_headings << [level, anchor, item]
827
      "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
828 829
    end
  end
830

831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857
  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

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

860 861
  # Renders the TOC with given headings
  def replace_toc(text, headings)
862 863 864 865 866 867 868
    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
869 870 871 872
        out = "<ul class=\"#{div_class}\"><li>"
        root = headings.map(&:first).min
        current = root
        started = false
873
        headings.each do |level, anchor, item|
jplang's avatar
jplang committed
874 875 876 877 878 879 880 881 882 883
          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
884
        end
jplang's avatar
jplang committed
885 886
        out << '</li></ul>' * (current - root)
        out << '</li></ul>'
887 888 889
      end
    end
  end
890

jplang's avatar
jplang committed
891 892 893 894 895
  # 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
896 897
      gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline   -> br
      html_safe
jplang's avatar
jplang committed
898
  end
899

900
  def lang_options_for_select(blank=true)
901
    (blank ? [["(auto)", ""]] : []) +
902
      valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
903
  end
904

905 906 907 908
  def label_tag_for(name, option_tags = nil, options = {})
    label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
    content_tag("label", label_text)
  end
909

910
  def labelled_tabular_form_for(*args, &proc)
911
    ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_tabular_form_for is deprecated and will be removed in Redmine 1.5. Use #labelled_form_for instead."
912 913
    args << {} unless args.last.is_a?(Hash)
    options = args.last
914
    options[:html] ||= {}
915
    options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
916
    options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
917
    form_for(*args, &proc)
918
  end
919

jplang's avatar
jplang committed
920 921 922
  def labelled_form_for(*args, &proc)
    args << {} unless args.last.is_a?(Hash)
    options = args.last
923
    options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
jplang's avatar
jplang committed
924 925 926
    form_for(*args, &proc)
  end

927 928 929
  def labelled_fields_for(*args, &proc)
    args << {} unless args.last.is_a?(Hash)
    options = args.last
930
    options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
</