# frozen_string_literal: true require 'rails_helper' require 'pretty_text' describe PrettyText do before do SiteSetting.enable_markdown_typographer = false end def n(html) html.strip end def cook(*args) PrettyText.cook(*args) end let(:wrapped_image) { "
\nScreen Shot 2014-04-14 at 9.47.10 PM.png966x737 1.47 MB\n
" } describe "Quoting" do describe "with avatar" do let(:default_avatar) { "//test.localhost/uploads/default/avatars/42d/57c/46ce7ee487/{size}.png" } fab!(:user) { Fabricate(:user) } before do User.stubs(:default_template).returns(default_avatar) end it "do off topic quoting with emoji unescape" do topic = Fabricate(:topic, title: "this is a test topic :slight_smile:") expected = <<~HTML HTML expect(cook("[quote=\"EvilTrout, post:2, topic:#{topic.id}\"]\nddd\n[/quote]", topic_id: 1)).to eq(n(expected)) end context "emojis" do let(:md) do <<~MD > This is a quote with a regular emoji :upside_down_face: > This is a quote with an emoji shortcut :) > This is a quote with a Unicode emoji ๐Ÿ˜Ž MD end it "does not unescape emojis when emojis are disabled" do SiteSetting.enable_emoji = false html = <<~HTML

This is a quote with a regular emoji :upside_down_face:

This is a quote with an emoji shortcut :)

This is a quote with a Unicode emoji ๐Ÿ˜Ž

HTML expect(cook(md)).to eq(html.strip) end it "does not convert emoji shortcuts when emoji shortcuts are disabled" do SiteSetting.enable_emoji_shortcuts = false html = <<~HTML

This is a quote with a regular emoji :upside_down_face:

This is a quote with an emoji shortcut :)

This is a quote with a Unicode emoji :sunglasses:

HTML expect(cook(md)).to eq(html.strip) end it "unescapes all emojis" do html = <<~HTML

This is a quote with a regular emoji :upside_down_face:

This is a quote with an emoji shortcut :slight_smile:

This is a quote with a Unicode emoji :sunglasses:

HTML expect(cook(md)).to eq(html.strip) end it "adds an only-emoji class when a line has only one emoji" do md = <<~MD โ˜น๏ธ foo ๐Ÿ˜€ foo ๐Ÿ˜€ bar :smile_cat: :smile_cat: :smile_cat: :smile_cat: :smile_cat: :smile_cat: :smile_cat: baz? :smile_cat: ๐Ÿ˜€ ๐Ÿ˜‰ foo ๐Ÿ˜‰ ๐Ÿ˜‰ ๐Ÿ˜‰ ๐Ÿ˜‰ ๐Ÿ˜‰ ๐Ÿ˜‰ ๐Ÿ˜‰ ๐Ÿ˜‰๐Ÿ˜‰๐Ÿ˜‰ ๐Ÿ˜‰ ๐Ÿ˜‰ ๐Ÿ˜‰ ๐Ÿ˜‰d๐Ÿ˜‰ ๐Ÿ˜‰ ๐Ÿ˜‰ ๐Ÿ˜‰ ๐Ÿ˜‰d ๐Ÿ˜‰๐Ÿ˜‰๐Ÿ˜‰๐Ÿ˜‰ MD html = <<~HTML

:frowning:
foo :grinning:
foo :grinning: bar
:smile_cat:
:smile_cat: :smile_cat:
:smile_cat: :smile_cat: :smile_cat: :smile_cat:
baz? :smile_cat:
:grinning:
:wink: foo
:wink: :wink:
:wink: :wink:
:wink: :wink: :wink:
:wink::wink::wink:
:wink: :wink: :wink:
:wink:dโ€‹:wink: :wink:
:wink: :wink: :wink:d
:wink::wink::wink::wink:

HTML expect(cook(md)).to eq(html.strip) end it "does use emoji CDN when enabled" do SiteSetting.external_emoji_url = "https://emoji.cdn.com" html = <<~HTML

This is a quote with a regular emoji :upside_down_face:

This is a quote with an emoji shortcut :slight_smile:

This is a quote with a Unicode emoji :sunglasses:

HTML expect(cook(md)).to eq(html.strip) end it "does use emoji CDN when others CDNs are also enabled" do set_cdn_url('https://cdn.com') setup_s3 SiteSetting.s3_cdn_url = "https://s3.cdn.com" SiteSetting.external_emoji_url = "https://emoji.cdn.com" html = <<~HTML

This is a quote with a regular emoji :upside_down_face:

This is a quote with an emoji shortcut :slight_smile:

This is a quote with a Unicode emoji :sunglasses:

