From bc29dbbbe761d0c12ff7ec1a14fa2315d09194f8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marius=20B=C4=82LTEANU?=
Date: Thu, 29 Jan 2026 14:22:40 +0700
Subject: [PATCH] Moves parse hires images and parse inline attachments to
ImagesScrubber.
---
app/helpers/application_helper.rb | 56 ++-----------
lib/redmine/wiki_formatting.rb | 7 +-
.../wiki_formatting/common_mark/formatter.rb | 14 +++-
.../wiki_formatting/hires_images_scrubber.rb | 41 ++++++++++
lib/redmine/wiki_formatting/html_sanitizer.rb | 5 +-
.../inline_attachments_scrubber.rb | 79 +++++++++++++++++++
.../wiki_formatting/textile/formatter.rb | 16 +++-
test/helpers/application_helper_test.rb | 14 ++--
.../common_mark/application_helper_test.rb | 2 +-
9 files changed, 166 insertions(+), 68 deletions(-)
create mode 100644 lib/redmine/wiki_formatting/hires_images_scrubber.rb
create mode 100644 lib/redmine/wiki_formatting/inline_attachments_scrubber.rb
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index aabb78410..3a5b961b5 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -913,7 +913,9 @@ module ApplicationHelper
return '' if text.blank?
project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
- @only_path = only_path = options.delete(:only_path) == false ? false : true
+ only_path = options.delete(:only_path) == false ? false : true
+ options[:only_path] = only_path
+ @only_path = only_path
text = text.dup
macros = catch_macros(text)
@@ -922,7 +924,7 @@ module ApplicationHelper
text = h(text)
else
formatting = Setting.text_formatting
- text = Redmine::WikiFormatting.to_html(formatting, text, :object => obj, :attribute => attr)
+ text = Redmine::WikiFormatting.to_html(formatting, text, options.merge(:object => obj, :attribute => attr, :view => self))
end
@parsed_headings = []
@@ -931,7 +933,7 @@ module ApplicationHelper
parse_sections(text, project, obj, attr, only_path, options)
text = parse_non_pre_blocks(text, obj, macros, options) do |txt|
- [:parse_inline_attachments, :parse_hires_images, :parse_wiki_links, :parse_redmine_links].each do |method_name|
+ [:parse_wiki_links, :parse_redmine_links].each do |method_name|
send method_name, txt, project, obj, attr, only_path, options
end
end
@@ -976,54 +978,6 @@ module ApplicationHelper
parsed
end
- # add srcset attribute to img tags if filename includes @2x, @3x, etc.
- # to support hires displays
- def parse_hires_images(text, project, obj, attr, only_path, options)
- text.gsub!(/src="([^"]+@(\dx)\.(bmp|gif|jpg|jpe|jpeg|png))"/i) do |m|
- filename, dpr = $1, $2
- m + " srcset=\"#{filename} #{dpr}\""
- end
- end
-
- def parse_inline_attachments(text, project, obj, attr, only_path, options)
- return if options[:inline_attachments] == false
-
- # when using an image link, try to use an attachment, if possible
- attachments = options[:attachments] || []
- if obj.is_a?(Journal)
- attachments += obj.journalized.attachments if obj.journalized.respond_to?(:attachments)
- else
- attachments += obj.attachments if obj.respond_to?(:attachments)
- end
- if attachments.present?
- title_and_alt_re = /\s+(title|alt)="([^"]*)"/i
-
- text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png|webp))"([^>]*)/i) do |m|
- filename, ext, other_attrs = $1, $2, $3
-
- # search for the picture in attachments
- if found = Attachment.latest_attach(attachments, CGI.unescape(filename))
- image_url = download_named_attachment_url(found, found.filename, :only_path => only_path)
- desc = found.description.to_s.delete('"')
-
- # remove title and alt attributes after extracting them
- title_and_alt = other_attrs.scan(title_and_alt_re).to_h
- other_attrs.gsub!(title_and_alt_re, '')
-
- title_and_alt_attrs = if !desc.blank? && title_and_alt['alt'].blank?
- " title=\"#{desc}\" alt=\"#{desc}\""
- else
- # restore original title and alt attributes
- " #{title_and_alt.map { |k, v| %[#{k}="#{v}"] }.join(' ')}"
- end
- "src=\"#{image_url}\"#{title_and_alt_attrs} loading=\"lazy\"#{other_attrs}"
- else
- m
- end
- end
- end
- end
-
# Wiki links
#
# Examples:
diff --git a/lib/redmine/wiki_formatting.rb b/lib/redmine/wiki_formatting.rb
index ce6097646..3d61df87d 100644
--- a/lib/redmine/wiki_formatting.rb
+++ b/lib/redmine/wiki_formatting.rb
@@ -93,10 +93,10 @@ module Redmine
# Text retrieved from the cache store may be frozen
# We need to dup it so we can do in-place substitutions with gsub!
cache_store.fetch cache_key do
- formatter_for(format).new(text).to_html
+ formatter_for(format).new(text, options).to_html
end.dup
else
- formatter_for(format).new(text).to_html
+ formatter_for(format).new(text, options).to_html
end
text
end
@@ -127,8 +127,9 @@ module Redmine
include ActionView::Helpers::UrlHelper
include Redmine::WikiFormatting::LinksHelper
- def initialize(text)
+ def initialize(text, options = {})
@text = text
+ @options = options
end
def to_html(*args)
diff --git a/lib/redmine/wiki_formatting/common_mark/formatter.rb b/lib/redmine/wiki_formatting/common_mark/formatter.rb
index 9ef52692e..f9e22467d 100644
--- a/lib/redmine/wiki_formatting/common_mark/formatter.rb
+++ b/lib/redmine/wiki_formatting/common_mark/formatter.rb
@@ -65,19 +65,29 @@ module Redmine
class Formatter
include Redmine::WikiFormatting::SectionHelper
- def initialize(text)
+ def initialize(text, options = {})
@text = text
+ @options = options
end
def to_html(*args)
html = MarkdownFilter.new(@text, PIPELINE_CONFIG).call
fragment = Redmine::WikiFormatting::HtmlParser.parse(html)
SANITIZER.call(fragment)
- SCRUBBERS.each do |scrubber|
+ (SCRUBBERS + post_processor_scrubbers).each do |scrubber|
fragment.scrub!(scrubber)
end
fragment.to_s
end
+
+ private
+
+ def post_processor_scrubbers
+ [
+ Redmine::WikiFormatting::InlineAttachmentsScrubber.new(@options),
+ Redmine::WikiFormatting::HiresImagesScrubber.new
+ ]
+ end
end
end
end
diff --git a/lib/redmine/wiki_formatting/hires_images_scrubber.rb b/lib/redmine/wiki_formatting/hires_images_scrubber.rb
new file mode 100644
index 000000000..b483497d4
--- /dev/null
+++ b/lib/redmine/wiki_formatting/hires_images_scrubber.rb
@@ -0,0 +1,41 @@
+# 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
+ class HiresImagesScrubber < Loofah::Scrubber
+ HIRES_FILENAME_REGEX = /@(?\dx)\.(?:bmp|gif|jpg|jpe|jpeg|png)\z/i
+
+ def scrub(node)
+ return unless node.name == 'img' && node['src'].present?
+
+ src = node['src']
+
+ return unless src.include?('@')
+
+ match = src.match(HIRES_FILENAME_REGEX)
+
+ return unless match
+
+ # Set the srcset attribute.
+ node['srcset'] = "#{src} #{match[:dpr]}"
+ end
+ end
+ end
+end
diff --git a/lib/redmine/wiki_formatting/html_sanitizer.rb b/lib/redmine/wiki_formatting/html_sanitizer.rb
index d818687b0..b0f608c56 100644
--- a/lib/redmine/wiki_formatting/html_sanitizer.rb
+++ b/lib/redmine/wiki_formatting/html_sanitizer.rb
@@ -22,7 +22,10 @@ module Redmine
# Combination of SanitizationFilter and ExternalLinksScrubber
class HtmlSanitizer
SANITIZER = Redmine::WikiFormatting::CommonMark::SanitizationFilter.new
- SCRUBBERS = [Redmine::WikiFormatting::CommonMark::ExternalLinksScrubber.new]
+ SCRUBBERS = [
+ Redmine::WikiFormatting::CommonMark::ExternalLinksScrubber.new,
+ Redmine::WikiFormatting::HiresImagesScrubber.new
+ ]
def self.call(html)
fragment = HtmlParser.parse(html)
diff --git a/lib/redmine/wiki_formatting/inline_attachments_scrubber.rb b/lib/redmine/wiki_formatting/inline_attachments_scrubber.rb
new file mode 100644
index 000000000..2da380df7
--- /dev/null
+++ b/lib/redmine/wiki_formatting/inline_attachments_scrubber.rb
@@ -0,0 +1,79 @@
+# 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
+ class InlineAttachmentsScrubber < Loofah::Scrubber
+ def initialize(options = {})
+ super()
+ @options = options
+ @obj = options[:object]
+ @view = options[:view]
+ @only_path = options[:only_path]
+ @attachments = options[:attachments] || []
+ if @obj.is_a?(Journal)
+ @attachments += @obj.journalized.attachments if @obj.journalized.respond_to?(:attachments)
+ elsif @obj.respond_to?(:attachments)
+ @attachments += @obj.attachments
+ end
+
+ if @attachments.present?
+ @attachments = @attachments.sort_by{|attachment| [attachment.created_on, attachment.id]}.reverse
+ end
+ end
+
+ def scrub(node)
+ return unless node.name == 'img' && node['src'].present?
+
+ parse_inline_attachments(node)
+ end
+
+ private
+
+ def parse_inline_attachments(node)
+ return if @attachments.blank?
+
+ src = node['src']
+
+ if src =~ %r{\A(?[^/"]+?\.(?:bmp|gif|jpg|jpeg|jpe|png|webp))\z}i
+ filename = $~[:filename]
+ if found = find_attachment(CGI.unescape(filename))
+ image_url = @view.download_named_attachment_url(found, found.filename, :only_path => @only_path)
+ node['src'] = image_url
+
+ desc = found.description.to_s.delete('"')
+ if !desc.blank? && node['alt'].blank?
+ node['title'] = desc
+ node['alt'] = desc
+ end
+ node['loading'] = 'lazy'
+ end
+ end
+ end
+
+ def find_attachment(filename)
+ return unless filename.valid_encoding?
+
+ @attachments.detect do |att|
+ filename.casecmp?(att.filename)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/redmine/wiki_formatting/textile/formatter.rb b/lib/redmine/wiki_formatting/textile/formatter.rb
index 57d8dbab4..0e7807987 100644
--- a/lib/redmine/wiki_formatting/textile/formatter.rb
+++ b/lib/redmine/wiki_formatting/textile/formatter.rb
@@ -32,18 +32,28 @@ module Redmine
extend Forwardable
def_delegators :@filter, :extract_sections, :rip_offtags
- def initialize(args)
- @filter = Filter.new(args)
+ def initialize(text, options = {})
+ @filter = Filter.new(text)
+ @options = options
end
def to_html(*rules)
html = @filter.to_html(rules)
fragment = Loofah.html5_fragment(html)
- SCRUBBERS.each do |scrubber|
+ (SCRUBBERS + post_processor_scrubbers).each do |scrubber|
fragment.scrub!(scrubber)
end
fragment.to_s
end
+
+ private
+
+ def post_processor_scrubbers
+ [
+ Redmine::WikiFormatting::InlineAttachmentsScrubber.new(@options),
+ Redmine::WikiFormatting::HiresImagesScrubber.new
+ ]
+ end
end
class Filter < RedCloth3
diff --git a/test/helpers/application_helper_test.rb b/test/helpers/application_helper_test.rb
index 87b57c7f2..9674a1407 100644
--- a/test/helpers/application_helper_test.rb
+++ b/test/helpers/application_helper_test.rb
@@ -169,16 +169,16 @@ class ApplicationHelperTest < Redmine::HelperTest
def test_attached_images
to_test = {
'Inline image: !logo.gif!' =>
- 'Inline image:
',
+ 'Inline image:
',
'Inline image: !logo.GIF!' =>
- 'Inline image:
',
+ 'Inline image:
',
'Inline WebP image: !logo.webp!' =>
- 'Inline WebP image:
',
+ 'Inline WebP image:
',
'No match: !ogo.gif!' => 'No match:
',
'No match: !ogo.GIF!' => 'No match:
',
# link image
'!logo.gif!:http://foo.bar/' =>
- '
',
+ '
',
}
attachments = Attachment.all
with_settings :text_formatting => 'textile' do
@@ -194,11 +194,11 @@ class ApplicationHelperTest < Redmine::HelperTest
textilizable('!logo.gif(alt text)!', attachments: attachments)
# When alt text and style are set
- assert_match %r[
],
+ assert_match %r[
],
textilizable('!{width:100px}logo.gif(alt text)!', attachments: attachments)
# When alt text is not set
- assert_match %r[
],
+ assert_match %r[
],
textilizable('!logo.gif!', attachments: attachments)
# When alt text is not set and the attachment has no description
@@ -272,7 +272,7 @@ class ApplicationHelperTest < Redmine::HelperTest
with_settings :text_formatting => 'textile' do
assert_equal(
%(
),
+ %(alt="" loading="lazy" srcset="/attachments/download/#{attachment.id}/image@2x.png 2x">
),
textilizable("!image@2x.png!", :attachments => [attachment])
)
end
diff --git a/test/unit/lib/redmine/wiki_formatting/common_mark/application_helper_test.rb b/test/unit/lib/redmine/wiki_formatting/common_mark/application_helper_test.rb
index 4ecc3e62a..21aaf79c4 100644
--- a/test/unit/lib/redmine/wiki_formatting/common_mark/application_helper_test.rb
+++ b/test/unit/lib/redmine/wiki_formatting/common_mark/application_helper_test.rb
@@ -58,7 +58,7 @@ class Redmine::WikiFormatting::CommonMark::ApplicationHelperTest < Redmine::Help
textilizable('', attachments: attachments)
# When alt text is not set
- assert_match %r[
],
+ assert_match %r[
],
textilizable('', attachments: attachments)
# When alt text is not set and the attachment has no description
--
2.50.1 (Apple Git-155)