119 lines
4.4 KiB
Ruby
119 lines
4.4 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module Tournaments
|
|
class ComposeInviteEmail
|
|
LINK_TOKEN = "{{link_invito}}"
|
|
EXPIRY_TOKEN = "{{scadenza}}"
|
|
MAX_HTML_BYTES = 100_000
|
|
ALLOWED_TAGS = %w[p br strong b em i u ul ol li a img h2 h3 h4 span div blockquote].freeze
|
|
ALLOWED_ATTR = %w[href src alt width height style target rel data-invite-note].freeze
|
|
ALLOWED_STYLE = %w[
|
|
width height max-width min-width margin margin-top margin-bottom margin-left margin-right
|
|
padding padding-top padding-bottom padding-left padding-right
|
|
float display text-align color background background-color
|
|
font-weight font-size line-height text-decoration border-radius border
|
|
].freeze
|
|
|
|
def self.call(html:, invite_url:, expires_on: nil)
|
|
new(html: html, invite_url: invite_url, expires_on: expires_on).call
|
|
end
|
|
|
|
def self.sanitize_html(html)
|
|
new(html: html.to_s, invite_url: "").sanitize(html.to_s)
|
|
end
|
|
|
|
def self.default_html(tournament:, invited_by:)
|
|
intro = I18n.t(
|
|
"mailers.tournament_invite.body_html",
|
|
inviter: CGI.escapeHTML(invited_by.name.to_s),
|
|
tournament: CGI.escapeHTML(tournament.name.to_s),
|
|
club: CGI.escapeHTML(tournament.club.name.to_s),
|
|
assignment: I18n.t("tournaments.hub.invite_email_scope_generic")
|
|
)
|
|
cta = CGI.escapeHTML(I18n.t("mailers.tournament_invite.cta"))
|
|
<<~HTML
|
|
<p>#{CGI.escapeHTML(I18n.t("mailers.tournament_invite.hello"))}</p>
|
|
<p>#{intro}</p>
|
|
<p><a href="#{LINK_TOKEN}" style="display:inline-block;background:#e53935;color:#ffffff;text-decoration:none;padding:12px 18px;border-radius:8px;font-weight:600;">#{cta}</a></p>
|
|
<p>#{CGI.escapeHTML(I18n.t("mailers.tournament_invite.link_fallback"))}<br>#{LINK_TOKEN}</p>
|
|
<p>#{CGI.escapeHTML(I18n.t("mailers.tournament_invite.steps"))}</p>
|
|
<p>#{CGI.escapeHTML(I18n.t("mailers.tournament_invite.expiry", date: EXPIRY_TOKEN))}</p>
|
|
<p>#{CGI.escapeHTML(I18n.t("mailers.tournament_invite.ignore"))}</p>
|
|
HTML
|
|
end
|
|
|
|
def initialize(html:, invite_url:, expires_on: nil)
|
|
@html = html.to_s
|
|
@invite_url = invite_url.to_s
|
|
@expires_on = expires_on.to_s
|
|
end
|
|
|
|
def call
|
|
html = @html.dup
|
|
html.gsub!(LINK_TOKEN, @invite_url)
|
|
html.gsub!(EXPIRY_TOKEN, @expires_on) if @expires_on.present?
|
|
sanitized = sanitize(html)
|
|
sanitized += fallback_cta if @invite_url.present? && !sanitized.include?(@invite_url)
|
|
sanitized
|
|
end
|
|
|
|
def sanitize(html)
|
|
html = html.bytesize > MAX_HTML_BYTES ? html.byteslice(0, MAX_HTML_BYTES) : html
|
|
fragment = Loofah.fragment(html.to_s)
|
|
fragment.css("[data-invite-note]").each do |el|
|
|
el.remove if el.text.to_s.strip.blank?
|
|
end
|
|
scrubber = Rails::HTML::PermitScrubber.new
|
|
scrubber.tags = ALLOWED_TAGS
|
|
scrubber.attributes = ALLOWED_ATTR
|
|
fragment.scrub!(scrubber)
|
|
fragment.css("a").each { |node| scrub_url!(node, "href") }
|
|
fragment.css("img").each do |node|
|
|
scrub_url!(node, "src")
|
|
end
|
|
fragment.css("[style]").each { |node| scrub_style!(node) }
|
|
fragment.css("a[target='_blank']").each do |node|
|
|
node["rel"] = "noopener noreferrer"
|
|
end
|
|
fragment.to_s
|
|
end
|
|
|
|
private
|
|
|
|
def scrub_url!(node, attr)
|
|
url = node[attr].to_s.strip
|
|
return if url == LINK_TOKEN || url.include?(LINK_TOKEN)
|
|
return if url.match?(/\Ahttps?:\/\//i) || url.start_with?("/rails/active_storage")
|
|
|
|
if attr == "src"
|
|
node.remove
|
|
else
|
|
node.remove_attribute(attr)
|
|
end
|
|
end
|
|
|
|
def scrub_style!(node)
|
|
decls = node["style"].to_s.split(";").map(&:strip).reject(&:blank?)
|
|
kept = decls.select do |decl|
|
|
prop, value = decl.split(":", 2).map { |part| part.to_s.strip }
|
|
next false if prop.blank? || value.blank?
|
|
next false unless ALLOWED_STYLE.include?(prop.downcase)
|
|
next false if value.match?(/expression|javascript|url\s*\(/i)
|
|
|
|
true
|
|
end
|
|
if kept.any?
|
|
node["style"] = kept.join("; ")
|
|
else
|
|
node.remove_attribute("style")
|
|
end
|
|
end
|
|
|
|
def fallback_cta
|
|
label = CGI.escapeHTML(I18n.t("mailers.tournament_invite.cta"))
|
|
url = CGI.escapeHTML(@invite_url)
|
|
%(<p><a href="#{url}" style="display:inline-block;background:#e53935;color:#ffffff;text-decoration:none;padding:12px 18px;border-radius:8px;font-weight:600;">#{label}</a></p><p>#{url}</p>)
|
|
end
|
|
end
|
|
end
|