HTML expect(cook(md)).to eq(html.strip) end end it "do off topic quoting of posts from secure categories" do category = Fabricate(:category, read_restricted: true) topic = Fabricate(:topic, title: "this is topic with secret category", category: category) expected = <<~HTML HTML expect(cook("[quote=\"maja, post:3, topic:#{topic.id}\"]\nI have nothing to say.\n[/quote]", topic_id: 1)).to eq(n(expected)) end it "indifferent about missing quotations" do md = <<~MD [quote=#{user.username}, post:123, topic:456, full:true] ddd [/quote] MD html = <<~HTML HTML expect(PrettyText.cook(md)).to eq(html.strip) end it "indifferent about curlies and no curlies" do md = <<~MD [quote=โ€œ#{user.username}, post:123, topic:456, full:trueโ€] ddd [/quote] MD html = <<~HTML HTML expect(PrettyText.cook(md)).to eq(html.strip) end it "trims spaces on quote params" do md = <<~MD [quote="#{user.username}, post:555, topic: 666"] ddd [/quote] MD html = <<~HTML HTML expect(PrettyText.cook(md)).to eq(html.strip) end end describe "with primary user group" do let(:default_avatar) { "//test.localhost/uploads/default/avatars/42d/57c/46ce7ee487/{size}.png" } fab!(:group) { Fabricate(:group) } fab!(:user) { Fabricate(:user, primary_group: group) } before do User.stubs(:default_template).returns(default_avatar) end it "adds primary group class to referenced users quote" do topic = Fabricate(:topic, title: "this is a test topic") expected = <<~HTML HTML expect(cook("[quote=\"#{user.username}, post:2, topic:#{topic.id}\"]\nddd\n[/quote]", topic_id: 1)).to eq(n(expected)) end end it "can handle inline block bbcode" do cooked = PrettyText.cook("[quote]te **s** t[/quote]") html = <<~HTML HTML expect(cooked).to eq(html.strip) end it "handles bbcode edge cases" do expect(PrettyText.cook "[constructor]\ntest").to eq("

[constructor]
\ntest

") end it "can handle quote edge cases" do expect(PrettyText.cook("[quote]abc\ntest\n[/quote]")).not_to include('aside') expect(PrettyText.cook("[quote] \ntest\n[/quote] ")).to include('aside') expect(PrettyText.cook("a\n[quote]\ntest\n[/quote]\n\n\na")).to include('aside') expect(PrettyText.cook("- a\n[quote]\ntest\n[/quote]\n\n\na")).to include('aside') expect(PrettyText.cook("[quote]\ntest")).not_to include('aside') expect(PrettyText.cook("[quote]\ntest\n[/quote]z")).not_to include('aside') nested = <<~QUOTE [quote] a [quote] b [/quote] c [/quote] QUOTE cooked = PrettyText.cook(nested) expect(cooked.scan('aside').length).to eq(4) expect(cooked.scan('quote]').length).to eq(0) end describe "with letter avatar" do fab!(:user) { Fabricate(:user) } context "subfolder" do it "should have correct avatar url" do set_subfolder "/forum" md = <<~MD [quote="#{user.username}, post:123, topic:456, full:true"] ddd [/quote] MD expect(PrettyText.cook(md)).to include("/forum/letter_avatar_proxy") end end end end describe "Mentions" do it "can handle mentions after abbr" do expect(PrettyText.cook("test test\n\n@bob")).to eq("

test test

\n

@bob

") end it "should handle 3 mentions in a row" do expect(PrettyText.cook('@hello @hello @hello')).to match_html "

@hello @hello @hello

" end it "can handle mention edge cases" do expect(PrettyText.cook("hi\n@s")).to eq("

hi
\n@s

") expect(PrettyText.cook("hi\n@ss")).to eq("

hi
\n@ss

") expect(PrettyText.cook("hi\n@s.")).to eq("

hi
\n@s.

") expect(PrettyText.cook("hi\n@s.s")).to eq("

hi
\n@s.s

") expect(PrettyText.cook("hi\n@.s.s")).to eq("

hi
\n@.s.s

") end it "handles user and group mentions correctly" do ['User', 'user2'].each do |username | Fabricate(:user, username: username) end Fabricate(:group, name: 'Group', mentionable_level: Group::ALIAS_LEVELS[:everyone]) Fabricate(:group, name: 'Group2', mentionable_level: Group::ALIAS_LEVELS[:members_mods_and_admins]) [ [ 'hi @uSer! @user2 hi', '

hi @uSer! @user2 hi

' ], [ "hi\n@user. @GROUP @somemention @group2", %Q|

hi
\n@user. @GROUP @somemention @group2

| ] ].each do |input, expected| expect(PrettyText.cook(input)).to eq(expected) end end context 'subfolder' do it "handles user and group mentions correctly" do set_subfolder "/forum" Fabricate(:user, username: 'user1') Fabricate(:group, name: 'groupA', mentionable_level: Group::ALIAS_LEVELS[:everyone]) input = 'hi there @user1 and @groupA' expected = '

hi there @user1 and @groupA

' expect(PrettyText.cook(input)).to eq(expected) end end it "does not assign the notify class to a group that can't be mentioned" do group = Fabricate(:group, visibility_level: Group.visibility_levels[:members], mentionable_level: Group::ALIAS_LEVELS[:nobody] ) expect(PrettyText.cook("test @#{group.name} test")).to eq( %Q|

test @#{group.name} test

