diff options
Diffstat (limited to 'lib')
23 files changed, 315 insertions, 98 deletions
diff --git a/lib/plugins/gravatar/lib/gravatar.rb b/lib/plugins/gravatar/lib/gravatar.rb index 4dc27db52..43820008f 100644 --- a/lib/plugins/gravatar/lib/gravatar.rb +++ b/lib/plugins/gravatar/lib/gravatar.rb @@ -32,7 +32,7 @@ module GravatarHelper :title => '', # The class to assign to the img tag for the gravatar. - :class => 'gravatar', + :class => 'gravatar avatar', } # The methods that will be made available to your views. @@ -69,7 +69,7 @@ module GravatarHelper options[:default] = CGI::escape(options[:default]) unless options[:default].nil? gravatar_api_url(email_hash).tap do |url| opts = [] - [:rating, :size, :default].each do |opt| + [:rating, :size, :default, :initials].each do |opt| unless options[opt].nil? value = h(options[opt]) opts << [opt, value].join('=') diff --git a/lib/redmine.rb b/lib/redmine.rb index 95b3b7f3f..78a1a6d8c 100644 --- a/lib/redmine.rb +++ b/lib/redmine.rb @@ -24,11 +24,6 @@ begin rescue LoadError # MiniMagick is not available end -begin - require 'commonmarker' unless Object.const_defined?(:Commonmarker) -rescue LoadError - # CommonMarker is not available -end module Redmine end diff --git a/lib/redmine/activity.rb b/lib/redmine/activity.rb index 826b81c9e..c972eeee2 100644 --- a/lib/redmine/activity.rb +++ b/lib/redmine/activity.rb @@ -19,11 +19,11 @@ module Redmine module Activity - mattr_accessor :available_event_types, :default_event_types, :plugins_event_types, :providers + mattr_accessor :available_event_types, :default_event_types, :plugins_event_classes, :providers @@available_event_types = [] @@default_event_types = [] - @@plugins_event_types = {} + @@plugins_event_classes = {} @@providers = Hash.new {|h, k| h[k]=[]} class << self @@ -41,19 +41,22 @@ module Redmine @@available_event_types << event_type unless @@available_event_types.include?(event_type) @@default_event_types << event_type unless options[:default] == false - @@plugins_event_types = { event_type => options[:plugin].to_s } unless options[:plugin].nil? + if options[:plugin] + providers.each do |provider| + @@plugins_event_classes[provider] = options[:plugin].to_s + end + end @@providers[event_type] += providers end def delete(event_type) @@available_event_types.delete event_type @@default_event_types.delete event_type - @@plugins_event_types.delete(event_type) @@providers.delete(event_type) end - def plugin_name(event_type) - @@plugins_event_types[event_type] + def plugin_name(class_name) + @@plugins_event_classes[class_name.to_s] end end end diff --git a/lib/redmine/core_ext/string/conversions.rb b/lib/redmine/core_ext/string/conversions.rb index e98e400e4..bbf57e363 100644 --- a/lib/redmine/core_ext/string/conversions.rb +++ b/lib/redmine/core_ext/string/conversions.rb @@ -39,7 +39,7 @@ module Redmine end # 2,5 => 2.5 s.tr!(',', '.') - begin; Kernel.Float(s); rescue; nil; end + Kernel.Float(s, exception: false) end end end diff --git a/lib/redmine/database.rb b/lib/redmine/database.rb index b3cbdc661..13c92b8a4 100644 --- a/lib/redmine/database.rb +++ b/lib/redmine/database.rb @@ -58,7 +58,7 @@ module Redmine # Returns true if the database is MySQL def mysql? - /mysql/i.match?(ActiveRecord::Base.connection.adapter_name) + /mysql|trilogy/i.match?(ActiveRecord::Base.connection.adapter_name) end def mysql_version diff --git a/lib/redmine/diff.rb b/lib/redmine/diff.rb index 40c444a42..c925d463a 100644 --- a/lib/redmine/diff.rb +++ b/lib/redmine/diff.rb @@ -76,13 +76,9 @@ module Redmine def line_to_html_raw(line, offsets) if offsets s = +'' - unless offsets.first == 0 - s << CGI.escapeHTML(line[0..offsets.first-1]) - end + s << CGI.escapeHTML(line[0..(offsets.first - 1)]) unless offsets.first == 0 s << '<span>' + CGI.escapeHTML(line[offsets.first..offsets.last]) + '</span>' - unless offsets.last == -1 - s << CGI.escapeHTML(line[offsets.last+1..-1]) - end + s << CGI.escapeHTML(line[(offsets.last + 1)..-1]) unless offsets.last == -1 s else CGI.escapeHTML(line) diff --git a/lib/redmine/field_format.rb b/lib/redmine/field_format.rb index c4fd1b592..39b21c874 100644 --- a/lib/redmine/field_format.rb +++ b/lib/redmine/field_format.rb @@ -110,8 +110,8 @@ module Redmine end private_class_method :add - def self.field_attributes(*args) - CustomField.store_accessor :format_store, *args + def self.field_attributes(*) + CustomField.store_accessor(:format_store, *) end field_attributes :url_pattern, :full_width_layout @@ -1086,8 +1086,15 @@ module Redmine class ProgressbarFormat < Numeric add 'progressbar' - self.form_partial = nil + self.form_partial = 'custom_fields/formats/progressbar' self.totalable_supported = false + field_attributes :ratio_interval + + # Take the default value from Setting.issue_done_ratio_interval.to_i + # in order to have a consistent behaviour for default ratio interval. + def self.default_ratio_interval + Setting.issue_done_ratio_interval.to_i + end def label "label_progressbar" @@ -1112,11 +1119,19 @@ module Redmine order_statement(custom_field) end + def before_custom_field_save(custom_field) + super + + if custom_field.ratio_interval.blank? + custom_field.ratio_interval = self.class.default_ratio_interval + end + end + def edit_tag(view, tag_id, tag_name, custom_value, options={}) view.select_tag( tag_name, view.options_for_select( - (0..100).step(Setting.issue_done_ratio_interval.to_i).to_a.collect {|r| ["#{r} %", r]}, + (0..100).step(custom_value.custom_field.ratio_interval.to_i).to_a.collect {|r| ["#{r} %", r]}, custom_value.value ), options.merge(id: tag_id, style: "width: 75px;") @@ -1124,17 +1139,17 @@ module Redmine end def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={}) - opts = view.options_for_select([[l(:label_no_change_option), '']] + (0..100).step(Setting.issue_done_ratio_interval.to_i).to_a.collect {|r| ["#{r} %", r]}) + opts = view.options_for_select([[l(:label_no_change_option), '']] + (0..100).step(custom_field.ratio_interval.to_i).to_a.collect {|r| ["#{r} %", r]}) view.select_tag(tag_name, opts, options.merge(id: tag_id, style: "width: 75px;")) + bulk_clear_tag(view, tag_id, tag_name, custom_field, value) end def formatted_value(view, custom_field, value, customized=nil, html=false) - text = "#{value}%" if html + text = "#{value}%" view.progress_bar(value.to_i, legend: (text if view.action_name == 'show')) else - text + value.to_s end end end diff --git a/lib/redmine/helpers/gantt.rb b/lib/redmine/helpers/gantt.rb index bd784609b..d23c40b38 100644 --- a/lib/redmine/helpers/gantt.rb +++ b/lib/redmine/helpers/gantt.rb @@ -198,12 +198,18 @@ module Redmine # Returns the distinct versions of the issues that belong to +project+ def project_versions(project) - project_issues(project).filter_map(&:fixed_version).uniq + @project_versions ||= {} + @project_versions[project&.id] ||= begin + ids = project_issues(project).filter_map(&:fixed_version_id).uniq + Version.where(id: ids).to_a + end end # Returns the issues that belong to +project+ and are assigned to +version+ def version_issues(project, version) - project_issues(project).select {|issue| issue.fixed_version == version} + @version_issues ||= {} + @version_issues[[project&.id, version&.id]] ||= + project_issues(project).select {|issue| issue.fixed_version_id == version&.id} end def render(options={}) @@ -232,7 +238,7 @@ module Redmine render_object_row(project, options) increment_indent(options) do # render issue that are not assigned to a version - issues = project_issues(project).select {|i| i.fixed_version.nil?} + issues = project_issues(project).select {|i| i.fixed_version_id.nil?} render_issues(issues, options) # then render project versions and their issues versions = project_versions(project) @@ -502,7 +508,7 @@ module Redmine lines(:image => gc, :top => top, :zoom => zoom, :subject_width => subject_width, :format => :image) # today red line - if User.current.today >= @date_from and User.current.today <= date_to + if User.current.today.between?(@date_from, date_to) gc.stroke('red') x = (User.current.today - @date_from + 1) * zoom + subject_width gc.draw('line %g,%g %g,%g' % [ @@ -725,7 +731,7 @@ module Redmine css_classes = +'' css_classes << ' issue-overdue' if issue.overdue? css_classes << ' issue-behind-schedule' if issue.behind_schedule? - css_classes << ' icon icon-issue' unless Setting.gravatar_enabled? && issue.assigned_to + css_classes << ' icon icon-issue' unless issue.assigned_to css_classes << ' issue-closed' if issue.closed? if issue.start_date && issue.due_before && issue.done_ratio progress_date = calc_progress_date(issue.start_date, @@ -734,8 +740,8 @@ module Redmine css_classes << ' over-end-date' if progress_date > self.date_to && issue.done_ratio > 0 end s = (+"").html_safe - s << view.sprite_icon('issue').html_safe unless Setting.gravatar_enabled? && issue.assigned_to - s << view.assignee_avatar(issue.assigned_to, :size => 13, :class => 'icon-gravatar') + s << view.sprite_icon('issue').html_safe unless issue.assigned_to + s << view.assignee_avatar(issue.assigned_to, :size => 13, :class => 'icon-avatar') s << view.link_to_issue(issue).html_safe s << view.content_tag(:input, nil, :type => 'checkbox', :name => 'ids[]', :value => issue.id, :style => 'display:none;', @@ -748,7 +754,7 @@ module Redmine html_class << (version.behind_schedule? ? 'version-behind-schedule' : '') << " " html_class << (version.overdue? ? 'version-overdue' : '') html_class << ' version-closed' unless version.open? - if version.start_date && version.due_date && version.visible_fixed_issues.completed_percent + if version.due_date && version.start_date && version.visible_fixed_issues.completed_percent progress_date = calc_progress_date(version.start_date, version.due_date, version.visible_fixed_issues.completed_percent) html_class << ' behind-start-date' if progress_date < self.date_from @@ -778,10 +784,14 @@ module Redmine tag_options[:id] = "issue-#{object.id}" tag_options[:class] = "issue-subject hascontextmenu" tag_options[:title] = object.subject - children = object.leaf? ? [] : object.children & project_issues(object.project) has_children = - children.present? && - children.collect(&:fixed_version).uniq.intersect?([object.fixed_version]) + if object.leaf? + false + else + children = object.children & project_issues(object.project) + fixed_version_id = object.fixed_version_id + children.any? {|child| child.fixed_version_id == fixed_version_id} + end when Version tag_options[:id] = "version-#{object.id}" tag_options[:class] = "version-name" diff --git a/lib/redmine/i18n.rb b/lib/redmine/i18n.rb index f41e69474..0b31cb235 100644 --- a/lib/redmine/i18n.rb +++ b/lib/redmine/i18n.rb @@ -65,9 +65,9 @@ module Redmine end # Localizes the given args with user's language - def lu(user, *args) + def lu(user, *) lang = user.try(:language).presence || Setting.default_language - ll(lang, *args) + ll(lang, *) end def format_date(date) @@ -173,24 +173,5 @@ module Redmine def current_language ::I18n.locale end - - # Custom backend based on I18n::Backend::Simple with the following changes: - # * available_locales are determined by looking at translation file names - class Backend < ::I18n::Backend::Simple - module Implementation - # Get available locales from the translations filenames - def available_locales - @available_locales ||= begin - redmine_locales = Dir[Rails.root / 'config' / 'locales' / '*.yml'].map { |f| File.basename(f, '.yml').to_sym } - super & redmine_locales - end - end - end - - # Adds custom pluralization rules - include ::I18n::Backend::Pluralization - # Adds fallback to default locale for untranslated strings - include ::I18n::Backend::Fallbacks - end end end diff --git a/lib/redmine/plugin.rb b/lib/redmine/plugin.rb index 223b3927a..74ae4efaf 100644 --- a/lib/redmine/plugin.rb +++ b/lib/redmine/plugin.rb @@ -250,7 +250,7 @@ module Redmine ) end elsif req.is_a?(Range) - unless compare_versions(req.first, current) <= 0 && compare_versions(req.last, current) >= 0 + unless compare_versions(req.first, current) <= 0 && compare_versions(req.last, current) >= 0 # rubocop:disable Style/ComparableBetween raise PluginRequirementError.new( "#{id} plugin requires a Redmine version between #{req.first} " \ "and #{req.last} but current is #{current.join('.')}" @@ -406,8 +406,8 @@ module Redmine # Meeting.find_events('scrums', User.current, 5.days.ago, Date.today, :project => foo) # events for project foo only # # Note that :view_scrums permission is required to view these events in the activity view. - def activity_provider(*args) - Redmine::Activity.register(*args) + def activity_provider(*) + Redmine::Activity.register(*) end # Registers a wiki formatter. @@ -423,8 +423,8 @@ module Redmine # Examples: # wiki_format_provider(:custom_formatter, CustomFormatter, :label => "My custom formatter") # - def wiki_format_provider(name, *args) - Redmine::WikiFormatting.register(name, *args) + def wiki_format_provider(name, *) + Redmine::WikiFormatting.register(name, *) end # Register plugin models that use acts_as_attachable. diff --git a/lib/redmine/preparation.rb b/lib/redmine/preparation.rb index 822662e11..a7387f5dc 100644 --- a/lib/redmine/preparation.rb +++ b/lib/redmine/preparation.rb @@ -280,6 +280,11 @@ module Redmine {:controller => 'auth_sources', :action => 'index'}, :icon => 'server-authentication', :html => {:class => 'icon icon-server-authentication'} + menu.push :applications, {:controller => 'oauth2_applications', :action => 'index'}, + :if => Proc.new { Setting.rest_api_enabled? }, + :caption => :'doorkeeper.layouts.admin.nav.applications', + :icon => 'apps', + :html => {:class => 'icon icon-applications'} menu.push :plugins, {:controller => 'admin', :action => 'plugins'}, :last => true, :icon => 'plugins', @@ -408,9 +413,7 @@ module Redmine WikiFormatting.map do |format| format.register :textile - if Object.const_defined?(:Commonmarker) - format.register :common_mark, label: 'CommonMark Markdown (GitHub Flavored)' - end + format.register :common_mark, label: 'CommonMark Markdown (GitHub Flavored)' end ActionView::Template.register_template_handler :rsb, Views::ApiTemplateHandler diff --git a/lib/redmine/quote_reply.rb b/lib/redmine/quote_reply.rb index 05737c079..f6d7821cd 100644 --- a/lib/redmine/quote_reply.rb +++ b/lib/redmine/quote_reply.rb @@ -20,21 +20,18 @@ module Redmine module QuoteReply module Helper - def javascripts_for_quote_reply_include_tag - javascript_include_tag 'turndown-7.2.0.min', 'quote_reply' - end - - def quote_reply(url, selector_for_content, icon_only: false) - quote_reply_function = "quoteReply('#{j url}', '#{j selector_for_content}', '#{j Setting.text_formatting}')" + def quote_reply_button(url:, icon_only: false) + button_params = { + data: { + action: 'quote-reply#quote', + quote_reply_url_param: url, + quote_reply_text_formatting_param: Setting.text_formatting + }, + class: "#{icon_only ? "icon-only" : "icon"} icon-quote" + } + button_params[:title] = l(:button_quote) if icon_only - html_options = { class: 'icon icon-comment' } - html_options[:title] = l(:button_quote) if icon_only - - link_to_function( - sprite_icon('comment', l(:button_quote), icon_only: icon_only), - quote_reply_function, - html_options - ) + link_to sprite_icon('quote-filled', l(:button_quote), icon_only: icon_only, style: :filled), '#', button_params end end diff --git a/lib/redmine/reaction.rb b/lib/redmine/reaction.rb new file mode 100644 index 000000000..09fb78ef8 --- /dev/null +++ b/lib/redmine/reaction.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +# Redmine - project management software +# Copyright (C) 2006- Jean-Philippe Lang +# +# 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. +# +# 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. +# +# 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. + +module Redmine + module Reaction + # Types of objects that can have reactions + REACTABLE_TYPES = %w(Journal Issue Message News Comment) + + # Returns true if the user can view the reaction of the object + def self.visible?(object, user = User.current) + Setting.reactions_enabled? && object.visible?(user) + end + + # Returns true if the user can add/remove a reaction to/from the object + def self.editable?(object, user = User.current) + user.logged? && visible?(object, user) && object&.project&.active? + end + + module Reactable + extend ActiveSupport::Concern + + included do + has_many :reactions, as: :reactable, dependent: :delete_all + + attr_writer :reaction_detail + end + + class_methods do + # Preloads reaction details for a collection of objects + def preload_reaction_details(objects) + return unless Setting.reactions_enabled? + + details = ::Reaction.build_detail_map_for(objects, User.current) + + objects.each do |object| + object.reaction_detail = details.fetch(object.id) { ::Reaction::Detail.new } + end + end + end + + def reaction_detail + # Loads and returns reaction details if they are not already loaded. + # This is intended for cases where explicit preloading is unnecessary, + # such as retrieving reactions for a single issue on its detail page. + load_reaction_detail unless defined?(@reaction_detail) + @reaction_detail + end + + def load_reaction_detail + self.class.preload_reaction_details([self]) + end + end + end +end diff --git a/lib/redmine/scm/adapters/mercurial_adapter.rb b/lib/redmine/scm/adapters/mercurial_adapter.rb index 28a1922ca..562dd59d5 100644 --- a/lib/redmine/scm/adapters/mercurial_adapter.rb +++ b/lib/redmine/scm/adapters/mercurial_adapter.rb @@ -50,7 +50,10 @@ module Redmine end def client_available - client_version_above?([5, 1]) + client_version_above?([5, 1]) && + # Redmine >= 6.1 has dropped support for Python 2.7, and + # Mercurial has never supported Python 3.0 to 3.4 + (python_version <=> [3, 5]) >= 0 end def hgversion @@ -67,6 +70,23 @@ module Redmine shellout("#{sq_bin} --version") {|io| io.read}.to_s end + def python_version + @@python_version ||= begin + debuginstall = hgdebuginstall_from_command_line + if (m = debuginstall.match(/checking Python version \(([\d.]+)\)/)) + m[1].scan(%r{\d+}) + .collect(&:to_i) + .presence + else + nil + end + end + end + + def hgdebuginstall_from_command_line + shellout("#{sq_bin} debuginstall") {|io| io.read}.to_s + end + def template_path @@template_path ||= template_path_for(client_version) end diff --git a/lib/redmine/sort_criteria.rb b/lib/redmine/sort_criteria.rb index 461cd3ac1..01cb95871 100644 --- a/lib/redmine/sort_criteria.rb +++ b/lib/redmine/sort_criteria.rb @@ -48,8 +48,8 @@ module Redmine normalize! end - def add(*args) - self.class.new(self).add!(*args) + def add(*) + self.class.new(self).add!(*) end def first_key diff --git a/lib/redmine/subclass_factory.rb b/lib/redmine/subclass_factory.rb index 0905f907a..53db6696b 100644 --- a/lib/redmine/subclass_factory.rb +++ b/lib/redmine/subclass_factory.rb @@ -38,10 +38,10 @@ module Redmine end # Returns an instance of the given subclass name - def new_subclass_instance(class_name, *args) + def new_subclass_instance(class_name, *) klass = get_subclass(class_name) if klass - klass.new(*args) + klass.new(*) end end end diff --git a/lib/redmine/syntax_highlighting.rb b/lib/redmine/syntax_highlighting.rb index b5785f837..9ae5fed44 100644 --- a/lib/redmine/syntax_highlighting.rb +++ b/lib/redmine/syntax_highlighting.rb @@ -125,6 +125,7 @@ module Redmine 'java_script' => 'javascript', 'xhtml' => 'html' } + private_constant :LANG_ALIASES def find_lexer(language) ::Rouge::Lexer.find(language) || diff --git a/lib/redmine/version.rb b/lib/redmine/version.rb index 31d80381d..79e1b5984 100644 --- a/lib/redmine/version.rb +++ b/lib/redmine/version.rb @@ -24,7 +24,7 @@ module Redmine module VERSION MAJOR = 6 MINOR = 0 - TINY = 4 + TINY = 6 # Branch values: # * official release: nil diff --git a/lib/redmine/wiki_formatting/common_mark/alerts_icons_filter.rb b/lib/redmine/wiki_formatting/common_mark/alerts_icons_filter.rb new file mode 100644 index 000000000..27429d778 --- /dev/null +++ b/lib/redmine/wiki_formatting/common_mark/alerts_icons_filter.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +# Redmine - project management software +# Copyright (C) 2006- Jean-Philippe Lang +# +# 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. +# +# 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. +# +# 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. + +module Redmine + module WikiFormatting + module CommonMark + # Defines the mapping from alert type (from CSS class) to SVG icon name. + # These icon names must correspond to IDs in your SVG sprite sheet (e.g., icons.svg). + ALERT_TYPE_TO_ICON_NAME = { + 'note' => 'help', + 'tip' => 'bulb', + 'warning' => 'warning', + 'caution' => 'alert-circle', + 'important' => 'message-report', + }.freeze + + class AlertsIconsFilter < HTML::Pipeline::Filter + def call + doc.search("p.markdown-alert-title").each do |node| + parent_node = node.parent + parent_class_attr = parent_node['class'] # e.g., "markdown-alert markdown-alert-note" + next unless parent_class_attr + + # Extract the specific alert type (e.g., "note", "tip", "warning") + # from the parent div's classes. + match_data = parent_class_attr.match(/markdown-alert-(\w+)/) + next unless match_data && match_data[1] # Ensure a type is found + + alert_type = match_data[1] + + # Get the corresponding icon name from our map. + icon_name = ALERT_TYPE_TO_ICON_NAME[alert_type] + next unless icon_name # Skip if no specific icon is defined for this alert type + + icon_html = ApplicationController.helpers.sprite_icon(icon_name, node.text) + + if icon_html + # Replace the existing text node with the icon HTML and label (text). + node.children.first.replace(icon_html) + end + end + doc + end + end + end + end +end diff --git a/lib/redmine/wiki_formatting/common_mark/formatter.rb b/lib/redmine/wiki_formatting/common_mark/formatter.rb index aab8eed8b..8b7a18394 100644 --- a/lib/redmine/wiki_formatting/common_mark/formatter.rb +++ b/lib/redmine/wiki_formatting/common_mark/formatter.rb @@ -18,7 +18,6 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. require 'html/pipeline' -require 'task_list/filter' module Redmine module WikiFormatting @@ -32,6 +31,10 @@ module Redmine tagfilter: true, autolink: true, footnotes: true, + header_ids: nil, + tasklist: true, + shortcodes: false, + alerts: true, }.freeze, # https://github.com/gjtorikian/commonmarker#parse-options @@ -41,7 +44,9 @@ module Redmine # https://github.com/gjtorikian/commonmarker#render-options commonmarker_render_options: { unsafe: true, + github_pre_lang: false, hardbreaks: Redmine::Configuration['common_mark_enable_hardbreaks'] == true, + tasklist_classes: true, }.freeze, commonmarker_plugins: { syntax_highlighter: nil @@ -54,7 +59,7 @@ module Redmine SyntaxHighlightFilter, FixupAutoLinksFilter, ExternalLinksFilter, - TaskList::Filter + AlertsIconsFilter ], PIPELINE_CONFIG class Formatter diff --git a/lib/redmine/wiki_formatting/common_mark/sanitization_filter.rb b/lib/redmine/wiki_formatting/common_mark/sanitization_filter.rb index cdefc372b..af72adc32 100644 --- a/lib/redmine/wiki_formatting/common_mark/sanitization_filter.rb +++ b/lib/redmine/wiki_formatting/common_mark/sanitization_filter.rb @@ -68,6 +68,26 @@ module Redmine end } + # Allow class on div and p tags only for alert blocks + # (must be exactly: "markdown-alert markdown-alert-*" for div, and "markdown-alert-title" for p) + (allowlist[:attributes]["div"] ||= []) << "class" + (allowlist[:attributes]["p"] ||= []) << "class" + allowlist[:transformers].push lambda{|env| + node = env[:node] + return unless node.element? + + case node.name + when 'div' + unless /\Amarkdown-alert markdown-alert-[a-z]+\z/.match?(node['class']) + node.remove_attribute('class') + end + when 'p' + unless node['class'] == 'markdown-alert-title' + node.remove_attribute('class') + end + end + } + # Allow table cell alignment by style attribute # # Only necessary if we used the TABLE_PREFER_STYLE_ATTRIBUTES @@ -78,20 +98,58 @@ module Redmine # allowlist[:attributes]["td"] = %w(style) # allowlist[:css] = { properties: ["text-align"] } - # Allow `id` in a and li elements for footnotes - # and remove any `id` properties not matching for footnotes + # Allow `id` in a elements for footnotes allowlist[:attributes]["a"].push "id" - allowlist[:attributes]["li"] = %w(id) + # Remove any `id` property not matching for footnotes allowlist[:transformers].push lambda{|env| node = env[:node] - return unless node.name == "a" || node.name == "li" + return unless node.name == "a" return unless node.has_attribute?("id") - return if node.name == "a" && node["id"] =~ /\Afnref-\d+\z/ - return if node.name == "li" && node["id"] =~ /\Afn-\d+\z/ + return if node.name == "a" && node["id"] =~ /\Afnref(-\d+){1,2}\z/ node.remove_attribute("id") } + # allow `id` in li element for footnotes + # allow `class` in li element for task list items + allowlist[:attributes]["li"] = %w(id class) + allowlist[:transformers].push lambda{|env| + node = env[:node] + return unless node.name == "li" + + if node.has_attribute?("id") && !(node["id"] =~ /\Afn-\d+\z/) + node.remove_attribute("id") + end + + if node.has_attribute?("class") && node["class"] != "task-list-item" + node.remove_attribute("class") + end + } + + # allow input type = "checkbox" with class "task-list-item-checkbox" + # for task list items + allowlist[:elements].push('input') + allowlist[:attributes]["input"] = %w(class type) + allowlist[:transformers].push lambda{|env| + node = env[:node] + + return unless node.name == "input" + return if node['type'] == "checkbox" && node['class'] == "task-list-item-checkbox" + + node.replace(node.children) + } + + # allow class "contains-task-list" on ul for task list items + allowlist[:attributes]["ul"] = %w(class) + allowlist[:transformers].push lambda{|env| + node = env[:node] + + return unless node.name == "ul" + return if node["class"] == "contains-task-list" + + node.remove_attribute("class") + } + # https://github.com/rgrove/sanitize/issues/209 allowlist[:protocols].delete("a") allowlist[:transformers].push lambda{|env| diff --git a/lib/redmine/wiki_formatting/macros.rb b/lib/redmine/wiki_formatting/macros.rb index ef1135cb6..16abcb429 100644 --- a/lib/redmine/wiki_formatting/macros.rb +++ b/lib/redmine/wiki_formatting/macros.rb @@ -248,7 +248,7 @@ module Redmine hide_label = args[1] || args[0] || l(:button_hide) js = "$('##{html_id}-show, ##{html_id}-hide').toggle(); $('##{html_id}').fadeToggle(150);" out = ''.html_safe - out << link_to_function(sprite_icon('angle-right', show_label), js, :id => "#{html_id}-show", :class => 'icon icon-collapsed collapsible') + out << link_to_function(sprite_icon('angle-right', show_label, rtl: true), js, :id => "#{html_id}-show", :class => 'icon icon-collapsed collapsible') out << link_to_function( sprite_icon('angle-down', hide_label), js, diff --git a/lib/tasks/icons.rake b/lib/tasks/icons.rake index e50c450a1..269ef43e2 100644 --- a/lib/tasks/icons.rake +++ b/lib/tasks/icons.rake @@ -16,7 +16,7 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. unless Rails.env.production? - ICON_RELEASE_VERSION = "v3.19.0" + ICON_RELEASE_VERSION = "v3.33.0" ICON_DEFAULT_STYLE = "outline" SOURCE = URI.parse("https://raw.githubusercontent.com/tabler/tabler-icons/#{ICON_RELEASE_VERSION}/icons") |