"Fossies" - the Fresh Open Source Software Archive

Member "redmine-4.1.1/app/helpers/application_helper.rb" (6 Apr 2020, 61865 Bytes) of package /linux/www/redmine-4.1.1.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Ruby source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. See also the latest Fossies "Diffs" side-by-side code changes report for "application_helper.rb": 4.1.0_vs_4.1.1.

    1 # frozen_string_literal: true
    2 
    3 # Redmine - project management software
    4 # Copyright (C) 2006-2019  Jean-Philippe Lang
    5 #
    6 # This program is free software; you can redistribute it and/or
    7 # modify it under the terms of the GNU General Public License
    8 # as published by the Free Software Foundation; either version 2
    9 # of the License, or (at your option) any later version.
   10 #
   11 # This program is distributed in the hope that it will be useful,
   12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
   13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   14 # GNU General Public License for more details.
   15 #
   16 # You should have received a copy of the GNU General Public License
   17 # along with this program; if not, write to the Free Software
   18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
   19 
   20 require 'forwardable'
   21 require 'cgi'
   22 
   23 module ApplicationHelper
   24   include Redmine::WikiFormatting::Macros::Definitions
   25   include Redmine::I18n
   26   include Redmine::Pagination::Helper
   27   include Redmine::SudoMode::Helper
   28   include Redmine::Themes::Helper
   29   include Redmine::Hook::Helper
   30   include Redmine::Helpers::URL
   31 
   32   extend Forwardable
   33   def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
   34 
   35   # Return true if user is authorized for controller/action, otherwise false
   36   def authorize_for(controller, action)
   37     User.current.allowed_to?({:controller => controller, :action => action}, @project)
   38   end
   39 
   40   # Display a link if user is authorized
   41   #
   42   # @param [String] name Anchor text (passed to link_to)
   43   # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
   44   # @param [optional, Hash] html_options Options passed to link_to
   45   # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
   46   def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
   47     link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
   48   end
   49 
   50   # Displays a link to user's account page if active
   51   def link_to_user(user, options={})
   52     if user.is_a?(User)
   53       name = h(user.name(options[:format]))
   54       if user.active? || (User.current.admin? && user.logged?)
   55         only_path = options[:only_path].nil? ? true : options[:only_path]
   56         link_to name, user_url(user, :only_path => only_path), :class => user.css_classes
   57       else
   58         name
   59       end
   60     else
   61       h(user.to_s)
   62     end
   63   end
   64 
   65   # Displays a link to edit group page if current user is admin
   66   # Otherwise display only the group name
   67   def link_to_group(group, options={})
   68     if group.is_a?(Group)
   69       name = h(group.name)
   70       if User.current.admin?
   71         only_path = options[:only_path].nil? ? true : options[:only_path]
   72         link_to name, edit_group_path(group, :only_path => only_path)
   73       else
   74         name
   75       end
   76     end
   77   end
   78 
   79   # Displays a link to +issue+ with its subject.
   80   # Examples:
   81   #
   82   #   link_to_issue(issue)                        # => Defect #6: This is the subject
   83   #   link_to_issue(issue, :truncate => 6)        # => Defect #6: This i...
   84   #   link_to_issue(issue, :subject => false)     # => Defect #6
   85   #   link_to_issue(issue, :project => true)      # => Foo - Defect #6
   86   #   link_to_issue(issue, :subject => false, :tracker => false)     # => #6
   87   #
   88   def link_to_issue(issue, options={})
   89     title = nil
   90     subject = nil
   91     text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
   92     if options[:subject] == false
   93       title = issue.subject.truncate(60)
   94     else
   95       subject = issue.subject
   96       if truncate_length = options[:truncate]
   97         subject = subject.truncate(truncate_length)
   98       end
   99     end
  100     only_path = options[:only_path].nil? ? true : options[:only_path]
  101     s = link_to(text, issue_url(issue, :only_path => only_path),
  102                 :class => issue.css_classes, :title => title)
  103     s << h(": #{subject}") if subject
  104     s = h("#{issue.project} - ") + s if options[:project]
  105     s
  106   end
  107 
  108   # Generates a link to an attachment.
  109   # Options:
  110   # * :text - Link text (default to attachment filename)
  111   # * :download - Force download (default: false)
  112   def link_to_attachment(attachment, options={})
  113     text = options.delete(:text) || attachment.filename
  114     if options.delete(:download)
  115       route_method = :download_named_attachment_url
  116       options[:filename] = attachment.filename
  117     else
  118       route_method = :attachment_url
  119       # make sure we don't have an extraneous :filename in the options
  120       options.delete(:filename)
  121     end
  122     html_options = options.slice!(:only_path, :filename)
  123     options[:only_path] = true unless options.key?(:only_path)
  124     url = send(route_method, attachment, options)
  125     link_to text, url, html_options
  126   end
  127 
  128   # Generates a link to a SCM revision
  129   # Options:
  130   # * :text - Link text (default to the formatted revision)
  131   def link_to_revision(revision, repository, options={})
  132     if repository.is_a?(Project)
  133       repository = repository.repository
  134     end
  135     text = options.delete(:text) || format_revision(revision)
  136     rev = revision.respond_to?(:identifier) ? revision.identifier : revision
  137     link_to(
  138         h(text),
  139         {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
  140         :title => l(:label_revision_id, format_revision(revision)),
  141         :accesskey => options[:accesskey]
  142       )
  143   end
  144 
  145   # Generates a link to a message
  146   def link_to_message(message, options={}, html_options = nil)
  147     link_to(
  148       message.subject.truncate(60),
  149       board_message_url(message.board_id, message.parent_id || message.id, {
  150         :r => (message.parent_id && message.id),
  151         :anchor => (message.parent_id ? "message-#{message.id}" : nil),
  152         :only_path => true
  153       }.merge(options)),
  154       html_options
  155     )
  156   end
  157 
  158   # Generates a link to a project if active
  159   # Examples:
  160   #
  161   #   link_to_project(project)                          # => link to the specified project overview
  162   #   link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
  163   #   link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
  164   #
  165   def link_to_project(project, options={}, html_options = nil)
  166     if project.archived?
  167       h(project.name)
  168     else
  169       link_to project.name,
  170               project_url(project, {:only_path => true}.merge(options)),
  171               html_options
  172     end
  173   end
  174 
  175   # Generates a link to a project settings if active
  176   def link_to_project_settings(project, options={}, html_options=nil)
  177     if project.active?
  178       link_to project.name, settings_project_path(project, options), html_options
  179     elsif project.archived?
  180       h(project.name)
  181     else
  182       link_to project.name, project_path(project, options), html_options
  183     end
  184   end
  185 
  186   # Generates a link to a version
  187   def link_to_version(version, options = {})
  188     return '' unless version && version.is_a?(Version)
  189     options = {:title => format_date(version.effective_date)}.merge(options)
  190     link_to_if version.visible?, format_version_name(version), version_path(version), options
  191   end
  192 
  193   RECORD_LINK = {
  194     'CustomValue'  => -> (custom_value) { link_to_record(custom_value.customized) },
  195     'Document'     => -> (document)     { link_to(document.title, document_path(document)) },
  196     'Group'        => -> (group)        { link_to(group.name, group_path(group)) },
  197     'Issue'        => -> (issue)        { link_to_issue(issue, :subject => false) },
  198     'Message'      => -> (message)      { link_to_message(message) },
  199     'News'         => -> (news)         { link_to(news.title, news_path(news)) },
  200     'Project'      => -> (project)      { link_to_project(project) },
  201     'User'         => -> (user)         { link_to_user(user) },
  202     'Version'      => -> (version)      { link_to_version(version) },
  203     'WikiPage'     => -> (wiki_page)    { link_to(wiki_page.pretty_title, project_wiki_page_path(wiki_page.project, wiki_page.title)) }
  204   }
  205 
  206   def link_to_record(record)
  207     if link = RECORD_LINK[record.class.name]
  208       self.instance_exec(record, &link)
  209     end
  210   end
  211 
  212   ATTACHMENT_CONTAINER_LINK = {
  213     # Custom list, since project/version attachments are listed in the files
  214     # view and not in the project/milestone view
  215     'Project'      => -> (project)      { link_to(l(:project_module_files), project_files_path(project)) },
  216     'Version'      => -> (version)      { link_to(l(:project_module_files), project_files_path(version.project)) },
  217   }
  218 
  219   def link_to_attachment_container(attachment_container)
  220     if link = ATTACHMENT_CONTAINER_LINK[attachment_container.class.name] ||
  221               RECORD_LINK[attachment_container.class.name]
  222       self.instance_exec(attachment_container, &link)
  223     end
  224   end
  225 
  226   # Helper that formats object for html or text rendering
  227   def format_object(object, html=true, &block)
  228     if block_given?
  229       object = yield object
  230     end
  231     case object.class.name
  232     when 'Array'
  233       formatted_objects = object.map {|o| format_object(o, html)}
  234       html ? safe_join(formatted_objects, ', ') : formatted_objects.join(', ')
  235     when 'Time'
  236       format_time(object)
  237     when 'Date'
  238       format_date(object)
  239     when 'Fixnum'
  240       object.to_s
  241     when 'Float'
  242       sprintf "%.2f", object
  243     when 'User'
  244       html ? link_to_user(object) : object.to_s
  245     when 'Project'
  246       html ? link_to_project(object) : object.to_s
  247     when 'Version'
  248       html ? link_to_version(object) : object.to_s
  249     when 'TrueClass'
  250       l(:general_text_Yes)
  251     when 'FalseClass'
  252       l(:general_text_No)
  253     when 'Issue'
  254       object.visible? && html ? link_to_issue(object) : "##{object.id}"
  255     when 'Attachment'
  256       html ? link_to_attachment(object) : object.filename
  257     when 'CustomValue', 'CustomFieldValue'
  258       if object.custom_field
  259         f = object.custom_field.format.formatted_custom_value(self, object, html)
  260         if f.nil? || f.is_a?(String)
  261           f
  262         else
  263           format_object(f, html, &block)
  264         end
  265       else
  266         object.value.to_s
  267       end
  268     else
  269       html ? h(object) : object.to_s
  270     end
  271   end
  272 
  273   def wiki_page_path(page, options={})
  274     url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
  275   end
  276 
  277   def thumbnail_tag(attachment)
  278     thumbnail_size = Setting.thumbnails_size.to_i
  279     link_to(
  280       image_tag(
  281         thumbnail_path(attachment),
  282         :srcset => "#{thumbnail_path(attachment, :size => thumbnail_size * 2)} 2x",
  283         :style => "max-width: #{thumbnail_size}px; max-height: #{thumbnail_size}px;"
  284       ),
  285       attachment_path(
  286         attachment
  287       ),
  288       :title => attachment.filename
  289     )
  290   end
  291 
  292   def toggle_link(name, id, options={})
  293     onclick = +"$('##{id}').toggle(); "
  294     onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
  295     onclick << "$(window).scrollTop($('##{options[:focus]}').position().top); " if options[:scroll]
  296     onclick << "return false;"
  297     link_to(name, "#", :onclick => onclick)
  298   end
  299 
  300   def link_to_previous_month(year, month, options={})
  301     target_year, target_month = if month == 1
  302                                   [year - 1, 12]
  303                                 else
  304                                   [year, month - 1]
  305                                 end
  306 
  307     name = if target_month == 12
  308              "#{month_name(target_month)} #{target_year}"
  309            else
  310              month_name(target_month)
  311            end
  312 
  313     link_to_month(("« " + name), target_year, target_month, options)
  314   end
  315 
  316   def link_to_next_month(year, month, options={})
  317     target_year, target_month = if month == 12
  318                                   [year + 1, 1]
  319                                 else
  320                                   [year, month + 1]
  321                                 end
  322 
  323     name = if target_month == 1
  324              "#{month_name(target_month)} #{target_year}"
  325            else
  326              month_name(target_month)
  327            end
  328 
  329     link_to_month((name + " »"), target_year, target_month, options)
  330   end
  331 
  332   def link_to_month(link_name, year, month, options={})
  333     link_to(link_name, {:params => request.query_parameters.merge(:year => year, :month => month)}, options)
  334   end
  335 
  336   # Used to format item titles on the activity view
  337   def format_activity_title(text)
  338     text
  339   end
  340 
  341   def format_activity_day(date)
  342     date == User.current.today ? l(:label_today).titleize : format_date(date)
  343   end
  344 
  345   def format_activity_description(text)
  346     h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
  347        ).gsub(/[\r\n]+/, "<br />").html_safe
  348   end
  349 
  350   def format_version_name(version)
  351     if version.project == @project
  352       h(version)
  353     else
  354       h("#{version.project} - #{version}")
  355     end
  356   end
  357 
  358   def format_changeset_comments(changeset, options={})
  359     method = options[:short] ? :short_comments : :comments
  360     textilizable changeset, method, :formatting => Setting.commit_logs_formatting?
  361   end
  362 
  363   def due_date_distance_in_words(date)
  364     if date
  365       l((date < User.current.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(User.current.today, date))
  366     end
  367   end
  368 
  369   # Renders a tree of projects as a nested set of unordered lists
  370   # The given collection may be a subset of the whole project tree
  371   # (eg. some intermediate nodes are private and can not be seen)
  372   def render_project_nested_lists(projects, &block)
  373     s = +''
  374     if projects.any?
  375       ancestors = []
  376       original_project = @project
  377       projects.sort_by(&:lft).each do |project|
  378         # set the project environment to please macros.
  379         @project = project
  380         if ancestors.empty? || project.is_descendant_of?(ancestors.last)
  381           s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
  382         else
  383           ancestors.pop
  384           s << "</li>"
  385           while ancestors.any? && !project.is_descendant_of?(ancestors.last)
  386             ancestors.pop
  387             s << "</ul></li>\n"
  388           end
  389         end
  390         classes = (ancestors.empty? ? 'root' : 'child')
  391         s << "<li class='#{classes}'><div class='#{classes}'>"
  392         s << h(block_given? ? capture(project, &block) : project.name)
  393         s << "</div>\n"
  394         ancestors << project
  395       end
  396       s << ("</li></ul>\n" * ancestors.size)
  397       @project = original_project
  398     end
  399     s.html_safe
  400   end
  401 
  402   def render_page_hierarchy(pages, node=nil, options={})
  403     content = +''
  404     if pages[node]
  405       content << "<ul class=\"pages-hierarchy\">\n"
  406       pages[node].each do |page|
  407         content << "<li>"
  408         if controller.controller_name == 'wiki' && controller.action_name == 'export'
  409           href = "##{page.title}"
  410         else
  411           href = {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil}
  412         end
  413         content << link_to(h(page.pretty_title), href,
  414                            :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
  415         content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
  416         content << "</li>\n"
  417       end
  418       content << "</ul>\n"
  419     end
  420     content.html_safe
  421   end
  422 
  423   # Renders flash messages
  424   def render_flash_messages
  425     s = +''
  426     flash.each do |k,v|
  427       s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
  428     end
  429     s.html_safe
  430   end
  431 
  432   # Renders tabs and their content
  433   def render_tabs(tabs, selected=params[:tab])
  434     if tabs.any?
  435       unless tabs.detect {|tab| tab[:name] == selected}
  436         selected = nil
  437       end
  438       selected ||= tabs.first[:name]
  439       render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected}
  440     else
  441       content_tag 'p', l(:label_no_data), :class => "nodata"
  442     end
  443   end
  444 
  445   # Returns the tab action depending on the tab properties
  446   def get_tab_action(tab)
  447     if tab[:onclick]
  448       return tab[:onclick]
  449     elsif tab[:partial]
  450       return "showTab('#{tab[:name]}', this.href)"
  451     else
  452       return nil
  453     end
  454   end
  455 
  456   # Returns the default scope for the quick search form
  457   # Could be 'all', 'my_projects', 'subprojects' or nil (current project)
  458   def default_search_project_scope
  459     if @project && !@project.leaf?
  460       'subprojects'
  461     end
  462   end
  463 
  464   # Returns an array of projects that are displayed in the quick-jump box
  465   def projects_for_jump_box(user=User.current)
  466     if user.logged?
  467       user.projects.active.select(:id, :name, :identifier, :lft, :rgt).to_a
  468     else
  469       []
  470     end
  471   end
  472 
  473   def render_projects_for_jump_box(projects, selected=nil)
  474     jump_box = Redmine::ProjectJumpBox.new User.current
  475     query = params[:q] if request.format.js?
  476     bookmarked = jump_box.bookmarked_projects(query)
  477     recents = jump_box.recently_used_projects(query)
  478     projects = projects - (recents + bookmarked)
  479     projects_label = (bookmarked.any? || recents.any?) ? :label_optgroup_others : :label_project_plural
  480     jump = params[:jump].presence || current_menu_item
  481     s = (+'').html_safe
  482     build_project_link = ->(project, level = 0){
  483       padding = level * 16
  484       text = content_tag('span', project.name, :style => "padding-left:#{padding}px;")
  485       s << link_to(text, project_path(project, :jump => jump),
  486                    :title => project.name,
  487                    :class => (project == selected ? 'selected' : nil))
  488     }
  489     [
  490       [bookmarked, :label_optgroup_bookmarks, true],
  491       [recents,   :label_optgroup_recents,    false],
  492       [projects,  projects_label,             true]
  493     ].each do |projects, label, is_tree|
  494       next if projects.blank?
  495       s << content_tag(:strong, l(label))
  496       if is_tree
  497         project_tree(projects, &build_project_link)
  498       else
  499         # we do not want to render recently used projects as a tree, but in the
  500         # order they were used (most recent first)
  501         projects.each(&build_project_link)
  502       end
  503     end
  504     s
  505   end
  506 
  507   # Renders the project quick-jump box
  508   def render_project_jump_box
  509     projects = projects_for_jump_box(User.current)
  510     if @project && @project.persisted?
  511       text = @project.name_was
  512     end
  513     text ||= l(:label_jump_to_a_project)
  514     url = autocomplete_projects_path(:format => 'js', :jump => current_menu_item)
  515     trigger = content_tag('span', text, :class => 'drdn-trigger')
  516     q = text_field_tag('q', '', :id => 'projects-quick-search',
  517                        :class => 'autocomplete',
  518                        :data => {:automcomplete_url => url},
  519                        :autocomplete => 'off')
  520     all = link_to(l(:label_project_all), projects_path(:jump => current_menu_item),
  521                   :class => (@project.nil? && controller.class.main_menu ? 'selected' : nil))
  522     content =
  523       content_tag('div',
  524                   content_tag('div', q, :class => 'quick-search') +
  525                     content_tag('div', render_projects_for_jump_box(projects, @project),
  526                                 :class => 'drdn-items projects selection') +
  527                     content_tag('div', all, :class => 'drdn-items all-projects selection'),
  528                   :class => 'drdn-content'
  529       )
  530     content_tag('div', trigger + content, :id => "project-jump", :class => "drdn")
  531   end
  532 
  533   def project_tree_options_for_select(projects, options = {})
  534     s = ''.html_safe
  535     if blank_text = options[:include_blank]
  536       if blank_text == true
  537         blank_text = '&nbsp;'.html_safe
  538       end
  539       s << content_tag('option', blank_text, :value => '')
  540     end
  541     project_tree(projects) do |project, level|
  542       name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
  543       tag_options = {:value => project.id}
  544       if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
  545         tag_options[:selected] = 'selected'
  546       else
  547         tag_options[:selected] = nil
  548       end
  549       tag_options.merge!(yield(project)) if block_given?
  550       s << content_tag('option', name_prefix + h(project), tag_options)
  551     end
  552     s.html_safe
  553   end
  554 
  555   # Yields the given block for each project with its level in the tree
  556   #
  557   # Wrapper for Project#project_tree
  558   def project_tree(projects, options={}, &block)
  559     Project.project_tree(projects, options, &block)
  560   end
  561 
  562   def principals_check_box_tags(name, principals)
  563     s = +''
  564     principals.each do |principal|
  565       s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } <span class='name icon icon-#{principal.class.name.downcase}'></span>#{h principal}</label>\n"
  566     end
  567     s.html_safe
  568   end
  569 
  570   # Returns a string for users/groups option tags
  571   def principals_options_for_select(collection, selected=nil)
  572     s = +''
  573     if collection.include?(User.current)
  574       s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
  575     end
  576     groups = +''
  577     collection.sort.each do |element|
  578       selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
  579       (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
  580     end
  581     unless groups.empty?
  582       s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
  583     end
  584     s.html_safe
  585   end
  586 
  587   def option_tag(name, text, value, selected=nil, options={})
  588     content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
  589   end
  590 
  591   def truncate_single_line_raw(string, length)
  592     string.to_s.truncate(length).gsub(%r{[\r\n]+}m, ' ')
  593   end
  594 
  595   # Truncates at line break after 250 characters or options[:length]
  596   def truncate_lines(string, options={})
  597     length = options[:length] || 250
  598     if string.to_s =~ /\A(.{#{length}}.*?)$/m
  599       "#{$1}..."
  600     else
  601       string
  602     end
  603   end
  604 
  605   def anchor(text)
  606     text.to_s.tr(' ', '_')
  607   end
  608 
  609   def html_hours(text)
  610     text.gsub(%r{(\d+)([\.:])(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">\2\3</span>').html_safe
  611   end
  612 
  613   def authoring(created, author, options={})
  614     l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
  615   end
  616 
  617   def time_tag(time)
  618     text = distance_of_time_in_words(Time.now, time)
  619     if @project
  620       link_to(text, project_activity_path(@project, :from => User.current.time_to_date(time)), :title => format_time(time))
  621     else
  622       content_tag('abbr', text, :title => format_time(time))
  623     end
  624   end
  625 
  626   def syntax_highlight_lines(name, content)
  627     syntax_highlight(name, content).each_line.to_a
  628   end
  629 
  630   def syntax_highlight(name, content)
  631     Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
  632   end
  633 
  634   def to_path_param(path)
  635     str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
  636     str.blank? ? nil : str
  637   end
  638 
  639   def reorder_handle(object, options={})
  640     data = {
  641       :reorder_url => options[:url] || url_for(object),
  642       :reorder_param => options[:param] || object.class.name.underscore
  643     }
  644     content_tag('span', '',
  645                 :class => "icon-only icon-sort-handle sort-handle",
  646                 :data => data,
  647                 :title => l(:button_sort))
  648   end
  649 
  650   def breadcrumb(*args)
  651     elements = args.flatten
  652     elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
  653   end
  654 
  655   def other_formats_links(&block)
  656     concat('<p class="other-formats">'.html_safe + l(:label_export_to))
  657     yield Redmine::Views::OtherFormatsBuilder.new(self)
  658     concat('</p>'.html_safe)
  659   end
  660 
  661   def page_header_title
  662     if @project.nil? || @project.new_record?
  663       h(Setting.app_title)
  664     else
  665       b = []
  666       ancestors = (@project.root? ? [] : @project.ancestors.visible.to_a)
  667       if ancestors.any?
  668         root = ancestors.shift
  669         b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
  670         if ancestors.size > 2
  671           b << "\xe2\x80\xa6"
  672           ancestors = ancestors[-2, 2]
  673         end
  674         b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
  675       end
  676       b << content_tag(:span, h(@project), class: 'current-project')
  677       if b.size > 1
  678         separator = content_tag(:span, ' &raquo; '.html_safe, class: 'separator')
  679         path = safe_join(b[0..-2], separator) + separator
  680         b = [content_tag(:span, path.html_safe, class: 'breadcrumbs'), b[-1]]
  681       end
  682       safe_join b
  683     end
  684   end
  685 
  686   # Returns a h2 tag and sets the html title with the given arguments
  687   def title(*args)
  688     strings = args.map do |arg|
  689       if arg.is_a?(Array) && arg.size >= 2
  690         link_to(*arg)
  691       else
  692         h(arg.to_s)
  693       end
  694     end
  695     html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
  696     content_tag('h2', strings.join(' &#187; ').html_safe)
  697   end
  698 
  699   # Sets the html title
  700   # Returns the html title when called without arguments
  701   # Current project name and app_title and automatically appended
  702   # Exemples:
  703   #   html_title 'Foo', 'Bar'
  704   #   html_title # => 'Foo - Bar - My Project - Redmine'
  705   def html_title(*args)
  706     if args.empty?
  707       title = @html_title || []
  708       title << @project.name if @project
  709       title << Setting.app_title unless Setting.app_title == title.last
  710       title.reject(&:blank?).join(' - ')
  711     else
  712       @html_title ||= []
  713       @html_title += args
  714     end
  715   end
  716 
  717   def actions_dropdown(&block)
  718     content = capture(&block)
  719     if content.present?
  720       trigger = content_tag('span', l(:button_actions), :class => 'icon-only icon-actions', :title => l(:button_actions))
  721       trigger = content_tag('span', trigger, :class => 'drdn-trigger')
  722       content = content_tag('div', content, :class => 'drdn-items')
  723       content = content_tag('div', content, :class => 'drdn-content')
  724       content_tag('span', trigger + content, :class => 'drdn')
  725     end
  726   end
  727 
  728   # Returns the theme, controller name, and action as css classes for the
  729   # HTML body.
  730   def body_css_classes
  731     css = []
  732     if theme = Redmine::Themes.theme(Setting.ui_theme)
  733       css << 'theme-' + theme.name
  734     end
  735 
  736     css << 'project-' + @project.identifier if @project && @project.identifier.present?
  737     css << 'has-main-menu' if display_main_menu?(@project)
  738     css << 'controller-' + controller_name
  739     css << 'action-' + action_name
  740     css << 'avatars-' + (Setting.gravatar_enabled? ? 'on' : 'off')
  741     if UserPreference::TEXTAREA_FONT_OPTIONS.include?(User.current.pref.textarea_font)
  742       css << "textarea-#{User.current.pref.textarea_font}"
  743     end
  744     css.join(' ')
  745   end
  746 
  747   def accesskey(s)
  748     @used_accesskeys ||= []
  749     key = Redmine::AccessKeys.key_for(s)
  750     return nil if @used_accesskeys.include?(key)
  751     @used_accesskeys << key
  752     key
  753   end
  754 
  755   # Formats text according to system settings.
  756   # 2 ways to call this method:
  757   # * with a String: textilizable(text, options)
  758   # * with an object and one of its attribute: textilizable(issue, :description, options)
  759   def textilizable(*args)
  760     options = args.last.is_a?(Hash) ? args.pop : {}
  761     case args.size
  762     when 1
  763       obj = options[:object]
  764       text = args.shift
  765     when 2
  766       obj = args.shift
  767       attr = args.shift
  768       text = obj.send(attr).to_s
  769     else
  770       raise ArgumentError, 'invalid arguments to textilizable'
  771     end
  772     return '' if text.blank?
  773     project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
  774     @only_path = only_path = options.delete(:only_path) == false ? false : true
  775 
  776     text = text.dup
  777     macros = catch_macros(text)
  778 
  779     if options[:formatting] == false
  780       text = h(text)
  781     else
  782       formatting = Setting.text_formatting
  783       text = Redmine::WikiFormatting.to_html(formatting, text, :object => obj, :attribute => attr)
  784     end
  785 
  786     @parsed_headings = []
  787     @heading_anchors = {}
  788     @current_section = 0 if options[:edit_section_links]
  789 
  790     parse_sections(text, project, obj, attr, only_path, options)
  791     text = parse_non_pre_blocks(text, obj, macros, options) do |text|
  792       [:parse_inline_attachments, :parse_hires_images, :parse_wiki_links, :parse_redmine_links].each do |method_name|
  793         send method_name, text, project, obj, attr, only_path, options
  794       end
  795     end
  796     parse_headings(text, project, obj, attr, only_path, options)
  797 
  798     if @parsed_headings.any?
  799       replace_toc(text, @parsed_headings)
  800     end
  801 
  802     text.html_safe
  803   end
  804 
  805   def parse_non_pre_blocks(text, obj, macros, options={})
  806     s = StringScanner.new(text)
  807     tags = []
  808     parsed = +''
  809     while !s.eos?
  810       s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
  811       text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
  812       if tags.empty?
  813         yield text
  814         inject_macros(text, obj, macros, true, options) if macros.any?
  815       else
  816         inject_macros(text, obj, macros, false, options) if macros.any?
  817       end
  818       parsed << text
  819       if tag
  820         if closing
  821           if tags.last && tags.last.casecmp(tag) == 0
  822             tags.pop
  823           end
  824         else
  825           tags << tag.downcase
  826         end
  827         parsed << full_tag
  828       end
  829     end
  830     # Close any non closing tags
  831     while tag = tags.pop
  832       parsed << "</#{tag}>"
  833     end
  834     parsed
  835   end
  836 
  837   # add srcset attribute to img tags if filename includes @2x, @3x, etc.
  838   # to support hires displays
  839   def parse_hires_images(text, project, obj, attr, only_path, options)
  840     text.gsub!(/src="([^"]+@(\dx)\.(bmp|gif|jpg|jpe|jpeg|png))"/i) do |m|
  841       filename, dpr = $1, $2
  842       m + " srcset=\"#{filename} #{dpr}\""
  843     end
  844   end
  845 
  846   def parse_inline_attachments(text, project, obj, attr, only_path, options)
  847     return if options[:inline_attachments] == false
  848 
  849     # when using an image link, try to use an attachment, if possible
  850     attachments = options[:attachments] || []
  851     attachments += obj.attachments if obj.respond_to?(:attachments)
  852     if attachments.present?
  853       text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
  854         filename, ext, alt, alttext = $1, $2, $3, $4
  855         # search for the picture in attachments
  856         if found = Attachment.latest_attach(attachments, CGI.unescape(filename))
  857           image_url = download_named_attachment_url(found, found.filename, :only_path => only_path)
  858           desc = found.description.to_s.gsub('"', '')
  859           if !desc.blank? && alttext.blank?
  860             alt = " title=\"#{desc}\" alt=\"#{desc}\""
  861           end
  862           "src=\"#{image_url}\"#{alt}"
  863         else
  864           m
  865         end
  866       end
  867     end
  868   end
  869 
  870   # Wiki links
  871   #
  872   # Examples:
  873   #   [[mypage]]
  874   #   [[mypage|mytext]]
  875   # wiki links can refer other project wikis, using project name or identifier:
  876   #   [[project:]] -> wiki starting page
  877   #   [[project:|mytext]]
  878   #   [[project:mypage]]
  879   #   [[project:mypage|mytext]]
  880   def parse_wiki_links(text, project, obj, attr, only_path, options)
  881     text.gsub!(/(!)?(\[\[([^\n\|]+?)(\|([^\n\|]+?))?\]\])/) do |m|
  882       link_project = project
  883       esc, all, page, title = $1, $2, $3, $5
  884       if esc.nil?
  885         page = CGI.unescapeHTML(page)
  886         if page =~ /^\#(.+)$/
  887           anchor = sanitize_anchor_name($1)
  888           url = "##{anchor}"
  889           next link_to(title.present? ? title.html_safe : h(page), url, :class => 'wiki-page')
  890         end
  891 
  892         if page =~ /^([^\:]+)\:(.*)$/
  893           identifier, page = $1, $2
  894           link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
  895           title ||= identifier if page.blank?
  896         end
  897 
  898         if link_project && link_project.wiki && User.current.allowed_to?(:view_wiki_pages, link_project)
  899           # extract anchor
  900           anchor = nil
  901           if page =~ /^(.+?)\#(.+)$/
  902             page, anchor = $1, $2
  903           end
  904           anchor = sanitize_anchor_name(anchor) if anchor.present?
  905           # check if page exists
  906           wiki_page = link_project.wiki.find_page(page)
  907           url =
  908             if anchor.present? && wiki_page.present? &&
  909                  (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) &&
  910                  obj.page == wiki_page
  911               "##{anchor}"
  912             else
  913               case options[:wiki_links]
  914               when :local
  915                 "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
  916               when :anchor
  917                 # used for single-file wiki export
  918                 "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '')
  919               else
  920                 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
  921                 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
  922                 url_for(:only_path => only_path, :controller => 'wiki',
  923                         :action => 'show', :project_id => link_project,
  924                         :id => wiki_page_id, :version => nil, :anchor => anchor,
  925                         :parent => parent)
  926               end
  927             end
  928           link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
  929         else
  930           # project or wiki doesn't exist
  931           all
  932         end
  933       else
  934         all
  935       end
  936     end
  937   end
  938 
  939   # Redmine links
  940   #
  941   # Examples:
  942   #   Issues:
  943   #     #52 -> Link to issue #52
  944   #     ##52 -> Link to issue #52, including the issue's subject
  945   #   Changesets:
  946   #     r52 -> Link to revision 52
  947   #     commit:a85130f -> Link to scmid starting with a85130f
  948   #   Documents:
  949   #     document#17 -> Link to document with id 17
  950   #     document:Greetings -> Link to the document with title "Greetings"
  951   #     document:"Some document" -> Link to the document with title "Some document"
  952   #   Versions:
  953   #     version#3 -> Link to version with id 3
  954   #     version:1.0.0 -> Link to version named "1.0.0"
  955   #     version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
  956   #   Attachments:
  957   #     attachment:file.zip -> Link to the attachment of the current object named file.zip
  958   #   Source files:
  959   #     source:some/file -> Link to the file located at /some/file in the project's repository
  960   #     source:some/file@52 -> Link to the file's revision 52
  961   #     source:some/file#L120 -> Link to line 120 of the file
  962   #     source:some/file@52#L120 -> Link to line 120 of the file's revision 52
  963   #     export:some/file -> Force the download of the file
  964   #   Forums:
  965   #     forum#1 -> Link to forum with id 1
  966   #     forum:Support -> Link to forum named "Support"
  967   #     forum:"Technical Support" -> Link to forum named "Technical Support"
  968   #   Forum messages:
  969   #     message#1218 -> Link to message with id 1218
  970   #   Projects:
  971   #     project:someproject -> Link to project named "someproject"
  972   #     project#3 -> Link to project with id 3
  973   #   News:
  974   #     news#2 -> Link to news item with id 1
  975   #     news:Greetings -> Link to news item named "Greetings"
  976   #     news:"First Release" -> Link to news item named "First Release"
  977   #   Users:
  978   #     user:jsmith -> Link to user with login jsmith
  979   #     @jsmith -> Link to user with login jsmith
  980   #     user#2 -> Link to user with id 2
  981   #
  982   #   Links can refer other objects from other projects, using project identifier:
  983   #     identifier:r52
  984   #     identifier:document:"Some document"
  985   #     identifier:version:1.0.0
  986   #     identifier:source:some/file
  987   def parse_redmine_links(text, default_project, obj, attr, only_path, options)
  988     text.gsub!(LINKS_RE) do |_|
  989       tag_content = $~[:tag_content]
  990       leading = $~[:leading]
  991       esc = $~[:esc]
  992       project_prefix = $~[:project_prefix]
  993       project_identifier = $~[:project_identifier]
  994       prefix = $~[:prefix]
  995       repo_prefix = $~[:repo_prefix]
  996       repo_identifier = $~[:repo_identifier]
  997       sep = $~[:sep1] || $~[:sep2] || $~[:sep3] || $~[:sep4]
  998       identifier = $~[:identifier1] || $~[:identifier2] || $~[:identifier3]
  999       comment_suffix = $~[:comment_suffix]
 1000       comment_id = $~[:comment_id]
 1001 
 1002       if tag_content
 1003         $&
 1004       else
 1005         link = nil
 1006         project = default_project
 1007         if project_identifier
 1008           project = Project.visible.find_by_identifier(project_identifier)
 1009         end
 1010         if esc.nil?
 1011           if prefix.nil? && sep == 'r'
 1012             if project
 1013               repository = nil
 1014               if repo_identifier
 1015                 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
 1016               else
 1017                 repository = project.repository
 1018               end
 1019               # project.changesets.visible raises an SQL error because of a double join on repositories
 1020               if repository &&
 1021                    (changeset = Changeset.visible.
 1022                                     find_by_repository_id_and_revision(repository.id, identifier))
 1023                 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"),
 1024                                {:only_path => only_path, :controller => 'repositories',
 1025                                 :action => 'revision', :id => project,
 1026                                 :repository_id => repository.identifier_param,
 1027                                 :rev => changeset.revision},
 1028                                :class => 'changeset',
 1029                                :title => truncate_single_line_raw(changeset.comments, 100))
 1030               end
 1031             end
 1032           elsif sep == '#' || sep == '##'
 1033             oid = identifier.to_i
 1034             case prefix
 1035             when nil
 1036               if oid.to_s == identifier &&
 1037                 issue = Issue.visible.find_by_id(oid)
 1038                 anchor = comment_id ? "note-#{comment_id}" : nil
 1039                 url = issue_url(issue, :only_path => only_path, :anchor => anchor)
 1040                 link =
 1041                   if sep == '##'
 1042                     link_to("#{issue.tracker.name} ##{oid}#{comment_suffix}: #{issue.subject}",
 1043                             url,
 1044                             :class => issue.css_classes,
 1045                             :title => "#{l(:field_status)}: #{issue.status.name}")
 1046                   else
 1047                     link_to("##{oid}#{comment_suffix}",
 1048                             url,
 1049                             :class => issue.css_classes,
 1050                             :title => "#{issue.tracker.name}: #{issue.subject.truncate(100)} (#{issue.status.name})")
 1051                   end
 1052               elsif identifier == 'note'
 1053                 link = link_to("#note-#{comment_id}", "#note-#{comment_id}")
 1054               end
 1055             when 'document'
 1056               if document = Document.visible.find_by_id(oid)
 1057                 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
 1058               end
 1059             when 'version'
 1060               if version = Version.visible.find_by_id(oid)
 1061                 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
 1062               end
 1063             when 'message'
 1064               if message = Message.visible.find_by_id(oid)
 1065                 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
 1066               end
 1067             when 'forum'
 1068               if board = Board.visible.find_by_id(oid)
 1069                 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
 1070               end
 1071             when 'news'
 1072               if news = News.visible.find_by_id(oid)
 1073                 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
 1074               end
 1075             when 'project'
 1076               if p = Project.visible.find_by_id(oid)
 1077                 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
 1078               end
 1079             when 'user'
 1080               u = User.visible.find_by(:id => oid, :type => 'User')
 1081               link = link_to_user(u, :only_path => only_path) if u
 1082             end
 1083           elsif sep == ':'
 1084             name = remove_double_quotes(identifier)
 1085             case prefix
 1086             when 'document'
 1087               if project && document = project.documents.visible.find_by_title(name)
 1088                 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
 1089               end
 1090             when 'version'
 1091               if project && version = project.versions.visible.find_by_name(name)
 1092                 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
 1093               end
 1094             when 'forum'
 1095               if project && board = project.boards.visible.find_by_name(name)
 1096                 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
 1097               end
 1098             when 'news'
 1099               if project && news = project.news.visible.find_by_title(name)
 1100                 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
 1101               end
 1102             when 'commit', 'source', 'export'
 1103               if project
 1104                 repository = nil
 1105                 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
 1106                   repo_prefix, repo_identifier, name = $1, $2, $3
 1107                   repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
 1108                 else
 1109                   repository = project.repository
 1110                 end
 1111                 if prefix == 'commit'
 1112                   if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
 1113                     link = link_to(
 1114                              h("#{project_prefix}#{repo_prefix}#{name}"),
 1115                              {:only_path => only_path, :controller => 'repositories',
 1116                               :action => 'revision', :id => project,
 1117                               :repository_id => repository.identifier_param,
 1118                               :rev => changeset.identifier},
 1119                              :class => 'changeset',
 1120                              :title => truncate_single_line_raw(changeset.comments, 100))
 1121                   end
 1122                 else
 1123                   if repository && User.current.allowed_to?(:browse_repository, project)
 1124                     name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
 1125                     path, rev, anchor = $1, $3, $5
 1126                     link =
 1127                       link_to(
 1128                         h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"),
 1129                         {:only_path => only_path, :controller => 'repositories',
 1130                          :action => (prefix == 'export' ? 'raw' : 'entry'),
 1131                          :id => project, :repository_id => repository.identifier_param,
 1132                          :path => to_path_param(path),
 1133                          :rev => rev,
 1134                          :anchor => anchor},
 1135                         :class => (prefix == 'export' ? 'source download' : 'source'))
 1136                   end
 1137                 end
 1138                 repo_prefix = nil
 1139               end
 1140             when 'attachment'
 1141               attachments = options[:attachments] || []
 1142               attachments += obj.attachments if obj.respond_to?(:attachments)
 1143               if attachments && attachment = Attachment.latest_attach(attachments, name)
 1144                 link = link_to_attachment(attachment, :only_path => only_path, :class => 'attachment')
 1145               end
 1146             when 'project'
 1147               if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
 1148                 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
 1149               end
 1150             when 'user'
 1151               u = User.visible.find_by("LOWER(login) = :s AND type = 'User'", :s => name.downcase)
 1152               link = link_to_user(u, :only_path => only_path) if u
 1153             end
 1154           elsif sep == "@"
 1155             name = remove_double_quotes(identifier)
 1156             u = User.visible.find_by("LOWER(login) = :s AND type = 'User'", :s => name.downcase)
 1157             link = link_to_user(u, :only_path => only_path) if u
 1158           end
 1159         end
 1160         (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
 1161       end
 1162     end
 1163   end
 1164 
 1165   LINKS_RE =
 1166     %r{
 1167             <a( [^>]+?)?>(?<tag_content>.*?)</a>|
 1168             (?<leading>[\s\(,\-\[\>]|^)
 1169             (?<esc>!)?
 1170             (?<project_prefix>(?<project_identifier>[a-z0-9\-_]+):)?
 1171             (?<prefix>attachment|document|version|forum|news|message|project|commit|source|export|user)?
 1172             (
 1173               (
 1174                 (?<sep1>\#\#?)|
 1175                 (
 1176                   (?<repo_prefix>(?<repo_identifier>[a-z0-9\-_]+)\|)?
 1177                   (?<sep2>r)
 1178                 )
 1179               )
 1180               (
 1181                 (?<identifier1>((\d)+|(note)))
 1182                 (?<comment_suffix>
 1183                   (\#note)?
 1184                   -(?<comment_id>\d+)
 1185                 )?
 1186               )|
 1187               (
 1188               (?<sep3>:)
 1189               (?<identifier2>[^"\s<>][^\s<>]*?|"[^"]+?")
 1190               )|
 1191               (
 1192               (?<sep4>@)
 1193               (?<identifier3>[A-Za-z0-9_\-@\.]*)
 1194               )
 1195             )
 1196             (?=
 1197               (?=[[:punct:]][^A-Za-z0-9_/])|
 1198               ,|
 1199               \s|
 1200               \]|
 1201               <|
 1202               $)
 1203     }x
 1204   HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
 1205 
 1206   def parse_sections(text, project, obj, attr, only_path, options)
 1207     return unless options[:edit_section_links]
 1208     text.gsub!(HEADING_RE) do
 1209       heading, level = $1, $2
 1210       @current_section += 1
 1211       if @current_section > 1
 1212         content_tag(
 1213           'div',
 1214           link_to(
 1215             l(:button_edit_section),
 1216             options[:edit_section_links].merge(
 1217               :section => @current_section),
 1218             :class => 'icon-only icon-edit'),
 1219           :class => "contextual heading-#{level}",
 1220           :title => l(:button_edit_section),
 1221           :id => "section-#{@current_section}") + heading.html_safe
 1222       else
 1223         heading
 1224       end
 1225     end
 1226   end
 1227 
 1228   # Headings and TOC
 1229   # Adds ids and links to headings unless options[:headings] is set to false
 1230   def parse_headings(text, project, obj, attr, only_path, options)
 1231     return if options[:headings] == false
 1232 
 1233     text.gsub!(HEADING_RE) do
 1234       level, attrs, content = $2.to_i, $3, $4
 1235       item = strip_tags(content).strip
 1236       anchor = sanitize_anchor_name(item)
 1237       # used for single-file wiki export
 1238       anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
 1239       @heading_anchors[anchor] ||= 0
 1240       idx = (@heading_anchors[anchor] += 1)
 1241       if idx > 1
 1242         anchor = "#{anchor}-#{idx}"
 1243       end
 1244       @parsed_headings << [level, anchor, item]
 1245       "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
 1246     end
 1247   end
 1248 
 1249   MACROS_RE = /(
 1250                 (!)?                        # escaping
 1251                 (
 1252                 \{\{                        # opening tag
 1253                 ([\w]+)                     # macro name
 1254                 (\(([^\n\r]*?)\))?          # optional arguments
 1255                 ([\n\r].*?[\n\r])?          # optional block of text
 1256                 \}\}                        # closing tag
 1257                 )
 1258                )/mx unless const_defined?(:MACROS_RE)
 1259 
 1260   MACRO_SUB_RE = /(
 1261                   \{\{
 1262                   macro\((\d+)\)
 1263                   \}\}
 1264                   )/x unless const_defined?(:MACRO_SUB_RE)
 1265 
 1266   # Extracts macros from text
 1267   def catch_macros(text)
 1268     macros = {}
 1269     text.gsub!(MACROS_RE) do
 1270       all, macro = $1, $4.downcase
 1271       if macro_exists?(macro) || all =~ MACRO_SUB_RE
 1272         index = macros.size
 1273         macros[index] = all
 1274         "{{macro(#{index})}}"
 1275       else
 1276         all
 1277       end
 1278     end
 1279     macros
 1280   end
 1281 
 1282   # Executes and replaces macros in text
 1283   def inject_macros(text, obj, macros, execute=true, options={})
 1284     text.gsub!(MACRO_SUB_RE) do
 1285       all, index = $1, $2.to_i
 1286       orig = macros.delete(index)
 1287       if execute && orig && orig =~ MACROS_RE
 1288         esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
 1289         if esc.nil?
 1290           h(exec_macro(macro, obj, args, block, options) || all)
 1291         else
 1292           h(all)
 1293         end
 1294       elsif orig
 1295         h(orig)
 1296       else
 1297         h(all)
 1298       end
 1299     end
 1300   end
 1301 
 1302   TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
 1303 
 1304   # Renders the TOC with given headings
 1305   def replace_toc(text, headings)
 1306     text.gsub!(TOC_RE) do
 1307       left_align, right_align = $2, $3
 1308       # Keep only the 4 first levels
 1309       headings = headings.select{|level, anchor, item| level <= 4}
 1310       if headings.empty?
 1311         ''
 1312       else
 1313         div_class = +'toc'
 1314         div_class << ' right' if right_align
 1315         div_class << ' left' if left_align
 1316         out = +"<ul class=\"#{div_class}\"><li><strong>#{l :label_table_of_contents}</strong></li><li>"
 1317         root = headings.map(&:first).min
 1318         current = root
 1319         started = false
 1320         headings.each do |level, anchor, item|
 1321           if level > current
 1322             out << '<ul><li>' * (level - current)
 1323           elsif level < current
 1324             out << "</li></ul>\n" * (current - level) + "</li><li>"
 1325           elsif started
 1326             out << '</li><li>'
 1327           end
 1328           out << "<a href=\"##{anchor}\">#{item}</a>"
 1329           current = level
 1330           started = true
 1331         end
 1332         out << '</li></ul>' * (current - root)
 1333         out << '</li></ul>'
 1334       end
 1335     end
 1336   end
 1337 
 1338   # Same as Rails' simple_format helper without using paragraphs
 1339   def simple_format_without_paragraph(text)
 1340     text.to_s.
 1341       gsub(/\r\n?/, "\n").                    # \r\n and \r -> \n
 1342       gsub(/\n\n+/, "<br /><br />").          # 2+ newline  -> 2 br
 1343       gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline   -> br
 1344       html_safe
 1345   end
 1346 
 1347   def lang_options_for_select(blank=true)
 1348     (blank ? [["(auto)", ""]] : []) + languages_options
 1349   end
 1350 
 1351   def labelled_form_for(*args, &proc)
 1352     args << {} unless args.last.is_a?(Hash)
 1353     options = args.last
 1354     if args.first.is_a?(Symbol)
 1355       options.merge!(:as => args.shift)
 1356     end
 1357     options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
 1358     form_for(*args, &proc)
 1359   end
 1360 
 1361   def labelled_fields_for(*args, &proc)
 1362     args << {} unless args.last.is_a?(Hash)
 1363     options = args.last
 1364     options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
 1365     fields_for(*args, &proc)
 1366   end
 1367 
 1368   def form_tag_html(html_options)
 1369     # Set a randomized name attribute on all form fields by default
 1370     # as a workaround to https://bugzilla.mozilla.org/show_bug.cgi?id=1279253
 1371     html_options['name'] ||= "#{html_options['id'] || 'form'}-#{SecureRandom.hex(4)}"
 1372     super
 1373   end
 1374 
 1375   # Render the error messages for the given objects
 1376   def error_messages_for(*objects)
 1377     objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
 1378     errors = objects.map {|o| o.errors.full_messages}.flatten
 1379     render_error_messages(errors)
 1380   end
 1381 
 1382   # Renders a list of error messages
 1383   def render_error_messages(errors)
 1384     html = +""
 1385     if errors.present?
 1386       html << "<div id='errorExplanation'><ul>\n"
 1387       errors.each do |error|
 1388         html << "<li>#{h error}</li>\n"
 1389       end
 1390       html << "</ul></div>\n"
 1391     end
 1392     html.html_safe
 1393   end
 1394 
 1395   def delete_link(url, options={})
 1396     options = {
 1397       :method => :delete,
 1398       :data => {:confirm => l(:text_are_you_sure)},
 1399       :class => 'icon icon-del'
 1400     }.merge(options)
 1401 
 1402     link_to l(:button_delete), url, options
 1403   end
 1404 
 1405   def link_to_function(name, function, html_options={})
 1406     content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
 1407   end
 1408 
 1409   def link_to_context_menu
 1410     link_to l(:button_actions), '#', title: l(:button_actions), class: 'icon-only icon-actions js-contextmenu'
 1411   end
 1412 
 1413   # Helper to render JSON in views
 1414   def raw_json(arg)
 1415     arg.to_json.to_s.gsub('/', '\/').html_safe
 1416   end
 1417 
 1418   def back_url_hidden_field_tag
 1419     url = validate_back_url(back_url)
 1420     hidden_field_tag('back_url', url, :id => nil) unless url.blank?
 1421   end
 1422 
 1423   def cancel_button_tag(fallback_url)
 1424     url = validate_back_url(back_url) || fallback_url
 1425     link_to l(:button_cancel), url
 1426   end
 1427 
 1428   def check_all_links(form_name)
 1429     link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
 1430     " | ".html_safe +
 1431     link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
 1432   end
 1433 
 1434   def toggle_checkboxes_link(selector)
 1435     link_to_function '',
 1436                      "toggleCheckboxesBySelector('#{selector}')",
 1437                      :title => "#{l(:button_check_all)} / #{l(:button_uncheck_all)}",
 1438                      :class => 'icon icon-checked'
 1439   end
 1440 
 1441   def progress_bar(pcts, options={})
 1442     pcts = [pcts, pcts] unless pcts.is_a?(Array)
 1443     pcts = pcts.collect(&:floor)
 1444     pcts[1] = pcts[1] - pcts[0]
 1445     pcts << (100 - pcts[1] - pcts[0])
 1446     titles = options[:titles].to_a
 1447     titles[0] = "#{pcts[0]}%" if titles[0].blank?
 1448     legend = options[:legend] || ''
 1449     content_tag(
 1450       'table',
 1451       content_tag(
 1452         'tr',
 1453         (if pcts[0] > 0
 1454            content_tag('td', '', :style => "width: #{pcts[0]}%;",
 1455                        :class => 'closed', :title => titles[0])
 1456          else
 1457            ''.html_safe
 1458          end) +
 1459         (if pcts[1] > 0
 1460            content_tag('td', '', :style => "width: #{pcts[1]}%;",
 1461                       :class => 'done', :title => titles[1])
 1462          else
 1463            ''.html_safe
 1464          end) +
 1465         (if pcts[2] > 0
 1466            content_tag('td', '', :style => "width: #{pcts[2]}%;",
 1467                                    :class => 'todo', :title => titles[2])
 1468          else
 1469            ''.html_safe
 1470          end)
 1471       ), :class => "progress progress-#{pcts[0]}").html_safe +
 1472       content_tag('p', legend, :class => 'percent').html_safe
 1473   end
 1474 
 1475   def checked_image(checked=true)
 1476     if checked
 1477       @checked_image_tag ||= content_tag(:span, nil, :class => 'icon-only icon-checked')
 1478     end
 1479   end
 1480 
 1481   def context_menu
 1482     unless @context_menu_included
 1483       content_for :header_tags do
 1484         javascript_include_tag('context_menu') +
 1485           stylesheet_link_tag('context_menu')
 1486       end
 1487       if l(:direction) == 'rtl'
 1488         content_for :header_tags do
 1489           stylesheet_link_tag('context_menu_rtl')
 1490         end
 1491       end
 1492       @context_menu_included = true
 1493     end
 1494     nil
 1495   end
 1496 
 1497   def calendar_for(field_id)
 1498     include_calendar_headers_tags
 1499     javascript_tag("$(function() { $('##{field_id}').addClass('date').datepickerFallback(datepickerOptions); });")
 1500   end
 1501 
 1502   def include_calendar_headers_tags
 1503     unless @calendar_headers_tags_included
 1504       tags = ''.html_safe
 1505       @calendar_headers_tags_included = true
 1506       content_for :header_tags do
 1507         start_of_week = Setting.start_of_week
 1508         start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
 1509         # Redmine uses 1..7 (monday..sunday) in settings and locales
 1510         # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
 1511         start_of_week = start_of_week.to_i % 7
 1512         tags << javascript_tag(
 1513                    "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
 1514                      "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
 1515                      path_to_image('/images/calendar.png') +
 1516                      "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
 1517                      "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
 1518                      "beforeShow: beforeShowDatePicker};")
 1519         jquery_locale = l('jquery.locale', :default => current_language.to_s)
 1520         unless jquery_locale == 'en'
 1521           tags << javascript_include_tag("i18n/datepicker-#{jquery_locale}.js")
 1522         end
 1523         tags
 1524       end
 1525     end
 1526   end
 1527 
 1528   # Overrides Rails' stylesheet_link_tag with themes and plugins support.
 1529   # Examples:
 1530   #   stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
 1531   #   stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
 1532   #
 1533   def stylesheet_link_tag(*sources)
 1534     options = sources.last.is_a?(Hash) ? sources.pop : {}
 1535     plugin = options.delete(:plugin)
 1536     sources = sources.map do |source|
 1537       if plugin
 1538         "/plugin_assets/#{plugin}/stylesheets/#{source}"
 1539       elsif current_theme && current_theme.stylesheets.include?(source)
 1540         current_theme.stylesheet_path(source)
 1541       else
 1542         source
 1543       end
 1544     end
 1545     super *sources, options
 1546   end
 1547 
 1548   # Overrides Rails' image_tag with themes and plugins support.
 1549   # Examples:
 1550   #   image_tag('image.png') # => picks image.png from the current theme or defaults
 1551   #   image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
 1552   #
 1553   def image_tag(source, options={})
 1554     if plugin = options.delete(:plugin)
 1555       source = "/plugin_assets/#{plugin}/images/#{source}"
 1556     elsif current_theme && current_theme.images.include?(source)
 1557       source = current_theme.image_path(source)
 1558     end
 1559     super source, options
 1560   end
 1561 
 1562   # Overrides Rails' javascript_include_tag with plugins support
 1563   # Examples:
 1564   #   javascript_include_tag('scripts') # => picks scripts.js from defaults
 1565   #   javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
 1566   #
 1567   def javascript_include_tag(*sources)
 1568     options = sources.last.is_a?(Hash) ? sources.pop : {}
 1569     if plugin = options.delete(:plugin)
 1570       sources = sources.map do |source|
 1571         if plugin
 1572           "/plugin_assets/#{plugin}/javascripts/#{source}"
 1573         else
 1574           source
 1575         end
 1576       end
 1577     end
 1578     super *sources, options
 1579   end
 1580 
 1581   def sidebar_content?
 1582     content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
 1583   end
 1584 
 1585   def view_layouts_base_sidebar_hook_response
 1586     @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
 1587   end
 1588 
 1589   def email_delivery_enabled?
 1590     !!ActionMailer::Base.perform_deliveries
 1591   end
 1592 
 1593   def sanitize_anchor_name(anchor)
 1594     anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
 1595   end
 1596 
 1597   # Returns the javascript tags that are included in the html layout head
 1598   def javascript_heads
 1599     tags = javascript_include_tag('jquery-2.2.4-ui-1.11.0-ujs-5.2.3', 'tribute-3.7.3.min', 'application', 'responsive')
 1600     unless User.current.pref.warn_on_leaving_unsaved == '0'
 1601       tags << "\n".html_safe + javascript_tag("$(window).on('load', function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
 1602     end
 1603     tags
 1604   end
 1605 
 1606   def favicon
 1607     "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
 1608   end
 1609 
 1610   # Returns the path to the favicon
 1611   def favicon_path
 1612     icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
 1613     image_path(icon)
 1614   end
 1615 
 1616   # Returns the full URL to the favicon
 1617   def favicon_url
 1618     # TODO: use #image_url introduced in Rails4
 1619     path = favicon_path
 1620     base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
 1621     base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
 1622   end
 1623 
 1624   def robot_exclusion_tag
 1625     '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
 1626   end
 1627 
 1628   # Returns true if arg is expected in the API response
 1629   def include_in_api_response?(arg)
 1630     unless @included_in_api_response
 1631       param = params[:include]
 1632       @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
 1633       @included_in_api_response.collect!(&:strip)
 1634     end
 1635     @included_in_api_response.include?(arg.to_s)
 1636   end
 1637 
 1638   # Returns options or nil if nometa param or X-Redmine-Nometa header
 1639   # was set in the request
 1640   def api_meta(options)
 1641     if params[:nometa].present? || request.headers['X-Redmine-Nometa']
 1642       # compatibility mode for activeresource clients that raise
 1643       # an error when deserializing an array with attributes
 1644       nil
 1645     else
 1646       options
 1647     end
 1648   end
 1649 
 1650   def export_csv_encoding_select_tag
 1651     return if l(:general_csv_encoding).casecmp('UTF-8') == 0
 1652     options = [l(:general_csv_encoding), 'UTF-8']
 1653     content_tag(:p) do
 1654       concat(
 1655         content_tag(:label) do
 1656           concat l(:label_encoding) + ' '
 1657           concat select_tag('encoding', options_for_select(options, l(:general_csv_encoding)))
 1658         end
 1659       )
 1660     end
 1661   end
 1662 
 1663   # Returns an array of error messages for bulk edited items (issues, time entries)
 1664   def bulk_edit_error_messages(items)
 1665     messages = {}
 1666     items.each do |item|
 1667       item.errors.full_messages.each do |message|
 1668         messages[message] ||= []
 1669         messages[message] << item
 1670       end
 1671     end
 1672     messages.map { |message, items|
 1673       "#{message}: " + items.map {|i| "##{i.id}"}.join(', ')
 1674     }
 1675   end
 1676 
 1677   private
 1678 
 1679   def wiki_helper
 1680     helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
 1681     extend helper
 1682     return self
 1683   end
 1684 
 1685   # remove double quotes if any
 1686   def remove_double_quotes(identifier)
 1687     name = identifier.gsub(%r{^"(.*)"$}, "\\1")
 1688     return CGI.unescapeHTML(name)
 1689   end
 1690 end