| ) end it "assigns the notify class if the user can mention" do group = Fabricate(:group, visibility_level: Group.visibility_levels[:members], mentionable_level: Group::ALIAS_LEVELS[:members_mods_and_admins] ) expect(PrettyText.cook("test @#{group.name} test", user_id: Fabricate(:admin).id)).to eq( %Q|

test @#{group.name} test

| ) end it 'does not mention staged users' do user = Fabricate(:user, staged: true) expect(PrettyText.cook("something @#{user.username} something")).to eq( %Q|

something @#{user.username} something

| ) end describe 'when mentions are disabled' do before do SiteSetting.enable_mentions = false end it 'should not convert mentions to links' do _user = Fabricate(:user) expect(PrettyText.cook('hi @user')).to eq('

hi @user

') end end it "can handle mentions inside a hyperlink" do expect(PrettyText.cook(" @inner ")).to match_html '

@inner

' end it "can handle mentions inside a hyperlink" do expect(PrettyText.cook("[link @inner](http://site.com)")).to match_html '

link @inner

' end it "can handle a list of mentions" do expect(PrettyText.cook("@a,@b")).to match_html('

@a,@b

') end it "should handle group mentions with a hyphen and without" do expect(PrettyText.cook('@hello @hello-hello')).to match_html "

@hello @hello-hello

" end it 'should allow for @mentions to have punctuation' do expect(PrettyText.cook("hello @bob's @bob,@bob; @bob\"")).to match_html( "

hello @bob's @bob,@bob; @bob\"

" ) end it 'should not treat a medium link as a mention' do expect(PrettyText.cook(". http://test/@sam")).not_to include('mention') end context "with Unicode usernames disabled" do before { SiteSetting.unicode_usernames = false } it 'does not detect mention' do expect(PrettyText.cook("Hello @็‹ฎๅญ")).to_not include("mention") end end context "with Unicode usernames enabled" do before { SiteSetting.unicode_usernames = true } it 'does detect mention' do expect(PrettyText.cook("Hello @็‹ฎๅญ")).to match_html '

Hello @็‹ฎๅญ

' end end end describe "code fences" do it 'indents code correctly' do code = <<~MD X ``` # x ``` MD cooked = PrettyText.cook(code) html = <<~HTML

X

     #
             x
        
HTML expect(cooked).to eq(html.strip) end it "doesn't replace emoji in code blocks with our emoji sets if emoji is enabled" do expect(PrettyText.cook("```\n๐Ÿ’ฃ`\n```\n")).not_to match(/\:bomb\:/) end it 'can include code class correctly' do # keep in mind spaces should be trimmed per spec expect(PrettyText.cook("``` ruby the mooby\n`````")).to eq('
') expect(PrettyText.cook("```cpp\ncpp\n```")).to match_html("
cpp\n
") expect(PrettyText.cook("```\ncpp\n```")).to match_html("
cpp\n
") expect(PrettyText.cook("```text\ncpp\n```")).to match_html("
cpp\n
") end it 'indents code correctly' do code = "X\n```\n\n #\n x\n```" cooked = PrettyText.cook(code) expect(cooked).to match_html("

X

\n
\n    #\n    x\n
") end it 'does censor code fences' do begin ['apple', 'banana'].each { |w| Fabricate(:watched_word, word: w, action: WatchedWord.actions[:censor]) } expect(PrettyText.cook("# banana")).not_to include('banana') ensure Discourse.redis.flushdb end end end describe "rel attributes" do before do SiteSetting.add_rel_nofollow_to_user_content = true SiteSetting.exclude_rel_nofollow_domains = "foo.com|bar.com" end it "should inject nofollow in all user provided links" do expect(PrettyText.cook('cnn')).to match(/noopener nofollow ugc/) end it "should not inject nofollow in all local links" do expect(PrettyText.cook("cnn") !~ /nofollow ugc/).to eq(true) end it "should not inject nofollow in all subdomain links" do expect(PrettyText.cook("cnn") !~ /nofollow ugc/).to eq(true) end it "should inject nofollow in all non subdomain links" do expect(PrettyText.cook("cnn")).to match(/nofollow ugc/) end it "should not inject nofollow for foo.com" do expect(PrettyText.cook("cnn") !~ /nofollow ugc/).to eq(true) end it "should inject nofollow for afoo.com" do expect(PrettyText.cook("cnn")).to match(/nofollow ugc/) end it "should not inject nofollow for bar.foo.com" do expect(PrettyText.cook("cnn") !~ /nofollow ugc/).to eq(true) end it "should not inject nofollow if omit_nofollow option is given" do expect(PrettyText.cook('cnn', omit_nofollow: true) !~ /nofollow ugc/).to eq(true) end it 'adds the noopener attribute even if omit_nofollow option is given' do raw_html = 'Check out my site!' expect( PrettyText.cook(raw_html, omit_nofollow: true) ).to match(/noopener/) end it 'adds the noopener attribute even if omit_nofollow option is given' do raw_html = 'Check out my site!' expect( PrettyText.cook(raw_html, omit_nofollow: false) ).to match(/noopener nofollow ugc/) end end describe "Excerpt" do it "sanitizes attempts to inject invalid attributes" do spinner = "", 100)).to eq("[image]") end context 'alt tags' do it "should keep alt tags" do expect(PrettyText.excerpt("car", 100)).to eq("[car]") end describe 'when alt tag is empty' do it "should not keep alt tags" do expect(PrettyText.excerpt("", 100)).to eq("[#{I18n.t('excerpt_image')}]") end end end context 'title tags' do it "should keep title tags" do expect(PrettyText.excerpt("", 100)).to eq("[car]") end describe 'when title tag is empty' do it "should not keep title tags" do expect(PrettyText.excerpt("", 100)).to eq("[#{I18n.t('excerpt_image')}]") end end end it "should convert images to markdown if the option is set" do expect(PrettyText.excerpt("", 100, markdown_images: true)).to eq("![car](http://cnn.com/a.gif)") end it "should keep details if too long" do expect(PrettyText.excerpt("
expand

hello

", 6)).to match_html "
expand
" end it "doesn't disable details if short enough" do expect(PrettyText.excerpt("
expand

hello

", 60)).to match_html "
expandhello
" end it "should remove meta informations" do expect(PrettyText.excerpt(wrapped_image, 100)).to match_html "
[image]" end it "should strip images when option is set" do expect(PrettyText.excerpt("", 100, strip_images: true)).to be_blank expect(PrettyText.excerpt(" Hello world!", 100, strip_images: true)).to eq("Hello world!") end it "should strip images, but keep emojis when option is set" do emoji_image = "heart" html = " Hello world #{emoji_image}" expect(PrettyText.excerpt(html, 100, strip_images: true)).to eq("Hello world heart") expect(PrettyText.excerpt(html, 100, strip_images: true, keep_emoji_images: true)).to match_html("Hello world #{emoji_image}") end end context "emojis" do it "should remove broken emoji" do html = <<~EOS \":bike:\" \":cat:\" \":discourse:\" EOS expect(PrettyText.excerpt(html, 7)).to eq(":bike: …") expect(PrettyText.excerpt(html, 8)).to eq(":bike: …") expect(PrettyText.excerpt(html, 9)).to eq(":bike: …") expect(PrettyText.excerpt(html, 10)).to eq(":bike: …") expect(PrettyText.excerpt(html, 11)).to eq(":bike: …") expect(PrettyText.excerpt(html, 12)).to eq(":bike: :cat: …") expect(PrettyText.excerpt(html, 13)).to eq(":bike: :cat: …") expect(PrettyText.excerpt(html, 14)).to eq(":bike: :cat: …") end end it "should have an option to strip links" do expect(PrettyText.excerpt("cnn", 100, strip_links: true)).to eq("cnn") end it "should preserve links" do expect(PrettyText.excerpt("cnn", 100)).to match_html "cnn" end it "should deal with special keys properly" do expect(PrettyText.excerpt("
", 100)).to eq("") end it "should truncate stuff properly" do expect(PrettyText.excerpt("hello world", 5)).to eq("hello…") expect(PrettyText.excerpt("

hello

world

", 6)).to eq("hello w…") end it "should insert a space between to Ps" do expect(PrettyText.excerpt("

a

b

", 5)).to eq("a b") end it "should strip quotes" do expect(PrettyText.excerpt("boom", 5)).to eq("boom") end it "should not count the surrounds of a link" do expect(PrettyText.excerpt("cnn", 3)).to match_html "cnn" end it "uses an ellipsis instead of html entities if provided with the option" do expect(PrettyText.excerpt("cnn", 2, text_entities: true)).to match_html "cn..." end it "should truncate links" do expect(PrettyText.excerpt("cnn", 2)).to match_html "cn…" end it "doesn't extract empty quotes as links" do expect(PrettyText.extract_links("\n").to_a).to be_empty end it "doesn't extract links from elided parts" do expect(PrettyText.extract_links("
cnn
\n").to_a).to be_empty end def extract_urls(text) PrettyText.extract_links(text).map(&:url).to_a end it "should be able to extract links" do expect(extract_urls("http://bla.com")).to eq(["http://cnn.com"]) end it "should extract links to topics" do expect(extract_urls("")).to eq(["/t/321"]) end it "should lazyYT videos" do expect(extract_urls("
")).to eq(["https://www.youtube.com/watch?v=yXEuEUQIP3Q"]) end it "should extract links to posts" do expect(extract_urls("")).to eq(["/t/1234/4567"]) end it "should not extract links to anchors" do expect(extract_urls("TOS")).to eq([]) end it "should not extract links inside quotes" do links = PrettyText.extract_links(" http://useless1.com http://useless2.com ") expect(links.map { |l| [l.url, l.is_quote] }.sort).to eq([ ["http://body_only.com", false], ["http://body_and_quote.com", false], ["/t/1234", true], ].sort) end it "should not preserve tags in code blocks" do expect(PrettyText.excerpt("
<h3>Hours</h3>
", 100)).to eq("<h3>Hours</h3>") end it "should handle nil" do expect(PrettyText.excerpt(nil, 100)).to eq('') end it "handles custom bbcode excerpt" do raw = <<~RAW [excerpt] hello [site](https://site.com) [/excerpt] more stuff RAW post = Fabricate(:post, raw: raw) expect(post.excerpt).to eq("hello site") end it "handles span excerpt at the beginning of a post" do expect(PrettyText.excerpt("hi test", 100)).to eq('hi') post = Fabricate(:post, raw: "hi test") expect(post.excerpt).to eq("hi") end it "ignores max excerpt length if a span excerpt is specified" do two_hundred = "123456789 " * 20 + "." text = two_hundred + "#{two_hundred}" + two_hundred expect(PrettyText.excerpt(text, 100)).to eq(two_hundred) post = Fabricate(:post, raw: text) expect(post.excerpt).to eq(two_hundred) end it "unescapes html entities when we want text entities" do expect(PrettyText.excerpt("'", 500, text_entities: true)).to eq("'") end it "should have an option to preserve emoji images" do emoji_image = "heart" expect(PrettyText.excerpt(emoji_image, 100, keep_emoji_images: true)).to match_html(emoji_image) end it "should have an option to remap emoji to code points" do emoji_image = "I :heart: you :unknown: " expect(PrettyText.excerpt(emoji_image, 100, remap_emoji: true)).to match_html("I โค you :unknown:") end it "should have an option to preserve emoji codes" do emoji_code = ":heart:" expect(PrettyText.excerpt(emoji_code, 100)).to eq(":heart:") end context 'option to preserve onebox source' do it "should return the right excerpt" do onebox = "\n\n\n" expected = "meta.discourse.org" expect(PrettyText.excerpt(onebox, 100, keep_onebox_source: true)) .to eq(expected) expect(PrettyText.excerpt("#{onebox}\n \n \n \n\n\n #{onebox}", 100, keep_onebox_source: true)) .to eq("#{expected}\n\n#{expected}") end it 'should continue to strip quotes' do expect(PrettyText.excerpt( "boom", 100, keep_onebox_source: true )).to eq("boom") end end it 'should strip audio/video' do html = <<~HTML

Listen to this!

HTML expect(PrettyText.excerpt(html, 100)).to eq("Listen to this!") html = <<~HTML

Watch this, but do not include the video in the excerpt.

HTML ellipsis = "…" excerpt_size = 40 excerpt = PrettyText.excerpt(html, excerpt_size) expect(excerpt.size).to eq(excerpt_size + ellipsis.size) expect(excerpt).to eq("Watch this, but do not include the video#{ellipsis}") end end describe "strip links" do it "returns blank for blank input" do expect(PrettyText.strip_links("")).to be_blank end it "does nothing to a string without links" do expect(PrettyText.strip_links("I'm the batman")).to eq("I'm the batman") end it "strips links but leaves the text content" do expect(PrettyText.strip_links("I'm the linked batman")).to eq("I'm the linked batman") end it "escapes the text content" do expect(PrettyText.strip_links("I'm the linked <batman>")).to eq("I'm the linked <batman>") end end describe "strip_image_wrapping" do def strip_image_wrapping(html) doc = Nokogiri::HTML5.fragment(html) described_class.strip_image_wrapping(doc) doc.to_html end it "doesn't change HTML when there's no wrapped image" do html = "" expect(strip_image_wrapping(html)).to eq(html) end it "strips the metadata" do expect(strip_image_wrapping(wrapped_image)).to match_html "
" end end describe 'format_for_email' do let(:base_url) { "http://baseurl.net" } fab!(:post) { Fabricate(:post) } before do Discourse.stubs(:base_url).returns(base_url) end it 'does not crash' do PrettyText.format_for_email('test', post) end it "adds base url to relative links" do html = "

@wiseguy, @trollol what do you guys think?

" output = described_class.format_for_email(html, post) expect(output).to eq("

@wiseguy, @trollol what do you guys think?

") end it "doesn't change external absolute links" do html = "

Check out this guy.

" expect(described_class.format_for_email(html, post)).to eq(html) end it "doesn't change internal absolute links" do html = "

Check out this guy.

" expect(described_class.format_for_email(html, post)).to eq(html) end it "can tolerate invalid URLs" do html = "

Check out this guy.

" expect { described_class.format_for_email(html, post) }.to_not raise_error end it "doesn't change mailto" do html = "

Contact me at this address.

" expect(PrettyText.format_for_email(html, post)).to eq(html) end it "prefers data-original-href attribute to get Vimeo iframe link and escapes it" do html = "

Check out this video โ€“ .

" expect(PrettyText.format_for_email(html, post)).to match(Regexp.escape("https://vimeo.com/329875646/%3E%20%3Cscript%3Ealert(1)%3C/script%3E")) end describe "#convert_vimeo_iframes" do it "converts HTML md = PrettyText.format_for_email(html, post) expect(md).not_to include('This is a Vimeo link:

https://vimeo.com/1

HTML end end describe "#strip_secure_media" do before do setup_s3 SiteSetting.s3_cdn_url = "https://s3.cdn.com" SiteSetting.secure_media = true SiteSetting.login_required = true end it "replaces secure video content" do html = <<~HTML HTML md = PrettyText.format_for_email(html, post) expect(md).not_to include(' Audio label HTML md = PrettyText.format_for_email(html, post) expect(md).not_to include(' HTML md = PrettyText.format_for_email(html, post) expect(md).not_to include(' HTML md = PrettyText.format_for_email(html, post) md = PrettyText.format_for_email(md, post) expect(md.scan(/stripped-secure-view-media/).length).to eq(1) expect(md.scan(/Redacted/).length).to eq(1) end it "replaces secure images with a placeholder, keeping the url in an attribute" do url = "/secure-media-uploads/original/1X/testimage.png" html = <<~HTML HTML md = PrettyText.format_for_email(html, post) expect(md).not_to include(' a - li ``` test ``` ``` test ``` MD html = <<~HTML

a


a

  • li

test
      
test
      
HTML expect(PrettyText.cook(raw)).to eq(html.strip) end describe "emoji" do it "replaces unicode emoji with our emoji sets if emoji is enabled" do expect(PrettyText.cook("๐Ÿ’ฃ")).to match(/\:bomb\:/) end it "does not replace left right arrow" do expect(PrettyText.cook("↔")).to eq('

โ†”

') end it "doesn't replace emoji in inline code blocks with our emoji sets if emoji is enabled" do expect(PrettyText.cook("`๐Ÿ’ฃ`")).not_to match(/\:bomb\:/) end it "replaces some glyphs that are not in the emoji range" do expect(PrettyText.cook("โ˜น")).to match(/\:frowning\:/) expect(PrettyText.cook("โ˜บ")).to match(/\:relaxed\:/) expect(PrettyText.cook("โ˜ป")).to match(/\:slight_smile\:/) expect(PrettyText.cook("โ™ก")).to match(/\:heart\:/) expect(PrettyText.cook("โค")).to match(/\:heart\:/) expect(PrettyText.cook("โค๏ธ")).to match(/\:heart\:/) # in emoji range but ensure it works along others end it "replaces digits" do expect(PrettyText.cook("๐Ÿ”ข")).to match(/\:1234\:/) expect(PrettyText.cook("1๏ธโƒฃ")).to match(/\:one\:/) expect(PrettyText.cook("#๏ธโƒฃ")).to match(/\:hash\:/) expect(PrettyText.cook("*๏ธโƒฃ")).to match(/\:asterisk\:/) end it "doesn't replace unicode emoji if emoji is disabled" do SiteSetting.enable_emoji = false expect(PrettyText.cook("๐Ÿ’ฃ")).not_to match(/\:bomb\:/) end it "doesn't replace emoji if emoji is disabled" do SiteSetting.enable_emoji = false expect(PrettyText.cook(":bomb:")).to eq("

:bomb:

") end it "doesn't replace shortcuts if disabled" do SiteSetting.enable_emoji_shortcuts = false expect(PrettyText.cook(":)")).to eq("

:)

") end it "does replace shortcuts if enabled" do expect(PrettyText.cook(":)")).to match("smile") end it "replaces skin toned emoji" do expect(PrettyText.cook("hello ๐Ÿ‘ฑ๐Ÿฟโ€โ™€๏ธ")).to eq("

hello \":blonde_woman:t6:\"

") expect(PrettyText.cook("hello ๐Ÿ‘ฉโ€๐ŸŽค")).to eq("

hello \":woman_singer:\"

") expect(PrettyText.cook("hello ๐Ÿ‘ฉ๐Ÿพโ€๐ŸŽ“")).to eq("

hello \":woman_student:t5:\"

") expect(PrettyText.cook("hello ๐Ÿคทโ€โ™€๏ธ")).to eq("

hello \":woman_shrugging:\"

") end it "correctly strips VARIATION SELECTOR-16 character (ufe0f) from some emojis" do expect(PrettyText.cook("โค๏ธ๐Ÿ’ฃ")).to match(/]+bomb[^>]+>/) end end describe "custom emoji" do it "replaces the custom emoji" do CustomEmoji.create!(name: 'trout', upload: Fabricate(:upload)) Emoji.clear_cache expect(PrettyText.cook("hello :trout:")).to match(/]+trout[^>]+>/) end end describe "custom emoji translation" do before do PrettyText.reset_translations SiteSetting.enable_emoji = true SiteSetting.enable_emoji_shortcuts = true plugin = Plugin::Instance.new plugin.translate_emoji "0:)", "otter" end after do Plugin::CustomEmoji.clear_cache PrettyText.reset_translations end it "sets the custom translation" do expect(PrettyText.cook("hello 0:)")).to match(/otter/) end end it "replaces skin toned emoji" do expect(PrettyText.cook("hello ๐Ÿ‘ฑ๐Ÿฟโ€โ™€๏ธ")).to eq("

