diff --git a/config/locales/en.yml b/config/locales/en.yml index 719adf8141d..bb3db89ccb7 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -4359,6 +4359,7 @@ en: label_total_days_off: "Total days off" macro_execution_error: "Error executing the macro %{macro_name}" macro_unavailable: "Macro %{macro_name} cannot be displayed." + macro_unknown: "Unknown or unsupported macro." macros: placeholder: "[Placeholder] Macro %{macro_name}" errors: diff --git a/lib/open_project/text_formatting/filters/macro_filter.rb b/lib/open_project/text_formatting/filters/macro_filter.rb index 8f2699efd01..b6a30765918 100644 --- a/lib/open_project/text_formatting/filters/macro_filter.rb +++ b/lib/open_project/text_formatting/filters/macro_filter.rb @@ -41,9 +41,13 @@ module OpenProject::TextFormatting def call # rubocop:disable Metrics/AbcSize doc.search("macro").each do |macro| + matched = false + registered.each do |macro_class| next unless macro_applies?(macro_class, macro) + matched = true + # If requested to skip macro expansion, do that if context[:disable_macro_expansion] macro.replace macro_placeholder(macro_class) @@ -60,6 +64,8 @@ module OpenProject::TextFormatting break end end + + macro.replace unknown_macro_placeholder unless matched end doc @@ -67,6 +73,13 @@ module OpenProject::TextFormatting private + def unknown_macro_placeholder + ApplicationController.helpers.content_tag :macro, + I18n.t(:macro_unknown), + class: "macro-unavailable", + data: { macro_name: "unknown" } + end + def macro_error_placeholder(macro_class, message) ApplicationController.helpers.content_tag :macro, "#{I18n.t(:macro_execution_error, diff --git a/lib/open_project/text_formatting/filters/sanitization_filter.rb b/lib/open_project/text_formatting/filters/sanitization_filter.rb index c891ebf9ad6..8e9b775920b 100644 --- a/lib/open_project/text_formatting/filters/sanitization_filter.rb +++ b/lib/open_project/text_formatting/filters/sanitization_filter.rb @@ -52,8 +52,8 @@ module OpenProject::TextFormatting remove_contents: Array(base[:remove_contents]) | %w[svg style], attributes: base_attrs.deep_merge( - # Whitelist class and data-* attributes on all macros - "macro" => ["class", :data], + # Explicit allowlist of data-* attributes used by registered macros. + "macro" => %w[class data-type data-classes data-page data-include-parent data-macro-name data-query-props data-pull-request-id data-pull-request-state], # mentions "mention" => %w[data-type data-text data-id class], # add styles to tables diff --git a/spec/lib/open_project/text_formatting/filters/macro_attribute_handling_spec.rb b/spec/lib/open_project/text_formatting/filters/macro_attribute_handling_spec.rb new file mode 100644 index 00000000000..ef846b2b775 --- /dev/null +++ b/spec/lib/open_project/text_formatting/filters/macro_attribute_handling_spec.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# 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. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require "spec_helper" + +RSpec.describe "macro element attribute handling" do # rubocop:disable RSpec/DescribeClass + def sanitize(html) + filter = OpenProject::TextFormatting::Filters::SanitizationFilter.new(html, {}) + result = filter.call + result.respond_to?(:to_html) ? result.to_html : result.to_s + end + + def apply_macro_filter(html) + filter = OpenProject::TextFormatting::Filters::MacroFilter.new(html, {}) + result = filter.call + result.respond_to?(:to_html) ? result.to_html : result.to_s + end + + describe OpenProject::TextFormatting::Filters::SanitizationFilter do + describe "macro element data attribute restrictions" do + it "strips data-controller from macro elements" do + html = '.' + expect(sanitize(html)).not_to include("data-controller") + end + + it "strips data-action from macro elements" do + html = '.' + expect(sanitize(html)).not_to include("data-action") + end + + it "strips arbitrary data-* stimulus value attributes from macro elements" do + html = '.' + output = sanitize(html) + expect(output).not_to include("data-poll-for-changes-url-value") + expect(output).not_to include("data-poll-for-changes-interval-value") + end + + it "strips data-controller from arbitrary non-macro elements" do + html = '

text

' + output = sanitize(html) + expect(output).not_to include("data-controller") + end + + it "preserves data-type on macro elements (used by create-work-package-link macro)" do + html = '.' + expect(sanitize(html)).to include('data-type="Task"') + end + + it "preserves data-page on macro elements (used by child-pages macro)" do + html = '.' + output = sanitize(html) + expect(output).to include('data-page="some-page"') + expect(output).to include('data-include-parent="true"') + end + + it "preserves data-macro-name on macro elements (used by placeholder rendering)" do + html = 'placeholder' + expect(sanitize(html)).to include('data-macro-name="toc"') + end + end + end + + describe OpenProject::TextFormatting::Filters::MacroFilter do + describe "unrecognized macro elements" do + it "replaces macro elements whose class does not match any registered macro with an unavailable placeholder" do + html = '

.

' + output = apply_macro_filter(html) + expect(output).not_to include('class="x"') + expect(output).to include("macro-unavailable") + expect(output).to include("Unknown or unsupported macro.") + end + + it "replaces macro elements with no class with an unavailable placeholder" do + html = "

.

" + output = apply_macro_filter(html) + expect(output).to include("macro-unavailable") + expect(output).to include("Unknown or unsupported macro.") + end + end + end +end