mirror of
https://gh.wpcy.net/https://github.com/discourse/discourse.git
synced 2026-05-01 11:47:16 +08:00
Graphviz can generate SVG output with anchor elements containing arbitrary URLs specified by users. This change implements server-side sanitization to allow only http and https URLs.
115 lines
3.7 KiB
Ruby
115 lines
3.7 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
# name: discourse-graphviz
|
|
# about: Provides the ability to add graphs to posts using the DOT language.
|
|
# meta_topic_id: 97554
|
|
# version: 0.0.1
|
|
# authors: Maja Komel, Joffrey Jaffeux
|
|
# url: https://github.com/discourse/discourse/tree/main/plugins/discourse-graphviz
|
|
|
|
enabled_site_setting :discourse_graphviz_enabled
|
|
|
|
register_svg_icon "diagram-project"
|
|
register_asset "stylesheets/common/graphviz.scss"
|
|
|
|
module ::DiscourseGraphviz
|
|
ALLOWED_URL_SCHEMES = %w[http https].freeze
|
|
|
|
def self.context
|
|
context = MiniRacer::Context.new
|
|
context.load("#{Rails.root}/plugins/discourse-graphviz/public/javascripts/viz-3.0.1.js")
|
|
context
|
|
end
|
|
|
|
def self.allowed_svg_xpath
|
|
@@allowed_svg_xpath ||=
|
|
"//*[#{UploadCreator::ALLOWED_SVG_ELEMENTS.map { |e| "name()!='#{e}'" }.join(" and ")}]"
|
|
end
|
|
|
|
def self.sanitize_svg_links(svg_node)
|
|
svg_node
|
|
.css("a")
|
|
.each do |anchor|
|
|
href = anchor["href"] || anchor["xlink:href"]
|
|
next unless href
|
|
|
|
begin
|
|
uri = URI.parse(href)
|
|
scheme = uri.scheme&.downcase
|
|
|
|
if scheme && !ALLOWED_URL_SCHEMES.include?(scheme)
|
|
anchor.replace(anchor.children)
|
|
elsif scheme.nil? && href.strip.match?(/\A\s*javascript:/i)
|
|
anchor.replace(anchor.children)
|
|
end
|
|
rescue URI::InvalidURIError
|
|
anchor.replace(anchor.children)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
after_initialize do
|
|
on(:before_post_process_cooked) do |doc, post|
|
|
if SiteSetting.discourse_graphviz_enabled
|
|
doc
|
|
.css("div.graphviz")
|
|
.each do |graph|
|
|
engine = graph.attribute("data-engine").value
|
|
svg_graph =
|
|
begin
|
|
DiscourseGraphviz.context.eval(
|
|
"vizRenderStringSync(#{graph.children[0].content.inspect}, {engine: '#{engine}'})",
|
|
)
|
|
rescue StandardError
|
|
nil
|
|
end
|
|
next if svg_graph.nil?
|
|
|
|
should_use_svg = SiteSetting.graphviz_default_svg
|
|
should_use_svg ||= graph.classes.include?("graphviz-svg")
|
|
should_use_svg &&= !graph.classes.include?("graphviz-no-svg")
|
|
|
|
if should_use_svg
|
|
# Changing to Nokogiri::HTML5.fragment returns `nil` for `.css('svg')`
|
|
# rubocop:todo Discourse/NoNokogiriHtmlFragment
|
|
new_graph_node = Nokogiri::HTML.fragment(svg_graph).css("svg").first
|
|
# rubocop:enable Discourse/NoNokogiriHtmlFragment
|
|
new_graph_node["class"] = "graphviz-svg-render"
|
|
DiscourseGraphviz.sanitize_svg_links(new_graph_node)
|
|
new_graph_node.xpath(DiscourseGraphviz.allowed_svg_xpath).remove
|
|
graph.replace new_graph_node
|
|
next
|
|
end
|
|
|
|
tmp_svg = Tempfile.new(%w[svgfile .svg])
|
|
tmp_png = Tempfile.new(%w[vizgraph- .png])
|
|
|
|
tmp_svg.write(svg_graph)
|
|
tmp_svg.rewind
|
|
|
|
graph_title =
|
|
Nokogiri
|
|
.parse(svg_graph)
|
|
.at("//comment()[contains(.,'Title')]")
|
|
&.content
|
|
&.match(/Title:\s(?<title>.+)\sPages:/)
|
|
&.[](:title)
|
|
filename = graph_title != "%0" ? graph_title : File.basename(tmp_png.path)
|
|
|
|
Discourse::Utils.execute_command("convert", "-density", "300", tmp_svg.path, tmp_png.path)
|
|
|
|
upload = UploadCreator.new(tmp_png, filename).create_for(-1)
|
|
|
|
# replace div.graphviz with image node
|
|
new_graph_node = Nokogiri::XML::Node.new("img", doc)
|
|
new_graph_node["src"] = upload.url
|
|
new_graph_node["alt"] = filename
|
|
graph.replace new_graph_node
|
|
|
|
tmp_svg.close!
|
|
tmp_png.close!
|
|
end
|
|
end
|
|
end
|
|
end
|