hello \":blonde_woman:t6:\"

") expect(PrettyText.cook("hello ๐Ÿ‘ฉโ€๐ŸŽค")).to eq("

hello \":woman_singer:\"

") expect(PrettyText.cook("hello ๐Ÿ‘ฉ๐Ÿพโ€๐ŸŽ“")).to eq("

hello \":woman_student:t5:\"

") expect(PrettyText.cook("hello ๐Ÿคทโ€โ™€๏ธ")).to eq("

hello \":woman_shrugging:\"

") end it "should not treat a non emoji as an emoji" do expect(PrettyText.cook(':email,class_name:')).not_to include('emoji') end it "supports href schemes" do SiteSetting.allowed_href_schemes = "macappstore|steam" cooked = cook("[Steam URL Scheme](steam://store/452530)") expected = '

Steam URL Scheme

' expect(cooked).to eq(n expected) end it "supports forbidden schemes" do SiteSetting.allowed_href_schemes = "macappstore|itunes" cooked = cook("[Steam URL Scheme](steam://store/452530)") expected = '

Steam URL Scheme

' expect(cooked).to eq(n expected) end it 'allows only tel URL scheme to start with a plus character' do SiteSetting.allowed_href_schemes = "tel|steam" cooked = cook("[Tel URL Scheme](tel://+452530579785)") expected = '

Tel URL Scheme

' expect(cooked).to eq(n expected) cooked2 = cook("[Steam URL Scheme](steam://+store/452530)") expected2 = '

Steam URL Scheme

' expect(cooked2).to eq(n expected2) end it "produces hashtag links" do category = Fabricate(:category, name: 'testing') category2 = Fabricate(:category, name: 'known') Fabricate(:topic, tags: [Fabricate(:tag, name: 'known')]) cooked = PrettyText.cook(" #unknown::tag #known #known::tag #testing") [ "#unknown::tag", "#known", "#known", "#testing" ].each do |element| expect(cooked).to include(element) end cooked = PrettyText.cook("[`a` #known::tag here](http://example.com)") html = <<~HTML

a #known::tag here

HTML expect(cooked).to eq(html.strip) cooked = PrettyText.cook("`a` #known::tag here") expect(cooked).to eq(html.strip) cooked = PrettyText.cook("test #known::tag") html = <<~HTML

test #known

HTML expect(cooked).to eq(html.strip) # ensure it does not fight with the autolinker expect(PrettyText.cook(' http://somewhere.com/#known')).not_to include('hashtag') expect(PrettyText.cook(' http://somewhere.com/?#known')).not_to include('hashtag') expect(PrettyText.cook(' http://somewhere.com/?abc#known')).not_to include('hashtag') end it "can handle mixed lists" do # known bug in old md engine cooked = PrettyText.cook("* a\n\n1. b") expect(cooked).to match_html("
    \n
  • a
  • \n
\n
    \n
  1. b
  2. \n
") end it "can handle traditional vs non traditional newlines" do SiteSetting.traditional_markdown_linebreaks = true expect(PrettyText.cook("1\n2")).to match_html "

1 2

" SiteSetting.traditional_markdown_linebreaks = false expect(PrettyText.cook("1\n2")).to match_html "

1
\n2

" end it "can handle emoji by name" do expected = <:smile::sunny:

HTML expect(PrettyText.cook(":smile::sunny:")).to eq(expected.strip) end it "handles emoji boundaries correctly" do cooked = PrettyText.cook("a,:man:t2:,b") expected = "

a,\":man:t2:\",b

" expect(cooked).to match(expected.strip) end it "can handle emoji by translation" do expected = "

\":wink:\"

" expect(PrettyText.cook(";)")).to eq(expected) end it "can handle multiple emojis by translation" do cooked = PrettyText.cook(":) ;) :)") expect(cooked.split("img").length - 1).to eq(3) end it "handles emoji boundries correctly" do expect(PrettyText.cook(",:)")).to include("emoji") expect(PrettyText.cook(":-)\n")).to include("emoji") expect(PrettyText.cook("a :)")).to include("emoji") expect(PrettyText.cook(":),")).not_to include("emoji") expect(PrettyText.cook("abcde ^:;-P")).to include("emoji") end describe "censoring" do after(:all) { Discourse.redis.flushdb } def expect_cooked_match(raw, expected_cooked) expect(PrettyText.cook(raw)).to eq(expected_cooked) end context "with basic words" do fab!(:watched_words) do ["shucks", "whiz", "whizzer", "a**le", "badword*", "shuck$", "cafรฉ", "$uper"].each do |word| Fabricate(:watched_word, action: WatchedWord.actions[:censor], word: word) end end it "works correctly" do expect_cooked_match("aw shucks, golly gee whiz.", "

