# frozen_string_literal: true require "email" RSpec.describe Email::Styles do let(:attachments) { {} } def basic_fragment(html) styler = Email::Styles.new(html) styler.format_basic Nokogiri::HTML5.fragment(styler.to_html) end def html_fragment(html) styler = Email::Styles.new(html) styler.format_basic styler.format_html Nokogiri::HTML5.fragment(styler.to_html) end describe "basic formatter" do it "adds a max-width to large images" do frag = basic_fragment("") expect(frag.at("img")["style"]).to match("max-width") end it "adds a width and height to emojis" do frag = basic_fragment("") expect(frag.at("img")["width"]).to eq("20") expect(frag.at("img")["height"]).to eq("20") end it "adds a width and height to custom emojis" do frag = basic_fragment("") expect(frag.at("img")["width"]).to eq("20") expect(frag.at("img")["height"]).to eq("20") end it "converts relative paths to absolute paths" do frag = basic_fragment("") expect(frag.at("img")["src"]).to eq("#{Discourse.base_url}/some-image.png") end it "strips classes and ids" do frag = basic_fragment("
") expect(frag.to_html).to eq("
") end end describe "html template formatter" do it "attaches a style to h3 tags" do frag = html_fragment("

hello

") expect(frag.at("h3")["style"]).to be_present end it "attaches a style to hr tags" do frag = html_fragment("hello
") expect(frag.at("hr")["style"]).to be_present end it "attaches a style to a tags" do frag = html_fragment("wat") expect(frag.at("a")["style"]).to be_present end it "attaches a style to ul and li tags" do frag = html_fragment("") expect(frag.at("ul")["style"]).to be_present expect(frag.at("li")["style"]).to be_present end it "converts iframes to links" do iframe_url = "http://www.youtube.com/embed/7twifrxOTQY?feature=oembed&wmode=opaque" frag = html_fragment("") expect(frag.at("iframe")).to be_blank expect(frag.at("a")).to be_present expect(frag.at("a")["href"]).to eq(iframe_url) end it "won't allow non URLs in iframe src, strips them with no link" do iframe_url = "alert('xss hole')" frag = html_fragment("") expect(frag.at("iframe")).to be_blank expect(frag.at("a")).to be_blank end it "won't allow empty iframe src, strips them with no link" do frag = html_fragment("") expect(frag.at("iframe")).to be_blank expect(frag.at("a")).to be_blank end it "prefers data-original-href attribute to get iframe link" do original_url = "https://vimeo.com/329875646/85f1546a42" iframe_url = "https://player.vimeo.com/video/329875646" frag = html_fragment( "", ) expect(frag.at("iframe")).to be_blank expect(frag.at("a")).to be_present expect(frag.at("a")["href"]).to eq(original_url) end it "replaces hashtag-cooked text with raw #hashtag" do category = Fabricate(:category, name: "dev", slug: "dev") post = Fabricate(:post, raw: "this is #dev") post.rebake! hashtag_html = post.cooked frag = html_fragment(hashtag_html) expect(frag.at("a").text.chomp).to eq("#dev") end end describe "rewriting protocol relative URLs to the forum" do it "doesn't rewrite a url to another site" do frag = html_fragment('hello') expect(frag.at("a")["href"]).to eq("//youtube.com/discourse") end context "without https" do before { SiteSetting.force_https = false } it "rewrites the href to have http" do frag = html_fragment('hello') expect(frag.at("a")["href"]).to eq("http://test.localhost/discourse") end it "rewrites the href for attachment files to have http" do frag = html_fragment( 'attachment_file.txt', ) expect(frag.at("a")["href"]).to eq( "http://try-discourse.global.ssl.fastly.net/uploads/default/368/40b610b0aa90cfcf.txt", ) end it "rewrites the src to have http" do frag = html_fragment('') expect(frag.at("img")["src"]).to eq("http://test.localhost/blah.jpg") end end context "with https" do before { SiteSetting.force_https = true } it "rewrites the forum URL to have https" do frag = html_fragment('hello') expect(frag.at("a")["href"]).to eq("https://test.localhost/discourse") end it "rewrites the href for attachment files to have https" do frag = html_fragment( 'attachment_file.txt', ) expect(frag.at("a")["href"]).to eq( "https://try-discourse.global.ssl.fastly.net/uploads/default/368/40b610b0aa90cfcf.txt", ) end it "rewrites the src to have https" do frag = html_fragment('') expect(frag.at("img")["src"]).to eq("https://test.localhost/blah.jpg") end end end describe "deduplicate styles" do it "removes double definitions" do frag = "hello" styler = Email::Styles.new(frag) styled = styler.to_html styled = Nokogiri::HTML5.fragment(styled) expect(styled.at("test")["style"]).to eq("color:red") end it "handles whitespace correctly" do frag = "hello" styler = Email::Styles.new(frag) styled = styler.to_html styled = Nokogiri::HTML5.fragment(styled) expect(styled.at("test")["style"]).to eq("color:red;background:yellow") end end describe "dark mode emails" do it "adds dark_mode_styles when site setting active" do frag = html_fragment('
test
') styler = Email::Styles.new(frag) styler.format_basic styler.format_html @frag = Nokogiri::HTML5.fragment(styler.to_s) # dark mode attribute expect(@frag.css('[dm="body"]')).to be_present end end describe "strip_avatars_and_emojis" do it "works for lonesome emoji with no title" do emoji = "" style = Email::Styles.new(emoji) style.strip_avatars_and_emojis expect(style.to_html).to match_html(emoji) end it "works for lonesome emoji with title" do emoji = "" style = Email::Styles.new(emoji) style.strip_avatars_and_emojis expect(style.to_html).to match_html("cry_cry") end it "works if img tag has no attrs" do cooked = "Create a method for click on image and use ng-click in in your slide box...it is simple" style = Email::Styles.new(cooked) style.strip_avatars_and_emojis expect(style.to_html).to include(cooked) end end describe "onebox_styles" do it "renders quote as
" do fragment = html_fragment( '', ) expect(fragment.to_s.squish).to match(%r{^$}) end it "removes GitHub excerpts" do stub_request(:head, "https://github.com/discourse/discourse/pull/1253").to_return( status: 200, body: "", headers: { }, ) stub_request(:get, "https://api.github.com/repos/discourse/discourse/pulls/1253").to_return( status: 200, body: onebox_response("githubpullrequest"), ) onebox = Oneboxer.onebox("https://github.com/discourse/discourse/pull/1253") fragment = html_fragment(onebox) expect(fragment.css(".github-body-container .excerpt")).to be_empty end end describe "replace_secure_uploads_urls" do before do setup_s3 SiteSetting.secure_uploads = true end let(:attachments) { { "testimage.png" => stub(url: "email/test.png") } } it "replaces secure uploads within a link with a placeholder" do frag = html_fragment( "", ) expect(frag.at("img")).not_to be_present expect(frag.to_s).to include("Redacted") end it "replaces secure images with a placeholder" do frag = html_fragment("") expect(frag.at("img")).not_to be_present expect(frag.to_s).to include("Redacted") end it "does not replace topic links with secure-uploads in the name" do frag = html_fragment("Visit Topic") expect(frag.to_s).not_to include("Redacted") end it "works in lightboxes with missing srcset attribute" do frag = html_fragment( "", ) expect(frag.at("img")).not_to be_present expect(frag.to_s).to include("Redacted") end it "works in lightboxes with srcset attribute set" do frag = html_fragment(<<~HTML) HTML expect(frag.at("img")).not_to be_present expect(frag.to_s).to include("Redacted") end it "skips links with no images as children" do frag = html_fragment( "Clearly not an image", ) expect(frag.to_s).to include("not an image") end end describe "#inline_secure_images" do before do setup_s3 SiteSetting.secure_uploads = true end fab!(:upload) do Fabricate(:upload, original_filename: "testimage.png", secure: true, sha1: "123456") end let(:attachments) { [stub(url: "cid:email/test.png")] } let(:attachments_index) { { upload.sha1 => 0 } } let(:html) do "" end def strip_and_inline # strip out the secure uploads styler = Email::Styles.new(html) styler.format_basic styler.format_html html = styler.to_html # pass in the attachments to match uploads based on sha + original filename styler = Email::Styles.new(html) styler.inline_secure_images(attachments, attachments_index) @frag = Nokogiri::HTML5.fragment(styler.to_s) end it "inlines attachments where stripped-secure-media data attr is present" do strip_and_inline expect(@frag.to_s).to include("cid:email/test.png") expect(@frag.css("[data-stripped-secure-upload]")).not_to be_present expect(@frag.children.attr("style").value).to eq("width: 20px; height: 30px;") end it "does not inline anything if the upload cannot be found" do upload.update(sha1: "blah12") strip_and_inline expect(@frag.to_s).not_to include("cid:email/test.png") expect(@frag.css("[data-stripped-secure-upload]")).to be_present end context "when an optimized image is used instead of the original" do let(:html) do "" end it "inlines attachments where the stripped-secure-media data attr is present" do optimized = Fabricate(:optimized_image, upload: upload, width: 20, height: 30) strip_and_inline expect(@frag.to_s).to include("cid:email/test.png") expect(@frag.css("[data-stripped-secure-upload]")).not_to be_present expect(@frag.children.attr("style").value).to eq("width: 20px; height: 30px;") end end context "when inlining an originally oneboxed image" do before { SiteSetting.authorized_extensions = "*" } let(:siteicon) { Fabricate(:upload, original_filename: "siteicon.ico") } let(:attachments) { [stub(url: "cid:email/test.png"), stub(url: "cid:email/test2.ico")] } let(:attachments_index) { { upload.sha1 => 0, siteicon.sha1 => 1 } } let(:html) { <<~HTML } HTML it "keeps the special site icon width and height and onebox styles" do optimized = Fabricate(:optimized_image, upload: upload, width: 20, height: 30) strip_and_inline expect(@frag.to_s).to include("cid:email/test.png") expect(@frag.to_s).to include("cid:email/test2.ico") expect(@frag.css("[data-stripped-secure-upload]")).not_to be_present expect(@frag.css("[data-embedded-secure-image]")[0].attr("style")).to eq( "width: 16px; height: 16px;", ) expect(@frag.css("[data-embedded-secure-image]")[1].attr("style")).to eq( "width: 60px; max-height: 80%; max-width: 20%; height: auto; float: left; margin-right: 10px;", ) end context "when inlining a oneboxed image with a direct parent of onebox-body" do let(:html) { <<~HTML } HTML it "keeps the special onebox styles" do strip_and_inline expect(@frag.to_s).to include("cid:email/test.png") expect(@frag.to_s).to include("cid:email/test2.ico") expect(@frag.css("[data-stripped-secure-upload]")).not_to be_present expect(@frag.css("[data-embedded-secure-image]")[1].attr("style")).to eq( "width: 60px; max-height: 80%; max-width: 20%; height: auto; float: left; margin-right: 10px;", ) end end context "when there is an inline-avatar in the onebox" do let(:html) { <<~HTML }

@martin check this out:

HTML it "keeps the special onebox styles" do strip_and_inline expect(@frag.to_s).to include("cid:email/test.png") expect(@frag.css("[data-stripped-secure-upload]")).not_to be_present expect(@frag.css("[data-embedded-secure-image]")[0].attr("style")).to eq( "width: 20px; height: 20px; float: none; vertical-align: middle; max-height: 80%; max-width: 20%; height: auto; float: left; margin-right: 10px;", ) end end end end end