mirror of
https://github.com/redmine/redmine.git
synced 2026-02-14 02:27:56 +01:00
Refactors parse_hires_images and parse_inline_attachments methods from ApplicationHelper to scrubbers (#43745).
git-svn-id: https://svn.redmine.org/redmine/trunk@24406 e93f8b46-1217-0410-a6f0-8f06a7374b81
This commit is contained in:
@@ -914,7 +914,7 @@ 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 = only_path = options[:only_path] = (options[:only_path] != false)
|
||||
|
||||
text = text.dup
|
||||
macros = catch_macros(text)
|
||||
@@ -923,7 +923,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 = []
|
||||
@@ -932,7 +932,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
|
||||
@@ -977,54 +977,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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -65,8 +65,9 @@ module Redmine
|
||||
class Formatter
|
||||
include Redmine::WikiFormatting::SectionHelper
|
||||
|
||||
def initialize(text)
|
||||
def initialize(text, options = {})
|
||||
@text = text
|
||||
@options = options
|
||||
end
|
||||
|
||||
def to_html(*args)
|
||||
@@ -75,7 +76,7 @@ module Redmine
|
||||
SANITIZER.call(fragment)
|
||||
|
||||
scrubber = Loofah::Scrubber.new do |node|
|
||||
SCRUBBERS.each do |s|
|
||||
(SCRUBBERS + post_processor_scrubbers).each do |s|
|
||||
result = s.scrub(node)
|
||||
break result if result == Loofah::Scrubber::STOP
|
||||
break if node.parent.nil?
|
||||
@@ -85,6 +86,15 @@ module Redmine
|
||||
fragment.scrub!(scrubber)
|
||||
fragment.to_s
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def post_processor_scrubbers
|
||||
[
|
||||
Redmine::WikiFormatting::InlineAttachmentsScrubber.new(@options),
|
||||
Redmine::WikiFormatting::HiresImagesScrubber.new
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
41
lib/redmine/wiki_formatting/hires_images_scrubber.rb
Normal file
41
lib/redmine/wiki_formatting/hires_images_scrubber.rb
Normal file
@@ -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 = /@(?<dpr>\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
|
||||
79
lib/redmine/wiki_formatting/inline_attachments_scrubber.rb
Normal file
79
lib/redmine/wiki_formatting/inline_attachments_scrubber.rb
Normal file
@@ -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(?<filename>[^/"]+?\.(?: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
|
||||
@@ -32,8 +32,9 @@ 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)
|
||||
@@ -41,7 +42,7 @@ module Redmine
|
||||
fragment = Loofah.html5_fragment(html)
|
||||
|
||||
scrubber = Loofah::Scrubber.new do |node|
|
||||
SCRUBBERS.each do |s|
|
||||
(SCRUBBERS + post_processor_scrubbers).each do |s|
|
||||
result = s.scrub(node)
|
||||
break result if result == Loofah::Scrubber::STOP
|
||||
break if node.parent.nil?
|
||||
@@ -51,6 +52,15 @@ module Redmine
|
||||
fragment.scrub!(scrubber)
|
||||
fragment.to_s
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def post_processor_scrubbers
|
||||
[
|
||||
Redmine::WikiFormatting::InlineAttachmentsScrubber.new(@options),
|
||||
Redmine::WikiFormatting::HiresImagesScrubber.new
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
class Filter < RedCloth3
|
||||
|
||||
@@ -169,16 +169,16 @@ class ApplicationHelperTest < Redmine::HelperTest
|
||||
def test_attached_images
|
||||
to_test = {
|
||||
'Inline image: !logo.gif!' =>
|
||||
'Inline image: <img src="/attachments/download/3/logo.gif" title="This is a logo" alt="This is a logo" loading="lazy">',
|
||||
'Inline image: <img src="/attachments/download/3/logo.gif" alt="This is a logo" title="This is a logo" loading="lazy">',
|
||||
'Inline image: !logo.GIF!' =>
|
||||
'Inline image: <img src="/attachments/download/3/logo.gif" title="This is a logo" alt="This is a logo" loading="lazy">',
|
||||
'Inline image: <img src="/attachments/download/3/logo.gif" alt="This is a logo" title="This is a logo" loading="lazy">',
|
||||
'Inline WebP image: !logo.webp!' =>
|
||||
'Inline WebP image: <img src="/attachments/download/24/logo.webp" title="WebP image" alt="WebP image" loading="lazy">',
|
||||
'Inline WebP image: <img src="/attachments/download/24/logo.webp" alt="WebP image" title="WebP image" loading="lazy">',
|
||||
'No match: !ogo.gif!' => 'No match: <img src="ogo.gif" alt="">',
|
||||
'No match: !ogo.GIF!' => 'No match: <img src="ogo.GIF" alt="">',
|
||||
# link image
|
||||
'!logo.gif!:http://foo.bar/' =>
|
||||
'<a href="http://foo.bar/"><img src="/attachments/download/3/logo.gif" title="This is a logo" alt="This is a logo" loading="lazy"></a>',
|
||||
'<a href="http://foo.bar/"><img src="/attachments/download/3/logo.gif" alt="This is a logo" title="This is a logo" loading="lazy"></a>',
|
||||
}
|
||||
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[<img src=".+?" title="alt text" alt="alt text" loading=".+?" style="width:100px;">],
|
||||
assert_match %r[<img src=".+?" style="width:100px;" title="alt text" alt="alt text" loading=".+?">],
|
||||
textilizable('!{width:100px}logo.gif(alt text)!', attachments: attachments)
|
||||
|
||||
# When alt text is not set
|
||||
assert_match %r[<img src=".+?" title="This is a logo" alt="This is a logo" loading=".+?">],
|
||||
assert_match %r[<img src=".+?" alt="This is a logo" title="This is a logo" loading=".+?">],
|
||||
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(
|
||||
%(<p><img src="/attachments/download/#{attachment.id}/image@2x.png" ) +
|
||||
%(srcset="/attachments/download/#{attachment.id}/image@2x.png 2x" alt="" loading="lazy"></p>),
|
||||
%(alt="" loading="lazy" srcset="/attachments/download/#{attachment.id}/image@2x.png 2x"></p>),
|
||||
textilizable("!image@2x.png!", :attachments => [attachment])
|
||||
)
|
||||
end
|
||||
|
||||
@@ -58,7 +58,7 @@ class Redmine::WikiFormatting::CommonMark::ApplicationHelperTest < Redmine::Help
|
||||
textilizable('', attachments: attachments)
|
||||
|
||||
# When alt text is not set
|
||||
assert_match %r[<img src=".+?" title="This is a logo" alt="This is a logo" loading=".+?">],
|
||||
assert_match %r[<img src=".+?" alt="This is a logo" title="This is a logo" loading=".+?">],
|
||||
textilizable('', attachments: attachments)
|
||||
|
||||
# When alt text is not set and the attachment has no description
|
||||
|
||||
Reference in New Issue
Block a user