aw โ– โ– โ– โ– โ– โ– , golly gee โ– โ– โ– โ– .

") end it "doesn't censor words unless they have boundaries." do expect_cooked_match("you are a whizzard! I love cheesewhiz. Whiz.", "

you are a whizzard! I love cheesewhiz. โ– โ– โ– โ– .

") end it "censors words even if previous partial matches exist." do expect_cooked_match("you are a whizzer! I love cheesewhiz. Whiz.", "

you are a โ– โ– โ– โ– โ– โ– โ– ! I love cheesewhiz. โ– โ– โ– โ– .

") end it "won't break links by censoring them." do expect_cooked_match("The link still works. [whiz](http://www.whiz.com)", '

The link still works. โ– โ– โ– โ– 

') end it "escapes regexp characters" do expect_cooked_match( "I have a pen, I have an a**le", "

I have a pen, I have an โ– โ– โ– โ– โ– 

" ) end it "works for words ending in non-word characters" do expect_cooked_match( "Aw shuck$, I can't fix the problem with money", "

Aw โ– โ– โ– โ– โ– โ– , I can't fix the problem with money

") end it "works for words ending in accented characters" do expect_cooked_match( "Let's go to a cafรฉ today", "

Let's go to a โ– โ– โ– โ–  today

") end it "works for words starting with non-word characters" do expect_cooked_match( "Discourse is $uper amazing", "

