mirror of
https://github.com/redmine/redmine.git
synced 2025-11-10 15:26:03 +01:00
Replacing html-pipeline with Loofah for HTML Filtering (#42737).
Patch by Takashi Kato (user:tohosaku). git-svn-id: https://svn.redmine.org/redmine/trunk@24094 e93f8b46-1217-0410-a6f0-8f06a7374b81
This commit is contained in:
5
Gemfile
5
Gemfile
@@ -37,9 +37,8 @@ gem 'tzinfo-data', platforms: [:mingw, :x64_mingw, :mswin]
|
|||||||
gem 'rotp', '>= 5.0.0'
|
gem 'rotp', '>= 5.0.0'
|
||||||
gem 'rqrcode'
|
gem 'rqrcode'
|
||||||
|
|
||||||
# HTML pipeline and sanitization
|
# HTML sanitization
|
||||||
gem "html-pipeline", "~> 2.13.2"
|
gem "sanitize", "~> 7.0"
|
||||||
gem "sanitize", "~> 6.0"
|
|
||||||
|
|
||||||
# Triggering of Webhooks
|
# Triggering of Webhooks
|
||||||
gem "rest-client", "~> 2.1"
|
gem "rest-client", "~> 2.1"
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ module Redmine
|
|||||||
'important' => 'message-report',
|
'important' => 'message-report',
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
class AlertsIconsFilter < HTML::Pipeline::Filter
|
class AlertsIconsScrubber < Loofah::Scrubber
|
||||||
def call
|
def scrub(doc)
|
||||||
doc.search("p.markdown-alert-title").each do |node|
|
doc.search("p.markdown-alert-title").each do |node|
|
||||||
parent_node = node.parent
|
parent_node = node.parent
|
||||||
parent_class_attr = parent_node['class'] # e.g., "markdown-alert markdown-alert-note"
|
parent_class_attr = parent_node['class'] # e.g., "markdown-alert markdown-alert-note"
|
||||||
|
|||||||
@@ -24,19 +24,19 @@ module Redmine
|
|||||||
module CommonMark
|
module CommonMark
|
||||||
# adds class="external" to external links, and class="email" to mailto
|
# adds class="external" to external links, and class="email" to mailto
|
||||||
# links
|
# links
|
||||||
class ExternalLinksFilter < HTML::Pipeline::Filter
|
class ExternalLinksScrubber < Loofah::Scrubber
|
||||||
def call
|
def scrub(node)
|
||||||
doc.search("a").each do |node|
|
if node.name == 'a'
|
||||||
url = node["href"]
|
url = node["href"]
|
||||||
next unless url
|
return unless url
|
||||||
next if url.starts_with?("/") || url.starts_with?("#") || !url.include?(':')
|
return if url.starts_with?("/") || url.starts_with?("#") || !url.include?(':')
|
||||||
|
|
||||||
scheme = begin
|
scheme = begin
|
||||||
URI.parse(url).scheme
|
URI.parse(url).scheme
|
||||||
rescue
|
rescue
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
next if scheme.blank?
|
return if scheme.blank?
|
||||||
|
|
||||||
klass = node["class"].presence
|
klass = node["class"].presence
|
||||||
node["class"] = [
|
node["class"] = [
|
||||||
@@ -50,7 +50,6 @@ module Redmine
|
|||||||
node["rel"] = rel.join(" ")
|
node["rel"] = rel.join(" ")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
doc
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -26,15 +26,13 @@ module Redmine
|
|||||||
# @<a href="mailto:user@example.org">user@example.org</a>
|
# @<a href="mailto:user@example.org">user@example.org</a>
|
||||||
# - autolinked hi res image names that look like email addresses:
|
# - autolinked hi res image names that look like email addresses:
|
||||||
# <a href="mailto:printscreen@2x.png">printscreen@2x.png</a>
|
# <a href="mailto:printscreen@2x.png">printscreen@2x.png</a>
|
||||||
class FixupAutoLinksFilter < HTML::Pipeline::Filter
|
class FixupAutoLinksScrubber < Loofah::Scrubber
|
||||||
USER_LINK_PREFIX = /(@|user:)\z/
|
USER_LINK_PREFIX = /(@|user:)\z/
|
||||||
HIRES_IMAGE = /.+@\dx\.(bmp|gif|jpg|jpe|jpeg|png)\z/
|
HIRES_IMAGE = /.+@\dx\.(bmp|gif|jpg|jpe|jpeg|png)\z/
|
||||||
|
|
||||||
def call
|
def scrub(node)
|
||||||
doc.search("a").each do |node|
|
if node.name == 'a'
|
||||||
unless (url = node['href']) && url.starts_with?('mailto:')
|
return unless (url = node['href']) && url.starts_with?('mailto:')
|
||||||
next
|
|
||||||
end
|
|
||||||
|
|
||||||
if ((p = node.previous) && p.text? &&
|
if ((p = node.previous) && p.text? &&
|
||||||
p.text =~(USER_LINK_PREFIX)) ||
|
p.text =~(USER_LINK_PREFIX)) ||
|
||||||
@@ -43,7 +41,6 @@ module Redmine
|
|||||||
node.replace node.text
|
node.replace node.text
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
doc
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -17,8 +17,6 @@
|
|||||||
# along with this program; if not, write to the Free Software
|
# along with this program; if not, write to the Free Software
|
||||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
|
||||||
require 'html/pipeline'
|
|
||||||
|
|
||||||
module Redmine
|
module Redmine
|
||||||
module WikiFormatting
|
module WikiFormatting
|
||||||
module CommonMark
|
module CommonMark
|
||||||
@@ -54,14 +52,13 @@ module Redmine
|
|||||||
}.freeze,
|
}.freeze,
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
MarkdownPipeline = HTML::Pipeline.new [
|
SANITIZER = SanitizationFilter.new
|
||||||
MarkdownFilter,
|
SCRUBBERS = [
|
||||||
SanitizationFilter,
|
SyntaxHighlightScrubber.new,
|
||||||
SyntaxHighlightFilter,
|
FixupAutoLinksScrubber.new,
|
||||||
FixupAutoLinksFilter,
|
ExternalLinksScrubber.new,
|
||||||
ExternalLinksFilter,
|
AlertsIconsScrubber.new
|
||||||
AlertsIconsFilter
|
]
|
||||||
], PIPELINE_CONFIG
|
|
||||||
|
|
||||||
class Formatter
|
class Formatter
|
||||||
include Redmine::WikiFormatting::SectionHelper
|
include Redmine::WikiFormatting::SectionHelper
|
||||||
@@ -71,8 +68,13 @@ module Redmine
|
|||||||
end
|
end
|
||||||
|
|
||||||
def to_html(*args)
|
def to_html(*args)
|
||||||
result = MarkdownPipeline.call @text
|
html = MarkdownFilter.new(@text, PIPELINE_CONFIG).call
|
||||||
result[:output].to_s
|
fragment = Redmine::WikiFormatting::HtmlParser.parse(html)
|
||||||
|
SANITIZER.call(fragment)
|
||||||
|
SCRUBBERS.each do |scrubber|
|
||||||
|
fragment.scrub!(scrubber)
|
||||||
|
end
|
||||||
|
fragment.to_s
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -25,10 +25,12 @@ module Redmine
|
|||||||
# We do not use the stock HTML::Pipeline::MarkdownFilter because this
|
# We do not use the stock HTML::Pipeline::MarkdownFilter because this
|
||||||
# does not allow for straightforward configuration of render and parsing
|
# does not allow for straightforward configuration of render and parsing
|
||||||
# options
|
# options
|
||||||
class MarkdownFilter < HTML::Pipeline::TextFilter
|
class MarkdownFilter
|
||||||
def initialize(text, context = nil, result = nil)
|
attr_reader :context
|
||||||
super
|
|
||||||
@text = @text.delete "\r"
|
def initialize(text, context = nil)
|
||||||
|
@text = text.delete "\r"
|
||||||
|
@context = context
|
||||||
end
|
end
|
||||||
|
|
||||||
def call
|
def call
|
||||||
|
|||||||
@@ -21,13 +21,99 @@ module Redmine
|
|||||||
module WikiFormatting
|
module WikiFormatting
|
||||||
module CommonMark
|
module CommonMark
|
||||||
# sanitizes rendered HTML using the Sanitize gem
|
# sanitizes rendered HTML using the Sanitize gem
|
||||||
class SanitizationFilter < HTML::Pipeline::SanitizationFilter
|
class SanitizationFilter
|
||||||
include Redmine::Helpers::URL
|
include Redmine::Helpers::URL
|
||||||
RELAXED_PROTOCOL_ATTRS = {
|
|
||||||
"a" => %w(href).freeze,
|
|
||||||
}.freeze
|
|
||||||
|
|
||||||
ALLOWED_CSS_PROPERTIES = %w[
|
attr_accessor :allowlist
|
||||||
|
|
||||||
|
LISTS = Set.new(%w[ul ol].freeze)
|
||||||
|
LIST_ITEM = 'li'
|
||||||
|
|
||||||
|
# List of table child elements. These must be contained by a <table> element
|
||||||
|
# or they are not allowed through. Otherwise they can be used to break out
|
||||||
|
# of places we're using tables to contain formatted user content (like pull
|
||||||
|
# request review comments).
|
||||||
|
TABLE_ITEMS = Set.new(%w[tr td th].freeze)
|
||||||
|
TABLE = 'table'
|
||||||
|
TABLE_SECTIONS = Set.new(%w[thead tbody tfoot].freeze)
|
||||||
|
|
||||||
|
# The main sanitization allowlist. Only these elements and attributes are
|
||||||
|
# allowed through by default.
|
||||||
|
ALLOWLIST = {
|
||||||
|
:elements => %w[
|
||||||
|
h1 h2 h3 h4 h5 h6 br b i strong em a pre code img input tt u
|
||||||
|
div ins del sup sub p ol ul table thead tbody tfoot blockquote
|
||||||
|
dl dt dd kbd q samp var hr ruby rt rp li tr td th s strike summary
|
||||||
|
details caption figure figcaption
|
||||||
|
abbr bdo cite dfn mark small span time wbr
|
||||||
|
].freeze,
|
||||||
|
:remove_contents => ['script'].freeze,
|
||||||
|
:attributes => {
|
||||||
|
'a' => %w[href id name].freeze,
|
||||||
|
'img' => %w[src longdesc].freeze,
|
||||||
|
'code' => ['class'].freeze,
|
||||||
|
'div' => %w[class itemscope itemtype].freeze,
|
||||||
|
'li' => %w[id class].freeze,
|
||||||
|
'input' => %w[class type].freeze,
|
||||||
|
'p' => ['class'].freeze,
|
||||||
|
'ul' => ['class'].freeze,
|
||||||
|
'blockquote' => ['cite'].freeze,
|
||||||
|
'del' => ['cite'].freeze,
|
||||||
|
'ins' => ['cite'].freeze,
|
||||||
|
'q' => ['cite'].freeze,
|
||||||
|
:all => %w[
|
||||||
|
abbr accept accept-charset
|
||||||
|
accesskey action align alt
|
||||||
|
aria-describedby aria-hidden aria-label aria-labelledby
|
||||||
|
axis border cellpadding cellspacing char
|
||||||
|
charoff charset checked
|
||||||
|
clear cols colspan color
|
||||||
|
compact coords datetime dir
|
||||||
|
disabled enctype for frame
|
||||||
|
headers height hreflang
|
||||||
|
hspace ismap label lang
|
||||||
|
maxlength media method
|
||||||
|
multiple nohref noshade
|
||||||
|
nowrap open progress prompt readonly rel rev
|
||||||
|
role rows rowspan rules scope
|
||||||
|
selected shape size span
|
||||||
|
start style summary tabindex target
|
||||||
|
title type usemap valign value
|
||||||
|
vspace width itemprop
|
||||||
|
].freeze
|
||||||
|
}.freeze,
|
||||||
|
:protocols => {
|
||||||
|
'blockquote' => { 'cite' => ['http', 'https', :relative].freeze },
|
||||||
|
'del' => { 'cite' => ['http', 'https', :relative].freeze },
|
||||||
|
'ins' => { 'cite' => ['http', 'https', :relative].freeze },
|
||||||
|
'q' => { 'cite' => ['http', 'https', :relative].freeze },
|
||||||
|
'img' => {
|
||||||
|
'src' => ['http', 'https', :relative].freeze,
|
||||||
|
'longdesc' => ['http', 'https', :relative].freeze
|
||||||
|
}.freeze
|
||||||
|
},
|
||||||
|
:transformers => [
|
||||||
|
# Top-level <li> elements are removed because they can break out of
|
||||||
|
# containing markup.
|
||||||
|
lambda { |env|
|
||||||
|
name = env[:node_name]
|
||||||
|
node = env[:node]
|
||||||
|
if name == LIST_ITEM && node.ancestors.none? { |n| LISTS.include?(n.name) }
|
||||||
|
node.replace(node.children)
|
||||||
|
end
|
||||||
|
},
|
||||||
|
|
||||||
|
# Table child elements that are not contained by a <table> are removed.
|
||||||
|
lambda { |env|
|
||||||
|
name = env[:node_name]
|
||||||
|
node = env[:node]
|
||||||
|
if (TABLE_SECTIONS.include?(name) || TABLE_ITEMS.include?(name)) && node.ancestors.none? { |n| n.name == TABLE }
|
||||||
|
node.replace(node.children)
|
||||||
|
end
|
||||||
|
}
|
||||||
|
].freeze,
|
||||||
|
:css => {
|
||||||
|
:properties => %w[
|
||||||
color background-color
|
color background-color
|
||||||
width min-width max-width
|
width min-width max-width
|
||||||
height min-height max-height
|
height min-height max-height
|
||||||
@@ -38,26 +124,28 @@ module Redmine
|
|||||||
text-align
|
text-align
|
||||||
float
|
float
|
||||||
].freeze
|
].freeze
|
||||||
|
}
|
||||||
|
}.freeze
|
||||||
|
|
||||||
def allowlist
|
RELAXED_PROTOCOL_ATTRS = {
|
||||||
@allowlist ||= customize_allowlist(super.deep_dup)
|
"a" => %w(href).freeze,
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
def initialize
|
||||||
|
@allowlist = default_allowlist
|
||||||
|
add_transformers
|
||||||
|
end
|
||||||
|
|
||||||
|
def call(doc)
|
||||||
|
# Sanitize is applied to the whole document, so the API is different from loofeh's scrubber.
|
||||||
|
Sanitize.clean_node!(doc, allowlist)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
# customizes the allowlist defined in
|
def add_transformers
|
||||||
# https://github.com/jch/html-pipeline/blob/master/lib/html/pipeline/sanitization_filter.rb
|
|
||||||
def customize_allowlist(allowlist)
|
|
||||||
# Disallow `name` attribute globally, allow on `a`
|
|
||||||
allowlist[:attributes][:all].delete("name")
|
|
||||||
allowlist[:attributes]["a"].push("name")
|
|
||||||
|
|
||||||
allowlist[:attributes][:all].push("style")
|
|
||||||
allowlist[:css] = { properties: ALLOWED_CSS_PROPERTIES }
|
|
||||||
|
|
||||||
# allow class on code tags (this holds the language info from fenced
|
# allow class on code tags (this holds the language info from fenced
|
||||||
# code bocks and has the format language-foo)
|
# code bocks and has the format language-foo)
|
||||||
allowlist[:attributes]["code"] = %w(class)
|
|
||||||
allowlist[:transformers].push lambda {|env|
|
allowlist[:transformers].push lambda {|env|
|
||||||
node = env[:node]
|
node = env[:node]
|
||||||
return unless node.name == "code"
|
return unless node.name == "code"
|
||||||
@@ -70,8 +158,6 @@ module Redmine
|
|||||||
|
|
||||||
# Allow class on div and p tags only for alert blocks
|
# 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)
|
# (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|
|
allowlist[:transformers].push lambda {|env|
|
||||||
node = env[:node]
|
node = env[:node]
|
||||||
return unless node.element?
|
return unless node.element?
|
||||||
@@ -98,8 +184,6 @@ module Redmine
|
|||||||
# allowlist[:attributes]["td"] = %w(style)
|
# allowlist[:attributes]["td"] = %w(style)
|
||||||
# allowlist[:css] = { properties: ["text-align"] }
|
# allowlist[:css] = { properties: ["text-align"] }
|
||||||
|
|
||||||
# Allow `id` in a elements for footnotes
|
|
||||||
allowlist[:attributes]["a"].push "id"
|
|
||||||
# Remove any `id` property not matching for footnotes
|
# Remove any `id` property not matching for footnotes
|
||||||
allowlist[:transformers].push lambda {|env|
|
allowlist[:transformers].push lambda {|env|
|
||||||
node = env[:node]
|
node = env[:node]
|
||||||
@@ -112,7 +196,6 @@ module Redmine
|
|||||||
|
|
||||||
# allow `id` in li element for footnotes
|
# allow `id` in li element for footnotes
|
||||||
# allow `class` in li element for task list items
|
# allow `class` in li element for task list items
|
||||||
allowlist[:attributes]["li"] = %w(id class)
|
|
||||||
allowlist[:transformers].push lambda {|env|
|
allowlist[:transformers].push lambda {|env|
|
||||||
node = env[:node]
|
node = env[:node]
|
||||||
return unless node.name == "li"
|
return unless node.name == "li"
|
||||||
@@ -128,11 +211,8 @@ module Redmine
|
|||||||
|
|
||||||
# allow input type = "checkbox" with class "task-list-item-checkbox"
|
# allow input type = "checkbox" with class "task-list-item-checkbox"
|
||||||
# for task list items
|
# for task list items
|
||||||
allowlist[:elements].push('input')
|
|
||||||
allowlist[:attributes]["input"] = %w(class type)
|
|
||||||
allowlist[:transformers].push lambda {|env|
|
allowlist[:transformers].push lambda {|env|
|
||||||
node = env[:node]
|
node = env[:node]
|
||||||
|
|
||||||
return unless node.name == "input"
|
return unless node.name == "input"
|
||||||
return if node['type'] == "checkbox" && node['class'] == "task-list-item-checkbox"
|
return if node['type'] == "checkbox" && node['class'] == "task-list-item-checkbox"
|
||||||
|
|
||||||
@@ -140,10 +220,8 @@ module Redmine
|
|||||||
}
|
}
|
||||||
|
|
||||||
# allow class "contains-task-list" on ul for task list items
|
# allow class "contains-task-list" on ul for task list items
|
||||||
allowlist[:attributes]["ul"] = %w(class)
|
|
||||||
allowlist[:transformers].push lambda {|env|
|
allowlist[:transformers].push lambda {|env|
|
||||||
node = env[:node]
|
node = env[:node]
|
||||||
|
|
||||||
return unless node.name == "ul"
|
return unless node.name == "ul"
|
||||||
return if node["class"] == "contains-task-list"
|
return if node["class"] == "contains-task-list"
|
||||||
|
|
||||||
@@ -151,7 +229,6 @@ module Redmine
|
|||||||
}
|
}
|
||||||
|
|
||||||
# https://github.com/rgrove/sanitize/issues/209
|
# https://github.com/rgrove/sanitize/issues/209
|
||||||
allowlist[:protocols].delete("a")
|
|
||||||
allowlist[:transformers].push lambda {|env|
|
allowlist[:transformers].push lambda {|env|
|
||||||
node = env[:node]
|
node = env[:node]
|
||||||
return if node.type != Nokogiri::XML::Node::ELEMENT_NODE
|
return if node.type != Nokogiri::XML::Node::ELEMENT_NODE
|
||||||
@@ -168,11 +245,12 @@ module Redmine
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
|
end
|
||||||
|
|
||||||
# Allow `u` element to enable underline
|
# The allowlist to use when sanitizing. This can be passed in the context
|
||||||
allowlist[:elements].push('u')
|
# hash to the filter but defaults to ALLOWLIST constant value above.
|
||||||
|
def default_allowlist
|
||||||
allowlist
|
ALLOWLIST.deep_dup
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -22,11 +22,11 @@ module Redmine
|
|||||||
module CommonMark
|
module CommonMark
|
||||||
# Redmine Syntax highlighting for <pre><code class="language-foo">
|
# Redmine Syntax highlighting for <pre><code class="language-foo">
|
||||||
# blocks as generated by commonmarker
|
# blocks as generated by commonmarker
|
||||||
class SyntaxHighlightFilter < HTML::Pipeline::Filter
|
class SyntaxHighlightScrubber < Loofah::Scrubber
|
||||||
def call
|
def scrub(node)
|
||||||
doc.search("pre > code").each do |node|
|
if node.matches?("pre > code")
|
||||||
next unless lang = node["class"].presence
|
return unless lang = node["class"].presence
|
||||||
next unless lang =~ /\Alanguage-(\S+)\z/
|
return unless lang =~ /\Alanguage-(\S+)\z/
|
||||||
|
|
||||||
lang = $1
|
lang = $1
|
||||||
text = node.inner_text
|
text = node.inner_text
|
||||||
@@ -36,7 +36,7 @@ module Redmine
|
|||||||
|
|
||||||
if Redmine::SyntaxHighlighting.language_supported?(lang)
|
if Redmine::SyntaxHighlighting.language_supported?(lang)
|
||||||
html = Redmine::SyntaxHighlighting.highlight_by_language(text, lang)
|
html = Redmine::SyntaxHighlighting.highlight_by_language(text, lang)
|
||||||
next if html.nil?
|
return if html.nil?
|
||||||
|
|
||||||
node.inner_html = html
|
node.inner_html = html
|
||||||
node["class"] = "#{lang} syntaxhl"
|
node["class"] = "#{lang} syntaxhl"
|
||||||
@@ -45,7 +45,6 @@ module Redmine
|
|||||||
node.remove_attribute("class")
|
node.remove_attribute("class")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
doc
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ module Redmine
|
|||||||
'style' => ''
|
'style' => ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def self.parse(html)
|
||||||
|
Loofah.html5_fragment(html)
|
||||||
|
end
|
||||||
|
|
||||||
def self.to_text(html)
|
def self.to_text(html)
|
||||||
html = html.gsub(/[\n\r]/, ' ')
|
html = html.gsub(/[\n\r]/, ' ')
|
||||||
|
|
||||||
|
|||||||
@@ -19,17 +19,18 @@
|
|||||||
|
|
||||||
module Redmine
|
module Redmine
|
||||||
module WikiFormatting
|
module WikiFormatting
|
||||||
# Combination of SanitizationFilter and ExternalLinksFilter
|
# Combination of SanitizationFilter and ExternalLinksScrubber
|
||||||
class HtmlSanitizer
|
class HtmlSanitizer
|
||||||
Pipeline = HTML::Pipeline.new(
|
SANITIZER = Redmine::WikiFormatting::CommonMark::SanitizationFilter.new
|
||||||
[
|
SCRUBBERS = [Redmine::WikiFormatting::CommonMark::ExternalLinksScrubber.new]
|
||||||
Redmine::WikiFormatting::CommonMark::SanitizationFilter,
|
|
||||||
Redmine::WikiFormatting::CommonMark::ExternalLinksFilter,
|
|
||||||
], {})
|
|
||||||
|
|
||||||
def self.call(html)
|
def self.call(html)
|
||||||
result = Pipeline.call html
|
fragment = HtmlParser.parse(html)
|
||||||
result[:output].to_s
|
SANITIZER.call(fragment)
|
||||||
|
SCRUBBERS.each do |scrubber|
|
||||||
|
fragment.scrub!(scrubber)
|
||||||
|
end
|
||||||
|
fragment.to_s
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -20,17 +20,20 @@
|
|||||||
require_relative '../../../../../test_helper'
|
require_relative '../../../../../test_helper'
|
||||||
|
|
||||||
if Object.const_defined?(:Commonmarker)
|
if Object.const_defined?(:Commonmarker)
|
||||||
require 'redmine/wiki_formatting/common_mark/alerts_icons_filter'
|
require 'redmine/wiki_formatting/common_mark/alerts_icons_scrubber'
|
||||||
|
|
||||||
class Redmine::WikiFormatting::CommonMark::AlertsIconsFilterTest < ActiveSupport::TestCase
|
class Redmine::WikiFormatting::CommonMark::AlertsIconsFilterTest < ActiveSupport::TestCase
|
||||||
include Redmine::I18n
|
include Redmine::I18n
|
||||||
|
|
||||||
def format(markdown)
|
def format(markdown)
|
||||||
Redmine::WikiFormatting::CommonMark::MarkdownFilter.to_html(markdown, Redmine::WikiFormatting::CommonMark::PIPELINE_CONFIG)
|
Redmine::WikiFormatting::CommonMark::MarkdownFilter.new(markdown, Redmine::WikiFormatting::CommonMark::PIPELINE_CONFIG).call
|
||||||
end
|
end
|
||||||
|
|
||||||
def filter(html)
|
def filter(html)
|
||||||
Redmine::WikiFormatting::CommonMark::AlertsIconsFilter.to_html(html, @options)
|
fragment = Redmine::WikiFormatting::HtmlParser.parse(html)
|
||||||
|
scrubber = Redmine::WikiFormatting::CommonMark::AlertsIconsScrubber.new
|
||||||
|
fragment.scrub!(scrubber)
|
||||||
|
fragment.to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
def setup
|
def setup
|
||||||
|
|||||||
@@ -20,15 +20,13 @@
|
|||||||
require_relative '../../../../../test_helper'
|
require_relative '../../../../../test_helper'
|
||||||
|
|
||||||
if Object.const_defined?(:Commonmarker)
|
if Object.const_defined?(:Commonmarker)
|
||||||
require 'redmine/wiki_formatting/common_mark/external_links_filter'
|
|
||||||
|
|
||||||
class Redmine::WikiFormatting::CommonMark::ExternalLinksFilterTest < ActiveSupport::TestCase
|
class Redmine::WikiFormatting::CommonMark::ExternalFilterTest < ActiveSupport::TestCase
|
||||||
def filter(html)
|
def filter(html)
|
||||||
Redmine::WikiFormatting::CommonMark::ExternalLinksFilter.to_html(html, @options)
|
fragment = Redmine::WikiFormatting::HtmlParser.parse(html)
|
||||||
end
|
scrubber = Redmine::WikiFormatting::CommonMark::ExternalLinksScrubber.new
|
||||||
|
fragment.scrub!(scrubber)
|
||||||
def setup
|
fragment.to_s
|
||||||
@options = { }
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_external_links_should_have_external_css_class
|
def test_external_links_should_have_external_css_class
|
||||||
|
|||||||
@@ -20,19 +20,17 @@
|
|||||||
require_relative '../../../../../test_helper'
|
require_relative '../../../../../test_helper'
|
||||||
|
|
||||||
if Object.const_defined?(:Commonmarker)
|
if Object.const_defined?(:Commonmarker)
|
||||||
require 'redmine/wiki_formatting/common_mark/fixup_auto_links_filter'
|
|
||||||
|
|
||||||
class Redmine::WikiFormatting::CommonMark::FixupAutoLinksFilterTest < ActiveSupport::TestCase
|
class Redmine::WikiFormatting::CommonMark::FixupAutoLinksScrubberTest < ActiveSupport::TestCase
|
||||||
def filter(html)
|
def filter(html)
|
||||||
Redmine::WikiFormatting::CommonMark::FixupAutoLinksFilter.to_html(html, @options)
|
fragment = Redmine::WikiFormatting::HtmlParser.parse(html)
|
||||||
|
scrubber = Redmine::WikiFormatting::CommonMark::FixupAutoLinksScrubber.new
|
||||||
|
fragment.scrub!(scrubber)
|
||||||
|
fragment.to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
def format(markdown)
|
def format(markdown)
|
||||||
Redmine::WikiFormatting::CommonMark::MarkdownFilter.to_html(markdown, Redmine::WikiFormatting::CommonMark::PIPELINE_CONFIG)
|
Redmine::WikiFormatting::CommonMark::MarkdownFilter.new(markdown, Redmine::WikiFormatting::CommonMark::PIPELINE_CONFIG).call
|
||||||
end
|
|
||||||
|
|
||||||
def setup
|
|
||||||
@options = { }
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_should_fixup_autolinked_user_references
|
def test_should_fixup_autolinked_user_references
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ class Redmine::WikiFormatting::CommonMark::FormatterTest < ActionView::TestCase
|
|||||||
|
|
||||||
def test_should_support_html_tables
|
def test_should_support_html_tables
|
||||||
text = '<table style="background: red"><tr><td>Cell</td></tr></table>'
|
text = '<table style="background: red"><tr><td>Cell</td></tr></table>'
|
||||||
assert_equal '<table><tr><td>Cell</td></tr></table>', to_html(text)
|
assert_equal '<table><tbody><tr><td>Cell</td></tr></tbody></table>', to_html(text)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_should_remove_unsafe_uris
|
def test_should_remove_unsafe_uris
|
||||||
@@ -289,10 +289,10 @@ class Redmine::WikiFormatting::CommonMark::FormatterTest < ActionView::TestCase
|
|||||||
<p>Task list:</p>
|
<p>Task list:</p>
|
||||||
<ul class="contains-task-list">
|
<ul class="contains-task-list">
|
||||||
<li class="task-list-item">
|
<li class="task-list-item">
|
||||||
<input type="checkbox" class="task-list-item-checkbox" disabled> Task 1
|
<input type="checkbox" class="task-list-item-checkbox" disabled=""> Task 1
|
||||||
</li>
|
</li>
|
||||||
<li class="task-list-item">
|
<li class="task-list-item">
|
||||||
<input type="checkbox" class="task-list-item-checkbox" checked disabled> Task 2</li>
|
<input type="checkbox" class="task-list-item-checkbox" checked="" disabled=""> Task 2</li>
|
||||||
</ul>
|
</ul>
|
||||||
EXPECTED
|
EXPECTED
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,10 @@ if Object.const_defined?(:Commonmarker)
|
|||||||
|
|
||||||
class Redmine::WikiFormatting::CommonMark::MarkdownFilterTest < ActiveSupport::TestCase
|
class Redmine::WikiFormatting::CommonMark::MarkdownFilterTest < ActiveSupport::TestCase
|
||||||
def filter(markdown)
|
def filter(markdown)
|
||||||
Redmine::WikiFormatting::CommonMark::MarkdownFilter.to_html(markdown)
|
filter = Redmine::WikiFormatting::CommonMark::MarkdownFilter.new(
|
||||||
|
markdown,
|
||||||
|
Redmine::WikiFormatting::CommonMark::PIPELINE_CONFIG)
|
||||||
|
filter.call
|
||||||
end
|
end
|
||||||
|
|
||||||
# just a basic sanity test. more formatting tests in the formatter_test
|
# just a basic sanity test. more formatting tests in the formatter_test
|
||||||
|
|||||||
@@ -20,15 +20,13 @@
|
|||||||
require_relative '../../../../../test_helper'
|
require_relative '../../../../../test_helper'
|
||||||
|
|
||||||
if Object.const_defined?(:Commonmarker)
|
if Object.const_defined?(:Commonmarker)
|
||||||
require 'redmine/wiki_formatting/common_mark/sanitization_filter'
|
|
||||||
|
|
||||||
class Redmine::WikiFormatting::CommonMark::SanitizationFilterTest < ActiveSupport::TestCase
|
class Redmine::WikiFormatting::CommonMark::SanitizationFilterTest < ActiveSupport::TestCase
|
||||||
def filter(html)
|
def filter(html)
|
||||||
Redmine::WikiFormatting::CommonMark::SanitizationFilter.to_html(html, @options)
|
fragment = Redmine::WikiFormatting::HtmlParser.parse(html)
|
||||||
end
|
sanitizer = Redmine::WikiFormatting::CommonMark::SanitizationFilter.new
|
||||||
|
sanitizer.call(fragment)
|
||||||
def setup
|
fragment.to_s
|
||||||
@options = { }
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_should_filter_tags
|
def test_should_filter_tags
|
||||||
@@ -137,7 +135,7 @@ if Object.const_defined?(:Commonmarker)
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
'Lo<!-- comment -->rem</b> <a href=pants title="foo>ipsum <a href="http://foo.com/"><strong>dolor</a></strong> sit<br/>amet <script>alert("hello world");',
|
'Lo<!-- comment -->rem</b> <a href=pants title="foo>ipsum <a href="http://foo.com/"><strong>dolor</a></strong> sit<br/>amet <script>alert("hello world");',
|
||||||
'Lorem <a href="pants" title="foo>ipsum <a href="><strong>dolor</strong></a> sit<br>amet '
|
'Lorem <a href="pants" title="foo>ipsum <a href="><strong>dolor</strong></a> sit<br>amet '
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'<p>a</p><blockquote>b',
|
'<p>a</p><blockquote>b',
|
||||||
@@ -217,8 +215,7 @@ if Object.const_defined?(:Commonmarker)
|
|||||||
|
|
||||||
'protocol-based JS injection: null char' => [
|
'protocol-based JS injection: null char' => [
|
||||||
"<img src=java\0script:alert(\"XSS\")>",
|
"<img src=java\0script:alert(\"XSS\")>",
|
||||||
'<img src="java">'
|
'<img>'
|
||||||
# '<img>'
|
|
||||||
],
|
],
|
||||||
|
|
||||||
'protocol-based JS injection: invalid URL char' => [
|
'protocol-based JS injection: invalid URL char' => [
|
||||||
@@ -228,8 +225,7 @@ if Object.const_defined?(:Commonmarker)
|
|||||||
|
|
||||||
'protocol-based JS injection: spaces and entities' => [
|
'protocol-based JS injection: spaces and entities' => [
|
||||||
'<img src="  javascript:alert(\'XSS\');">',
|
'<img src="  javascript:alert(\'XSS\');">',
|
||||||
'<img src="">'
|
'<img>'
|
||||||
# '<img>'
|
|
||||||
],
|
],
|
||||||
|
|
||||||
'protocol whitespace' => [
|
'protocol whitespace' => [
|
||||||
|
|||||||
@@ -19,15 +19,13 @@
|
|||||||
|
|
||||||
require_relative '../../../../../test_helper'
|
require_relative '../../../../../test_helper'
|
||||||
if Object.const_defined?(:Commonmarker)
|
if Object.const_defined?(:Commonmarker)
|
||||||
require 'redmine/wiki_formatting/common_mark/syntax_highlight_filter'
|
|
||||||
|
|
||||||
class Redmine::WikiFormatting::CommonMark::SyntaxHighlightFilterTest < ActiveSupport::TestCase
|
class Redmine::WikiFormatting::CommonMark::SyntaxHighlightScrubberTest < ActiveSupport::TestCase
|
||||||
def filter(html)
|
def filter(html)
|
||||||
Redmine::WikiFormatting::CommonMark::SyntaxHighlightFilter.to_html(html, @options)
|
fragment = Redmine::WikiFormatting::HtmlParser.parse(html)
|
||||||
end
|
scrubber = Redmine::WikiFormatting::CommonMark::SyntaxHighlightScrubber.new
|
||||||
|
fragment.scrub!(scrubber)
|
||||||
def setup
|
fragment.to_s
|
||||||
@options = { }
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_should_highlight_supported_language
|
def test_should_highlight_supported_language
|
||||||
|
|||||||
Reference in New Issue
Block a user