discourse/lib/email/styles.rb

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

570 lines
19 KiB
Ruby
Raw Normal View History

# frozen_string_literal: true
2013-06-10 15:33:37 -04:00
#
# HTML emails don't support CSS, so we can use nokogiri to inline attributes based on
# matchers.
#
module Email
class Styles
MAX_IMAGE_DIMENSION = 400
ONEBOX_IMAGE_BASE_STYLE =
"max-height: 80%; max-width: 20%; height: auto; float: left; margin-right: 10px;"
ONEBOX_IMAGE_THUMBNAIL_STYLE = "width: 60px;"
ONEBOX_INLINE_AVATAR_STYLE = "width: 20px; height: 20px; float: none; vertical-align: middle;"
@@plugin_callbacks = []
2013-06-10 15:33:37 -04:00
attr_accessor :fragment
delegate :css, to: :fragment
def initialize(html, opts = nil)
2013-06-10 15:33:37 -04:00
@html = html
@opts = opts || {}
@fragment = Nokogiri::HTML5.parse(@html)
@custom_styles = nil
2013-06-10 15:33:37 -04:00
end
def self.register_plugin_style(&block)
@@plugin_callbacks.push(block)
end
def add_styles(node, new_styles)
existing = node["style"]
if existing.present?
# merge styles
node["style"] = "#{existing}; #{new_styles}"
else
node["style"] = new_styles
end
end
def custom_styles
return @custom_styles unless @custom_styles.nil?
css = EmailStyle.new.compiled_css
@custom_styles = {}
if !css.blank?
# there is a minor race condition here, CssParser could be
# loaded by ::CssParser::Parser not loaded
require "css_parser" unless defined?(::CssParser::Parser)
parser = ::CssParser::Parser.new(import: false)
parser.load_string!(css)
parser.each_selector do |selector, value|
@custom_styles[selector] ||= +""
@custom_styles[selector] << value
end
end
@custom_styles
end
def format_basic
uri = URI(Discourse.base_url)
# Remove SVGs
@fragment.css('svg, img[src$=".svg"]').remove
# images
@fragment
.css("img")
.each do |img|
next if img["class"] == "site-logo"
if (img["class"] && img["class"]["emoji"]) || (img["src"] && img["src"][%r{/_?emoji/}])
img["width"] = img["height"] = 20
else
# use dimensions of original iPhone screen for 'too big, let device rescale'
if img["width"].to_i > (320) || img["height"].to_i > (480)
img["width"] = img["height"] = "auto"
end
end
if img["src"]
# ensure all urls are absolute
img["src"] = "#{Discourse.base_url}#{img["src"]}" if img["src"][%r{\A/[^/]}]
# ensure no schemaless urls
img["src"] = "#{uri.scheme}:#{img["src"]}" if img["src"][%r{\A//}]
end
2013-08-26 18:08:38 -04:00
end
# add max-width to big images
big_images =
@fragment.css('img[width="auto"][height="auto"]') - @fragment.css("aside.onebox img") -
@fragment.css("img.site-logo, img.emoji")
big_images.each { |img| add_styles(img, "max-width: 100%;") if img["style"] !~ /max-width/ }
# topic featured link
@fragment
.css("a.topic-featured-link")
.each do |e|
e[
"style"
] = "color:#858585;padding:2px 8px;border:1px solid #e6e6e6;border-radius:2px;box-shadow:0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);"
end
# attachments
@fragment
.css("a.attachment")
.each do |a|
# ensure all urls are absolute
a["href"] = "#{Discourse.base_url}#{a["href"]}" if a["href"] =~ %r{\A/[^/]}
# ensure no schemaless urls
a["href"] = "#{uri.scheme}:#{a["href"]}" if a["href"] && a["href"].starts_with?("//")
end
end
def onebox_styles
# Links to other topics
2017-01-02 03:03:03 -05:00
style("aside.quote", "padding: 12px 25px 2px 12px; margin-bottom: 10px;")
style("aside.quote div.info-line", "color: #666; margin: 10px 0")
style(
"aside.quote .avatar",
"margin-right: 5px; width:20px; height:20px; vertical-align:middle;",
)
style("aside.quote", "border-left: 5px solid #e9e9e9; background-color: #f8f8f8; margin: 0;")
style(
"blockquote",
"border-left: 5px solid #e9e9e9; background-color: #f8f8f8; margin-left: 0; padding: 12px;",
)
# Oneboxes
style(
"aside.onebox",
"border: 5px solid #e9e9e9; padding: 12px 25px 12px 12px; margin-bottom: 10px;",
)
style("aside.onebox header img.site-icon", "width: 16px; height: 16px; margin-right: 3px;")
2016-12-05 06:00:04 -05:00
style("aside.onebox header a[href]", "color: #222222; text-decoration: none;")
style("aside.onebox .onebox-body", "clear: both")
style("aside.onebox .onebox-body img:not(.onebox-avatar-inline)", ONEBOX_IMAGE_BASE_STYLE)
style("aside.onebox .onebox-body img.thumbnail", ONEBOX_IMAGE_THUMBNAIL_STYLE)
2016-12-05 06:00:04 -05:00
style(
"aside.onebox .onebox-body h3, aside.onebox .onebox-body h4",
"font-size: 1.17em; margin: 10px 0;",
)
style(".onebox-metadata", "color: #919191")
style(".github-info", "margin-top: 10px;")
style(".github-info .added", "color: #090;")
style(".github-info .removed", "color: #e45735;")
style(".github-info div", "display: inline; margin-right: 10px;")
style(".github-icon-container", "float: left;")
style(".github-icon-container *", "fill: #646464; width: 40px; height: 40px;")
style(
".github-body-container",
'font-family: Consolas, Menlo, Monaco, "Lucida Console", "Liberation Mono", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Courier New", monospace; margin-top: 1em !important;',
)
style(".onebox-avatar-inline", ONEBOX_INLINE_AVATAR_STYLE)
@fragment.css(".github-body-container .excerpt").remove
2017-01-02 03:03:03 -05:00
@fragment.css("aside.quote blockquote > p").each { |p| p["style"] = "padding: 0;" }
# Convert all `aside.quote` tags to `blockquote`s
@fragment
.css("aside.quote")
.each do |n|
original_node = n.dup
original_node.search("div.quote-controls").remove
blockquote =
(
2017-01-02 03:03:03 -05:00
if original_node.css("blockquote").inner_html.strip.start_with?("<p")
original_node.css("blockquote").inner_html
else
2017-01-02 03:03:03 -05:00
"<p style='padding: 0;'>#{original_node.css("blockquote").inner_html}</p>"
end
)
2017-01-02 03:03:03 -05:00
n.inner_html = original_node.css("div.title").inner_html + blockquote
n.name = "blockquote"
end
# Finally, convert all `aside` tags to `div`s
@fragment.css("aside, article, header").each { |n| n.name = "div" }
# iframes can't go in emails, so replace them with clickable links
@fragment
.css("iframe")
.each do |i|
begin
# sometimes, iframes are blocklisted...
if i["src"].blank?
i.remove
next
end
src_uri =
i["data-original-href"].present? ? URI(i["data-original-href"]) : URI(i["src"])
# If an iframe is protocol relative, use SSL when displaying it
display_src =
"#{src_uri.scheme || "https"}://#{src_uri.host}#{src_uri.path}#{src_uri.query.nil? ? "" : "?" + src_uri.query}#{src_uri.fragment.nil? ? "" : "#" + src_uri.fragment}"
i.replace(
Nokogiri::HTML5.fragment(
"<p><a href='#{src_uri}'>#{CGI.escapeHTML(display_src)}</a><p>",
),
)
rescue URI::Error
# If the URL is weird, remove the iframe
i.remove
end
end
end
def format_html
2019-07-30 16:46:20 -04:00
correct_first_body_margin
correct_footer_style
correct_footer_style_highlight_first
strip_hashtag_link_icons
2019-07-30 16:46:20 -04:00
reset_tables
html_lang = SiteSetting.default_locale.sub("_", "-")
style("html", nil, :lang => html_lang, "xml:lang" => html_lang)
style("body", "line-height: 1.4; text-align:#{Rtl.new(nil).enabled? ? "right" : "left"};")
style("body", nil, dir: Rtl.new(nil).enabled? ? "rtl" : "ltr")
style(
".with-dir",
"text-align:#{Rtl.new(nil).enabled? ? "right" : "left"};",
dir: Rtl.new(nil).enabled? ? "rtl" : "ltr",
)
style("blockquote > :first-child", "margin-top: 0;")
style("blockquote > :last-child", "margin-bottom: 0;")
style("blockquote > p", "padding: 0;")
style(
".with-accent-colors",
"background-color: #{SiteSetting.email_accent_bg_color}; color: #{SiteSetting.email_accent_fg_color};",
)
style("h4", "color: #222;")
style("h3", "margin: 30px 0 10px;")
style("hr", "background-color: #ddd; height: 1px; border: 1px;")
style(
"a",
"text-decoration: none; font-weight: bold; color: #{SiteSetting.email_link_color};",
)
style("ul", "margin: 0 0 0 10px; padding: 0 0 0 20px;")
style("li", "padding-bottom: 10px")
2019-07-30 16:46:20 -04:00
style("div.summary-footer", "color:#666; font-size:95%; text-align:center; padding-top:15px;")
style("span.post-count", "margin: 0 5px; color: #777;")
style("pre", "word-wrap: break-word; max-width: 694px;")
style("code", "background-color: #f9f9f9; padding: 2px 5px;")
style("pre code", "display: block; background-color: #f9f9f9; overflow: auto; padding: 5px;")
style("pre.onebox code", "white-space: normal;")
style("pre code li", "white-space: pre;")
style(
".featured-topic a",
"text-decoration: none; font-weight: bold; color: #{SiteSetting.email_link_color}; line-height:1.5em;",
)
style(
".summary-email",
"-moz-box-sizing:border-box;-ms-text-size-adjust:100%;-webkit-box-sizing:border-box;-webkit-text-size-adjust:100%;box-sizing:border-box;color:#0a0a0a;font-family:Arial,sans-serif;font-size:14px;font-weight:400;line-height:1.3;margin:0;min-width:100%;padding:0;width:100%",
)
style(".previous-discussion", "font-size: 17px; color: #444; margin-bottom:10px;")
style(
".notification-date",
2019-07-30 16:46:20 -04:00
"text-align:right;color:#999999;padding-right:5px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;font-size:11px",
)
style(
".username",
"font-size:13px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;text-decoration:none;font-weight:bold",
)
style(".username-link", "color:#{SiteSetting.email_link_color};")
style(".username-title", "color:#777;margin-left:5px;")
style(
".user-title",
"font-size:13px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;text-decoration:none;margin-left:5px;color: #999;",
)
style(".post-wrapper", "margin-bottom:25px;")
style(".user-avatar", "vertical-align:top;width:55px;")
style(".user-avatar img", nil, width: "45", height: "45")
style("hr", "background-color: #ddd; height: 1px; border: 1px;")
style(".rtl", "direction: rtl;")
style("div.body", "padding-top:5px;")
style(".whisper div.body", "font-style: italic; color: #9c9c9c;")
style(".lightbox-wrapper .meta", "display: none")
style("div.undecorated-link-footer a", "font-weight: normal;")
style(
".mso-accent-link",
"mso-border-alt: 6px solid #{SiteSetting.email_accent_bg_color}; background-color: #{SiteSetting.email_accent_bg_color};",
)
style(
".reply-above-line",
"font-size: 10px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;color: #b5b5b5;padding: 5px 0px 20px;border-top: 1px dotted #ddd;",
)
2014-01-22 15:30:30 -05:00
onebox_styles
plugin_styles
dark_mode_styles
style(".post-excerpt img", "max-width: 50%; max-height: #{MAX_IMAGE_DIMENSION}px;")
format_custom
end
def format_custom
custom_styles.each { |selector, value| style(selector, value) }
end
# this method is reserved for styles specific to plugin
def plugin_styles
@@plugin_callbacks.each { |block| block.call(@fragment, @opts) }
end
2013-06-10 15:33:37 -04:00
def stripped_media
@stripped_media ||=
@fragment.css("[data-stripped-secure-media], [data-stripped-secure-upload]")
end
FEATURE: Allow email image embed with secure media (#10563) This PR introduces a few important changes to secure media redaction in emails. First of all, two new site settings have been introduced: * `secure_media_allow_embed_images_in_emails`: If enabled we will embed secure images in emails instead of redacting them. * `secure_media_max_email_embed_image_size_kb`: The cap to the size of the secure image we will embed, defaulting to 1mb, so the email does not become too big. Max is 10mb. Works in tandem with `email_total_attachment_size_limit_kb`. `Email::Sender` will now attach images to the email based on these settings. The sender will also call `inline_secure_images` in `Email::Styles` after secure media is redacted and attachments are added to replace redaction messages with attached images. I went with attachment and `cid` URLs because base64 image support is _still_ flaky in email clients. All redaction of secure media is now handled in `Email::Styles` and calls out to `PrettyText.strip_secure_media` to do the actual stripping and replacing with placeholders. `app/mailers/group_smtp_mailer.rb` and `app/mailers/user_notifications.rb` no longer do any stripping because they are earlier in the pipeline than `Email::Styles`. Finally the redaction notice has been restyled and includes a link to the media that the user can click, which will show it to them if they have the necessary permissions. ![image](https://user-images.githubusercontent.com/920448/92341012-b9a2c380-f0ff-11ea-860e-b376b4528357.png)
2020-09-09 19:50:16 -04:00
def stripped_upload_sha_map
@stripped_upload_sha_map ||=
begin
upload_shas = {}
stripped_media.each do |div|
url = div["data-stripped-secure-media"] || div["data-stripped-secure-upload"]
upload_shas[url] = Upload.sha1_from_long_url(url)
end
upload_shas
end
end
def stripped_secure_image_uploads
upload_shas = stripped_upload_sha_map
Upload.select(:original_filename, :sha1).where(sha1: upload_shas.values)
end
def inline_secure_images(attachments, attachments_index)
uploads = stripped_secure_image_uploads
upload_shas = stripped_upload_sha_map
FEATURE: Allow email image embed with secure media (#10563) This PR introduces a few important changes to secure media redaction in emails. First of all, two new site settings have been introduced: * `secure_media_allow_embed_images_in_emails`: If enabled we will embed secure images in emails instead of redacting them. * `secure_media_max_email_embed_image_size_kb`: The cap to the size of the secure image we will embed, defaulting to 1mb, so the email does not become too big. Max is 10mb. Works in tandem with `email_total_attachment_size_limit_kb`. `Email::Sender` will now attach images to the email based on these settings. The sender will also call `inline_secure_images` in `Email::Styles` after secure media is redacted and attachments are added to replace redaction messages with attached images. I went with attachment and `cid` URLs because base64 image support is _still_ flaky in email clients. All redaction of secure media is now handled in `Email::Styles` and calls out to `PrettyText.strip_secure_media` to do the actual stripping and replacing with placeholders. `app/mailers/group_smtp_mailer.rb` and `app/mailers/user_notifications.rb` no longer do any stripping because they are earlier in the pipeline than `Email::Styles`. Finally the redaction notice has been restyled and includes a link to the media that the user can click, which will show it to them if they have the necessary permissions. ![image](https://user-images.githubusercontent.com/920448/92341012-b9a2c380-f0ff-11ea-860e-b376b4528357.png)
2020-09-09 19:50:16 -04:00
stripped_media.each do |div|
upload =
uploads.find do |upl|
upl.sha1 ==
upload_shas[div["data-stripped-secure-media"] || div["data-stripped-secure-upload"]]
end
FEATURE: Allow email image embed with secure media (#10563) This PR introduces a few important changes to secure media redaction in emails. First of all, two new site settings have been introduced: * `secure_media_allow_embed_images_in_emails`: If enabled we will embed secure images in emails instead of redacting them. * `secure_media_max_email_embed_image_size_kb`: The cap to the size of the secure image we will embed, defaulting to 1mb, so the email does not become too big. Max is 10mb. Works in tandem with `email_total_attachment_size_limit_kb`. `Email::Sender` will now attach images to the email based on these settings. The sender will also call `inline_secure_images` in `Email::Styles` after secure media is redacted and attachments are added to replace redaction messages with attached images. I went with attachment and `cid` URLs because base64 image support is _still_ flaky in email clients. All redaction of secure media is now handled in `Email::Styles` and calls out to `PrettyText.strip_secure_media` to do the actual stripping and replacing with placeholders. `app/mailers/group_smtp_mailer.rb` and `app/mailers/user_notifications.rb` no longer do any stripping because they are earlier in the pipeline than `Email::Styles`. Finally the redaction notice has been restyled and includes a link to the media that the user can click, which will show it to them if they have the necessary permissions. ![image](https://user-images.githubusercontent.com/920448/92341012-b9a2c380-f0ff-11ea-860e-b376b4528357.png)
2020-09-09 19:50:16 -04:00
next if !upload
if attachments[attachments_index[upload.sha1]]
url = attachments[attachments_index[upload.sha1]].url
FEATURE: Allow email image embed with secure media (#10563) This PR introduces a few important changes to secure media redaction in emails. First of all, two new site settings have been introduced: * `secure_media_allow_embed_images_in_emails`: If enabled we will embed secure images in emails instead of redacting them. * `secure_media_max_email_embed_image_size_kb`: The cap to the size of the secure image we will embed, defaulting to 1mb, so the email does not become too big. Max is 10mb. Works in tandem with `email_total_attachment_size_limit_kb`. `Email::Sender` will now attach images to the email based on these settings. The sender will also call `inline_secure_images` in `Email::Styles` after secure media is redacted and attachments are added to replace redaction messages with attached images. I went with attachment and `cid` URLs because base64 image support is _still_ flaky in email clients. All redaction of secure media is now handled in `Email::Styles` and calls out to `PrettyText.strip_secure_media` to do the actual stripping and replacing with placeholders. `app/mailers/group_smtp_mailer.rb` and `app/mailers/user_notifications.rb` no longer do any stripping because they are earlier in the pipeline than `Email::Styles`. Finally the redaction notice has been restyled and includes a link to the media that the user can click, which will show it to them if they have the necessary permissions. ![image](https://user-images.githubusercontent.com/920448/92341012-b9a2c380-f0ff-11ea-860e-b376b4528357.png)
2020-09-09 19:50:16 -04:00
onebox_type = div["data-onebox-type"]
style =
if onebox_type
onebox_style =
(
if onebox_type == "avatar-inline"
ONEBOX_INLINE_AVATAR_STYLE
else
ONEBOX_IMAGE_THUMBNAIL_STYLE
end
)
"#{onebox_style} #{ONEBOX_IMAGE_BASE_STYLE}"
else
calculate_width_and_height_style(div)
end
div.add_next_sibling(<<~HTML)
<img src="#{url}" data-embedded-secure-image="true" style="#{style}" />
HTML
FEATURE: Allow email image embed with secure media (#10563) This PR introduces a few important changes to secure media redaction in emails. First of all, two new site settings have been introduced: * `secure_media_allow_embed_images_in_emails`: If enabled we will embed secure images in emails instead of redacting them. * `secure_media_max_email_embed_image_size_kb`: The cap to the size of the secure image we will embed, defaulting to 1mb, so the email does not become too big. Max is 10mb. Works in tandem with `email_total_attachment_size_limit_kb`. `Email::Sender` will now attach images to the email based on these settings. The sender will also call `inline_secure_images` in `Email::Styles` after secure media is redacted and attachments are added to replace redaction messages with attached images. I went with attachment and `cid` URLs because base64 image support is _still_ flaky in email clients. All redaction of secure media is now handled in `Email::Styles` and calls out to `PrettyText.strip_secure_media` to do the actual stripping and replacing with placeholders. `app/mailers/group_smtp_mailer.rb` and `app/mailers/user_notifications.rb` no longer do any stripping because they are earlier in the pipeline than `Email::Styles`. Finally the redaction notice has been restyled and includes a link to the media that the user can click, which will show it to them if they have the necessary permissions. ![image](https://user-images.githubusercontent.com/920448/92341012-b9a2c380-f0ff-11ea-860e-b376b4528357.png)
2020-09-09 19:50:16 -04:00
div.remove
end
end
end
def deduplicate_style(style)
styles = {}
style
.split(";")
.select(&:present?)
.map { _1.split(":", 2).map(&:strip) }
.each { |k, v| styles[k] = v if k.present? && v.present? }
styles.map { |k, v| "#{k}:#{v}" }.join(";")
end
def deduplicate_styles
@fragment
.css("[style]")
.each { |element| element["style"] = deduplicate_style element["style"] }
end
def to_html
FEATURE: Allow email image embed with secure media (#10563) This PR introduces a few important changes to secure media redaction in emails. First of all, two new site settings have been introduced: * `secure_media_allow_embed_images_in_emails`: If enabled we will embed secure images in emails instead of redacting them. * `secure_media_max_email_embed_image_size_kb`: The cap to the size of the secure image we will embed, defaulting to 1mb, so the email does not become too big. Max is 10mb. Works in tandem with `email_total_attachment_size_limit_kb`. `Email::Sender` will now attach images to the email based on these settings. The sender will also call `inline_secure_images` in `Email::Styles` after secure media is redacted and attachments are added to replace redaction messages with attached images. I went with attachment and `cid` URLs because base64 image support is _still_ flaky in email clients. All redaction of secure media is now handled in `Email::Styles` and calls out to `PrettyText.strip_secure_media` to do the actual stripping and replacing with placeholders. `app/mailers/group_smtp_mailer.rb` and `app/mailers/user_notifications.rb` no longer do any stripping because they are earlier in the pipeline than `Email::Styles`. Finally the redaction notice has been restyled and includes a link to the media that the user can click, which will show it to them if they have the necessary permissions. ![image](https://user-images.githubusercontent.com/920448/92341012-b9a2c380-f0ff-11ea-860e-b376b4528357.png)
2020-09-09 19:50:16 -04:00
# needs to be before class + id strip because we need to style redacted
# media and also not double-redact already redacted from lower levels
replace_secure_uploads_urls if SiteSetting.secure_uploads?
strip_classes_and_ids
replace_relative_urls
deduplicate_styles
@fragment.to_html
end
FEATURE: Allow email image embed with secure media (#10563) This PR introduces a few important changes to secure media redaction in emails. First of all, two new site settings have been introduced: * `secure_media_allow_embed_images_in_emails`: If enabled we will embed secure images in emails instead of redacting them. * `secure_media_max_email_embed_image_size_kb`: The cap to the size of the secure image we will embed, defaulting to 1mb, so the email does not become too big. Max is 10mb. Works in tandem with `email_total_attachment_size_limit_kb`. `Email::Sender` will now attach images to the email based on these settings. The sender will also call `inline_secure_images` in `Email::Styles` after secure media is redacted and attachments are added to replace redaction messages with attached images. I went with attachment and `cid` URLs because base64 image support is _still_ flaky in email clients. All redaction of secure media is now handled in `Email::Styles` and calls out to `PrettyText.strip_secure_media` to do the actual stripping and replacing with placeholders. `app/mailers/group_smtp_mailer.rb` and `app/mailers/user_notifications.rb` no longer do any stripping because they are earlier in the pipeline than `Email::Styles`. Finally the redaction notice has been restyled and includes a link to the media that the user can click, which will show it to them if they have the necessary permissions. ![image](https://user-images.githubusercontent.com/920448/92341012-b9a2c380-f0ff-11ea-860e-b376b4528357.png)
2020-09-09 19:50:16 -04:00
def to_s
@fragment.to_s
end
2014-09-13 01:26:31 -04:00
def strip_avatars_and_emojis
@fragment
.search("img")
.each do |img|
next unless img["src"]
if img["src"][/_avatar/]
img.parent["style"] = "vertical-align: top;" if img.parent&.name == "td"
2014-09-13 01:26:31 -04:00
img.remove
end
if img["title"] && img["src"][%r{/_?emoji/}]
2015-08-18 19:12:08 -04:00
img.add_previous_sibling(img["title"] || "emoji")
img.remove
end
2015-08-18 19:12:08 -04:00
end
2014-09-13 01:26:31 -04:00
end
def strip_hashtag_link_icons
FEATURE: Generic hashtag autocomplete lookup and markdown cooking (#18937) This commit fleshes out and adds functionality for the new `#hashtag` search and lookup system, still hidden behind the `enable_experimental_hashtag_autocomplete` feature flag. **Serverside** We have two plugin API registration methods that are used to define data sources (`register_hashtag_data_source`) and hashtag result type priorities depending on the context (`register_hashtag_type_in_context`). Reading the comments in plugin.rb should make it clear what these are doing. Reading the `HashtagAutocompleteService` in full will likely help a lot as well. Each data source is responsible for providing its own **lookup** and **search** method that returns hashtag results based on the arguments provided. For example, the category hashtag data source has to take into account parent categories and how they relate, and each data source has to define their own icon to use for the hashtag, and so on. The `Site` serializer has two new attributes that source data from `HashtagAutocompleteService`. There is `hashtag_icons` that is just a simple array of all the different icons that can be used for allowlisting in our markdown pipeline, and there is `hashtag_context_configurations` that is used to store the type priority orders for each registered context. When sending emails, we cannot render the SVG icons for hashtags, so we need to change the HTML hashtags to the normal `#hashtag` text. **Markdown** The `hashtag-autocomplete.js` file is where I have added the new `hashtag-autocomplete` markdown rule, and like all of our rules this is used to cook the raw text on both the clientside and on the serverside using MiniRacer. Only on the server side do we actually reach out to the database with the `hashtagLookup` function, on the clientside we just render a plainer version of the hashtag HTML. Only in the composer preview do we do further lookups based on this. This rule is the first one (that I can find) that uses the `currentUser` based on a passed in `user_id` for guardian checks in markdown rendering code. This is the `last_editor_id` for both the post and chat message. In some cases we need to cook without a user present, so the `Discourse.system_user` is used in this case. **Chat Channels** This also contains the changes required for chat so that chat channels can be used as a data source for hashtag searches and lookups. This data source will only be used when `enable_experimental_hashtag_autocomplete` is `true`, so we don't have to worry about channel results suddenly turning up. ------ **Known Rough Edges** - Onebox excerpts will not render the icon svg/use tags, I plan to address that in a follow up PR - Selecting a hashtag + pressing the Quote button will result in weird behaviour, I plan to address that in a follow up PR - Mixed hashtag contexts for hashtags without a type suffix will not work correctly, e.g. #ux which is both a category and a channel slug will resolve to a category when used inside a post or within a [chat] transcript in that post. Users can get around this manually by adding the correct suffix, for example ::channel. We may get to this at some point in future - Icons will not show for the hashtags in emails since SVG support is so terrible in email (this is not likely to be resolved, but still noting for posterity) - Additional refinements and review fixes wil
2022-11-20 17:37:06 -05:00
@fragment
.search(".hashtag-cooked")
.each do |hashtag|
hashtag.children.each(&:remove)
hashtag.add_child(<<~HTML)
FEATURE: Generic hashtag autocomplete lookup and markdown cooking (#18937) This commit fleshes out and adds functionality for the new `#hashtag` search and lookup system, still hidden behind the `enable_experimental_hashtag_autocomplete` feature flag. **Serverside** We have two plugin API registration methods that are used to define data sources (`register_hashtag_data_source`) and hashtag result type priorities depending on the context (`register_hashtag_type_in_context`). Reading the comments in plugin.rb should make it clear what these are doing. Reading the `HashtagAutocompleteService` in full will likely help a lot as well. Each data source is responsible for providing its own **lookup** and **search** method that returns hashtag results based on the arguments provided. For example, the category hashtag data source has to take into account parent categories and how they relate, and each data source has to define their own icon to use for the hashtag, and so on. The `Site` serializer has two new attributes that source data from `HashtagAutocompleteService`. There is `hashtag_icons` that is just a simple array of all the different icons that can be used for allowlisting in our markdown pipeline, and there is `hashtag_context_configurations` that is used to store the type priority orders for each registered context. When sending emails, we cannot render the SVG icons for hashtags, so we need to change the HTML hashtags to the normal `#hashtag` text. **Markdown** The `hashtag-autocomplete.js` file is where I have added the new `hashtag-autocomplete` markdown rule, and like all of our rules this is used to cook the raw text on both the clientside and on the serverside using MiniRacer. Only on the server side do we actually reach out to the database with the `hashtagLookup` function, on the clientside we just render a plainer version of the hashtag HTML. Only in the composer preview do we do further lookups based on this. This rule is the first one (that I can find) that uses the `currentUser` based on a passed in `user_id` for guardian checks in markdown rendering code. This is the `last_editor_id` for both the post and chat message. In some cases we need to cook without a user present, so the `Discourse.system_user` is used in this case. **Chat Channels** This also contains the changes required for chat so that chat channels can be used as a data source for hashtag searches and lookups. This data source will only be used when `enable_experimental_hashtag_autocomplete` is `true`, so we don't have to worry about channel results suddenly turning up. ------ **Known Rough Edges** - Onebox excerpts will not render the icon svg/use tags, I plan to address that in a follow up PR - Selecting a hashtag + pressing the Quote button will result in weird behaviour, I plan to address that in a follow up PR - Mixed hashtag contexts for hashtags without a type suffix will not work correctly, e.g. #ux which is both a category and a channel slug will resolve to a category when used inside a post or within a [chat] transcript in that post. Users can get around this manually by adding the correct suffix, for example ::channel. We may get to this at some point in future - Icons will not show for the hashtags in emails since SVG support is so terrible in email (this is not likely to be resolved, but still noting for posterity) - Additional refinements and review fixes wil
2022-11-20 17:37:06 -05:00
<span>##{hashtag["data-slug"]}</span>
HTML
end
end
def make_all_links_absolute
site_uri = URI(Discourse.base_url)
@fragment
.css("a")
.each do |link|
begin
link["href"] = "#{site_uri}#{link["href"]}" if URI(link["href"].to_s).host.blank?
rescue URI::Error
# leave it
end
end
end
private
2013-06-10 15:33:37 -04:00
def dark_mode_styles
# When we ship the email template and its styles we strip all css classes so to give our
# dark mode styles we are including in the template a selector we add a data-attr of 'dm=value' to
# the appropriate place
style(
".digest-header, .digest-topic, .digest-topic-title-wrapper, .digest-topic-stats, .popular-post-excerpt",
nil,
dm: "header",
)
style(
".digest-content, .header-popular-posts, .spacer, .popular-post-spacer, .popular-post-meta, .digest-new-header, .digest-new-topic, .body",
nil,
dm: "body",
)
style(".with-accent-colors, .digest-content-header", nil, dm: "body_primary")
style(".digest-topic-body", nil, dm: "topic-body")
style(".summary-footer", nil, dm: "text-color")
style("code, pre code, blockquote", nil, dm: "bg")
end
def replace_relative_urls
forum_uri = URI(Discourse.base_url)
host = forum_uri.host
scheme = forum_uri.scheme
@fragment
.css("[href]")
.each do |element|
href = element["href"]
element["href"] = "#{scheme}:#{href}" if href.start_with?("\/\/#{host}")
end
end
def calculate_width_and_height_style(div)
width = div["data-width"]
height = div["data-height"]
if width.present? && height.present? && height.to_i < MAX_IMAGE_DIMENSION &&
width.to_i < MAX_IMAGE_DIMENSION
"width: #{width}px; height: #{height}px;"
else
"max-width: 50%; max-height: #{MAX_IMAGE_DIMENSION}px;"
end
end
def replace_secure_uploads_urls
FEATURE: Allow email image embed with secure media (#10563) This PR introduces a few important changes to secure media redaction in emails. First of all, two new site settings have been introduced: * `secure_media_allow_embed_images_in_emails`: If enabled we will embed secure images in emails instead of redacting them. * `secure_media_max_email_embed_image_size_kb`: The cap to the size of the secure image we will embed, defaulting to 1mb, so the email does not become too big. Max is 10mb. Works in tandem with `email_total_attachment_size_limit_kb`. `Email::Sender` will now attach images to the email based on these settings. The sender will also call `inline_secure_images` in `Email::Styles` after secure media is redacted and attachments are added to replace redaction messages with attached images. I went with attachment and `cid` URLs because base64 image support is _still_ flaky in email clients. All redaction of secure media is now handled in `Email::Styles` and calls out to `PrettyText.strip_secure_media` to do the actual stripping and replacing with placeholders. `app/mailers/group_smtp_mailer.rb` and `app/mailers/user_notifications.rb` no longer do any stripping because they are earlier in the pipeline than `Email::Styles`. Finally the redaction notice has been restyled and includes a link to the media that the user can click, which will show it to them if they have the necessary permissions. ![image](https://user-images.githubusercontent.com/920448/92341012-b9a2c380-f0ff-11ea-860e-b376b4528357.png)
2020-09-09 19:50:16 -04:00
# strip again, this can be done at a lower level like in the user
# notification template but that may not catch everything
PrettyText.strip_secure_uploads(@fragment)
style(
"div.secure-upload-notice",
"border: 5px solid #e9e9e9; padding: 5px; display: inline-block;",
)
style("div.secure-upload-notice a", "color: #{SiteSetting.email_link_color}")
end
2013-07-26 18:08:58 -04:00
def correct_first_body_margin
@fragment.css("div.body p").each { |element| element["style"] = "margin-top:0; border: 0;" }
2013-07-26 18:08:58 -04:00
end
def correct_footer_style
@fragment
.css(".footer")
.each do |element|
element["style"] = "color:#666;"
2018-06-11 18:54:39 -04:00
element.css("a").each { |inner| inner["style"] = "color:#666;" }
end
end
def correct_footer_style_highlight_first
2018-06-11 18:54:39 -04:00
footernum = 0
@fragment
.css(".footer.highlight")
.each do |element|
linknum = 0
element
.css("a")
.each do |inner|
# we want the first footer link to be specially highlighted as IMPORTANT
if footernum == (0) && linknum == (0)
bg_color = SiteSetting.email_accent_bg_color
inner[
"style"
] = "background-color: #{bg_color}; color: #{SiteSetting.email_accent_fg_color}; border-top: 4px solid #{bg_color}; border-right: 6px solid #{bg_color}; border-bottom: 4px solid #{bg_color}; border-left: 6px solid #{bg_color}; display: inline-block; font-weight: bold;"
end
# rubocop:disable Lint/NonLocalExitFromIterator
return
end
2018-06-11 18:54:39 -04:00
return
# rubocop:enable Lint/NonLocalExitFromIterator
end
end
2013-06-10 15:33:37 -04:00
def strip_classes_and_ids
@fragment
.css("*")
.each do |element|
element.delete("class")
element.delete("id")
end
end
def reset_tables
style("table", nil, cellspacing: "0", cellpadding: "0", border: "0")
2013-06-10 15:33:37 -04:00
end
def style(selector, style, attribs = {})
@fragment
.css(selector)
.each do |element|
add_styles(element, style) if style
attribs.each { |k, v| element[k] = v }
end
end
2013-06-10 15:33:37 -04:00
end
end