Discourse is โ– โ– โ– โ– โ–  amazing

") end it "handles * as wildcard" do expect_cooked_match( "No badword or apple here plz.", "

No โ– โ– โ– โ– โ– โ– โ–  or โ– โ– โ– โ– โ–  here plz.

") end end context "with watched words as regular expressions" do before { SiteSetting.watched_words_regular_expressions = true } it "supports words as regular expressions" do ["xyz*", "plee+ase"].each do |word| Fabricate(:watched_word, action: WatchedWord.actions[:censor], word: word) end expect_cooked_match("Pleased to meet you, but pleeeease call me later, xyz123", "

Pleased to meet you, but โ– โ– โ– โ– โ– โ– โ– โ– โ–  call me later, โ– โ– โ– 123

") end it "supports custom boundaries" do Fabricate(:watched_word, action: WatchedWord.actions[:censor], word: "\\btown\\b") expect_cooked_match("Meet downtown in your town at the townhouse on Main St.", "

Meet downtown in your โ– โ– โ– โ–  at the townhouse on Main St.

") end end end describe "watched words - replace" do after(:all) { Discourse.redis.flushdb } it "replaces words with other words" do Fabricate(:watched_word, action: WatchedWord.actions[:replace], word: "dolor sit*", replacement: "something else") expect(PrettyText.cook("Lorem ipsum dolor sit amet")).to match_html(<<~HTML)

Lorem ipsum something else amet

HTML expect(PrettyText.cook("Lorem ipsum dolor sits amet")).to match_html(<<~HTML)

Lorem ipsum something else amet

HTML expect(PrettyText.cook("Lorem ipsum dolor sittt amet")).to match_html(<<~HTML)

Lorem ipsum something else amet

