Embed templates interpolate regex captures from user-authored article URLs into HTML attribute values. CommonMark's angle-bracket link destination syntax allows characters that the capture regexes don't filter, so the unescaped substitution could produce malformed attribute output. Escaping at substitution time keeps the render deterministic regardless of the URL. ### How was this tested? Added specs. Fixes [CW-6934](https://linear.app/chatwoot/issue/CW-6934/) Co-authored-by: Sony Mathew <sony@chatwoot.com> Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
98 lines
2.3 KiB
Ruby
98 lines
2.3 KiB
Ruby
class CustomMarkdownRenderer < CommonMarker::HtmlRenderer
|
|
CONFIG_PATH = Rails.root.join('config/markdown_embeds.yml')
|
|
|
|
def self.config
|
|
@config ||= YAML.load_file(CONFIG_PATH)
|
|
end
|
|
|
|
def self.embed_regexes
|
|
@embed_regexes ||= config.transform_values { |embed_config| Regexp.new(embed_config['regex']) }
|
|
end
|
|
|
|
def table(node)
|
|
out('<div class="tableWrapper">')
|
|
super
|
|
out('</div>')
|
|
end
|
|
|
|
def text(node)
|
|
content = node.string_content
|
|
|
|
if content.include?('^')
|
|
split_content = parse_sup(content)
|
|
out(split_content.join)
|
|
else
|
|
out(escape_html(content))
|
|
end
|
|
end
|
|
|
|
def link(node)
|
|
return if surrounded_by_empty_lines?(node) && render_embedded_content(node)
|
|
|
|
# If it's not a supported embed link, render normally
|
|
super
|
|
end
|
|
|
|
private
|
|
|
|
def surrounded_by_empty_lines?(node)
|
|
prev_node_empty?(node.previous) && next_node_empty?(node.next)
|
|
end
|
|
|
|
def prev_node_empty?(prev_node)
|
|
prev_node.nil? || node_empty?(prev_node)
|
|
end
|
|
|
|
def next_node_empty?(next_node)
|
|
next_node.nil? || node_empty?(next_node)
|
|
end
|
|
|
|
def node_empty?(node)
|
|
(node.type == :text && node.string_content.strip.empty?) || (node.type != :text)
|
|
end
|
|
|
|
def render_embedded_content(node)
|
|
link_url = node.url
|
|
embed_html = find_matching_embed(link_url)
|
|
|
|
return false unless embed_html
|
|
|
|
out(embed_html)
|
|
true
|
|
end
|
|
|
|
def find_matching_embed(link_url)
|
|
self.class.embed_regexes.each do |embed_key, regex|
|
|
match = link_url.match(regex)
|
|
next unless match
|
|
|
|
return render_embed_from_match(embed_key, match)
|
|
end
|
|
|
|
nil
|
|
end
|
|
|
|
def render_embed_from_match(embed_key, match_data)
|
|
embed_config = self.class.config[embed_key]
|
|
return nil unless embed_config
|
|
|
|
template = embed_config['template']
|
|
# Use gsub (not format) so CSS `%` values in templates don't need escaping.
|
|
# Captured values are HTML-escaped since they land inside HTML attribute contexts.
|
|
match_data.named_captures.each do |var_name, value|
|
|
template = template.gsub("%{#{var_name}}", CGI.escapeHTML(value))
|
|
end
|
|
template
|
|
end
|
|
|
|
def parse_sup(content)
|
|
content.split(/(\^[^\^]+\^)/).map do |segment|
|
|
if segment.start_with?('^') && segment.end_with?('^')
|
|
"<sup>#{escape_html(segment[1..-2])}</sup>"
|
|
else
|
|
escape_html(segment)
|
|
end
|
|
end
|
|
end
|
|
end
|