HTML end it "replaces words with links" do Fabricate(:watched_word, action: WatchedWord.actions[:replace], word: "meta", replacement: "https://meta.discourse.org") expect(PrettyText.cook("Meta is a Discourse forum")).to match_html(<<~HTML)

Meta is a Discourse forum

HTML end it "works with regex" do Fabricate(:watched_word, action: WatchedWord.actions[:replace], word: "f.o", replacement: "test") expect(PrettyText.cook("foo")).to match_html("

foo

") expect(PrettyText.cook("f.o")).to match_html("

test

") SiteSetting.watched_words_regular_expressions = true expect(PrettyText.cook("foo")).to match_html("

test

") expect(PrettyText.cook("f.o")).to match_html("

test

") end it "supports overlapping words" do Fabricate(:watched_word, action: WatchedWord.actions[:replace], word: "discourse", replacement: "https://discourse.org") Fabricate(:watched_word, action: WatchedWord.actions[:replace], word: "is", replacement: "https://example.com") expect(PrettyText.cook("Meta is a Discourse forum")).to match_html(<<~HTML)

Meta is a Discourse forum

HTML end end it 'supports typographer' do SiteSetting.enable_markdown_typographer = true expect(PrettyText.cook('->')).to eq('

โ†’

') SiteSetting.enable_markdown_typographer = false expect(PrettyText.cook('->')).to eq('

->

') end it 'uses quotation marks from site settings' do SiteSetting.enable_markdown_typographer = true expect(PrettyText.cook(%q|"Do you know," he said, "what 'Discourse' is?"|)).to eq(%q|

โ€œDo you know,โ€ he said, โ€œwhat โ€˜Discourseโ€™ is?โ€

|) SiteSetting.markdown_typographer_quotation_marks = "โ€ž|โ€œ|โ€š|โ€˜" expect(PrettyText.cook(%q|"WeiรŸt du", sagte er, "was 'Discourse' ist?"|)).to eq(%q|

โ€žWeiรŸt duโ€œ, sagte er, โ€žwas โ€šDiscourseโ€˜ ist?โ€œ

|) end it 'handles onebox correctly' do expect(PrettyText.cook("http://a.com\nhttp://b.com").split("onebox").length).to eq(3) expect(PrettyText.cook("http://a.com\n\nhttp://b.com").split("onebox").length).to eq(3) expect(PrettyText.cook("a\nhttp://a.com")).to include('onebox') expect(PrettyText.cook("> http://a.com")).not_to include('onebox') expect(PrettyText.cook("a\nhttp://a.com a")).not_to include('onebox') expect(PrettyText.cook("a\nhttp://a.com\na")).to include('onebox') expect(PrettyText.cook("http://a.com")).to include('onebox') expect(PrettyText.cook("http://a.com ")).to include('onebox') expect(PrettyText.cook("http://a.com a")).not_to include('onebox') expect(PrettyText.cook("- http://a.com")).not_to include('onebox') expect(PrettyText.cook("")).not_to include('onebox') expect(PrettyText.cook(" http://a.com")).not_to include('onebox') expect(PrettyText.cook("a\n http://a.com")).not_to include('onebox') expect(PrettyText.cook("sam@sam.com")).not_to include('onebox') expect(PrettyText.cook("\nhttp://a.com")).to include('onebox') end it "can handle bbcode" do expect(PrettyText.cook("a[b]b[/b]c")).to eq('

abc

') expect(PrettyText.cook("a[i]b[/i]c")).to eq('

abc

') end it "can handle bbcode after a newline" do # this is not 100% ideal cause we get an extra p here, but this is pretty rare expect(PrettyText.cook("a\n[code]code[/code]")).to eq("

a

\n
code
") # this is fine expect(PrettyText.cook("a\na[code]code[/code]")).to eq("

a
\nacode

") end it "can onebox local topics" do op = Fabricate(:post) reply = Fabricate(:post, topic_id: op.topic_id) url = Discourse.base_url + reply.url quote = create_post(topic_id: op.topic.id, raw: "This is a sample reply with a quote\n\n#{url}") quote.reload expect(quote.cooked).not_to include('[quote') end it "supports tables" do markdown = <<~MD | Tables | Are | Cool | | ------------- |:-------------:| -----:| | col 3 is | right-aligned | $1600 | MD expected = <<~HTML
Tables Are Cool
col 3 is right-aligned $1600
HTML expect(PrettyText.cook(markdown)).to eq(expected.strip) end it "supports img bbcode" do cooked = PrettyText.cook "[img]http://www.image/test.png[/img]" html = "

\"\"

" expect(cooked).to eq(html) end it "provides safety for img bbcode" do cooked = PrettyText.cook "[img]http://aaa.com[/img]" html = '

' expect(cooked).to eq(html) end it "supports email bbcode" do cooked = PrettyText.cook "[email]sam@sam.com[/email]" html = '

sam@sam.com

' expect(cooked).to eq(html) end it "supports url bbcode" do cooked = PrettyText.cook "[url]http://sam.com[/url]" html = '

http://sam.com

' expect(cooked).to eq(html) end it "supports nesting tags in url" do cooked = PrettyText.cook("[url=http://sam.com][b]I am sam[/b][/url]") html = '

I am sam

' expect(cooked).to eq(html) end it "supports query params in bbcode url" do cooked = PrettyText.cook("[url=https://www.amazon.com/Camcorder-Hausbell-302S-Control-Infrared/dp/B01KLOA1PI/?tag=discourse]BBcode link[/url]") html = '

BBcode link

' expect(cooked).to eq(html) end it "supports inline code bbcode" do cooked = PrettyText.cook "Testing [code]codified **stuff** and `more` stuff[/code]" html = "

Testing codified **stuff** and `more` stuff

" expect(cooked).to eq(html) end it "supports block code bbcode" do cooked = PrettyText.cook "[code]\ncodified\n\n\n **stuff** and `more` stuff\n[/code]" html = "
codified\n\n\n  **stuff** and `more` stuff
" expect(cooked).to eq(html) end it "support special handling for space in urls" do cooked = PrettyText.cook "http://testing.com?a%20b" html = '

http://testing.com?a%20b

' expect(cooked).to eq(html) end it "supports onebox for decoded urls" do cooked = PrettyText.cook "http://testing.com?a%50b" html = '

http://testing.com?aPb

' expect(cooked).to eq(html) end it "should sanitize the html" do expect(PrettyText.cook("alert(42)")).to eq "

alert(42)

" end it "should not onebox magically linked urls" do expect(PrettyText.cook('[url]site.com[/url]')).not_to include('onebox') end it "should sanitize the html" do expect(PrettyText.cook("

hi

")).to eq "

hi

" end it "should strip SCRIPT" do expect(PrettyText.cook("")).to eq "" end it "should allow sanitize bypass" do expect(PrettyText.cook("alert(42)", sanitize: false)).to eq "

alert(42)

" end # custom rule used to specify image dimensions via alt tags describe "image dimensions" do it "allows title plus dimensions" do cooked = PrettyText.cook <<~MD ![title with | title|220x100](http://png.com/my.png) ![](http://png.com/my.png) ![|220x100](http://png.com/my.png) ![stuff](http://png.com/my.png) ![|220x100,50%](http://png.com/my.png "some title") MD html = <<~HTML

title with | title


stuff

HTML expect(cooked).to eq(html.strip) end it "ignores whitespace and allows scaling by percent, width, height" do cooked = PrettyText.cook <<~MD ![|220x100, 50%](http://png.com/my.png) ![|220x100 , 50%](http://png.com/my.png) ![|220x100 ,50%](http://png.com/my.png) ![|220x100,150x](http://png.com/my.png) ![|220x100, x50](http://png.com/my.png) MD html = <<~HTML





HTML expect(cooked).to eq(html.strip) end end describe "upload decoding" do it "can decode upload:// for default setup" do set_cdn_url('https://cdn.com') upload = Fabricate(:upload) raw = <<~RAW ![upload](#{upload.short_url}) ![upload](#{upload.short_url} "some title to test") - ![upload](#{upload.short_url}) - test - ![upload](#{upload.short_url}) ![upload](#{upload.short_url.gsub(".png", "")}) [some attachment](#{upload.short_url}) [some attachment|attachment](#{upload.short_url}) [some attachment|random](#{upload.short_url}) RAW cdn_url = Discourse.store.cdn_url(upload.url) cooked = <<~HTML

upload

upload

  • upload

  • test

    • upload

upload

some attachment

some attachment

some attachment|random

HTML expect(PrettyText.cook(raw)).to eq(cooked.strip) end it "can place a blank image if we can not find the upload" do raw = <<~MD ![upload](upload://abcABC.png) [some attachment|attachment](upload://abcdefg.png) MD cooked = <<~HTML

upload

some attachment

HTML expect(PrettyText.cook(raw)).to eq(cooked.strip) end end it "can properly allowlist iframes" do SiteSetting.allowed_iframes = "https://bob.com/a|http://silly.com?EMBED=" raw = <<~IFRAMES IFRAMES # we require explicit HTTPS here html = <<~IFRAMES IFRAMES cooked = PrettyText.cook(raw).strip expect(cooked).to eq(html.strip) end it "You can disable linkify" do md = "www.cnn.com test.it http://test.com https://test.ab https://a" cooked = PrettyText.cook(md) html = <<~HTML

www.cnn.com test.it http://test.com https://test.ab https://a

HTML expect(cooked).to eq(html.strip) # notice how cnn.com is no longer linked but it is SiteSetting.markdown_linkify_tlds = "not_com|it" cooked = PrettyText.cook(md) html = <<~HTML

www.cnn.com test.it http://test.com https://test.ab https://a

HTML expect(cooked).to eq(html.strip) # no tlds anymore SiteSetting.markdown_linkify_tlds = "" cooked = PrettyText.cook(md) html = <<~HTML

www.cnn.com test.it http://test.com https://test.ab https://a

HTML expect(cooked).to eq(html.strip) # lastly ... what about no linkify SiteSetting.enable_markdown_linkify = false cooked = PrettyText.cook(md) html = <<~HTML

www.cnn.com test.it http://test.com https://test.ab https://a

HTML end it "has a proper data whitlist on div" do cooked = PrettyText.cook("
test
") expect(cooked).to include("data-theme-a") end it "allowlists lang attribute" do cooked = PrettyText.cook("

tester

tester
tester") expect(cooked).to eq("

tester

tester
tester") end it "allowlists ruby tags" do # read all about ruby chars at: https://en.wikipedia.org/wiki/Ruby_character # basically it is super hard to remember every single rare letter when there are # so many, so ruby tags provide a hint. # html = (<<~MD).strip X ๆผข ( ใ„ใ„ขห‹ ) MD cooked = PrettyText.cook html expect(cooked).to eq(html) end describe "d-wrap" do it "wraps the [wrap] tag inline" do cooked = PrettyText.cook("[wrap=toc]taco[/wrap]") html = <<~HTML

taco

HTML expect(cooked).to eq(html.strip) cooked = PrettyText.cook("Hello [wrap=toc id=1]taco[/wrap] world") html = <<~HTML

Hello taco world

HTML expect(cooked).to eq(html.strip) end it "wraps the [wrap] tag in block" do # can interfere with parsing SiteSetting.enable_markdown_typographer = true md = <<~MD [wrap=toc id="aโ€ aa='b"' bb="f'"] taco1 [/wrap] MD cooked = PrettyText.cook(md) html = <<~HTML

taco1

HTML expect(cooked).to eq(html.strip) end it "wraps the [wrap] tag without content" do md = <<~MD [wrap=toc] [/wrap] MD cooked = PrettyText.cook(md) html = <<~HTML
HTML expect(cooked).to eq(html.strip) end it "adds attributes as data-attributes" do cooked = PrettyText.cook("[wrap=toc name=\"single quote's\" id='1\"2']taco[/wrap]") html = <<~HTML

taco

HTML expect(cooked).to eq(html.strip) end it "prevents xss" do cooked = PrettyText.cook('[wrap=toc foo=""]taco[/wrap]') html = <<~HTML

taco

HTML expect(cooked).to eq(html.strip) end it "allows a limited set of attributes chars" do cooked = PrettyText.cook('[wrap=toc fo@"รจk-"!io=bar]taco[/wrap]') html = <<~HTML

taco

HTML expect(cooked).to eq(html.strip) end end it "adds anchor links to headings" do cooked = PrettyText.cook('# Hello world') html = <<~HTML

Hello world

HTML expect(cooked).to match_html(html) end end