# 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) { "
" } 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 = <<~HTMLThis is a quote with a regular emoji :upside_down_face:
This is a quote with an emoji shortcut :)
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 = <<~HTMLThis is a quote with a Unicode emoji ๐
This is a quote with a regular emoji
This is a quote with an emoji shortcut :)
HTML expect(cook(md)).to eq(html.strip) end it "unescapes all emojis" do html = <<~HTMLThis is a quote with a Unicode emoji
This is a quote with a regular emoji
This is a quote with an emoji shortcut
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("This is a quote with a Unicode emoji
[constructor]
\ntest
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
hi
\n@ss
hi
\n@s.
hi
\n@s.s
hi
\n@.s.s
hi
\n@user. @GROUP @somemention @group2
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 '' end it "can handle mentions inside a hyperlink" do expect(PrettyText.cook("[link @inner](http://site.com)")).to match_html '' 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 = <<~HTMLX
#
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
$redis.flushall
end
end
end
describe "rel nofollow" 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(/nofollow noopener/)
end
it "should not inject nofollow in all local links" do
expect(PrettyText.cook("cnn") !~ /nofollow/).to eq(true)
end
it "should not inject nofollow in all subdomain links" do
expect(PrettyText.cook("cnn") !~ /nofollow/).to eq(true)
end
it "should inject nofollow in all non subdomain links" do
expect(PrettyText.cook("cnn")).to match(/nofollow/)
end
it "should not inject nofollow for foo.com" do
expect(PrettyText.cook("cnn") !~ /nofollow/).to eq(true)
end
it "should inject nofollow for afoo.com" do
expect(PrettyText.cook("cnn")).to match(/nofollow/)
end
it "should not inject nofollow for bar.foo.com" do
expect(PrettyText.cook("cnn") !~ /nofollow/).to eq(true)
end
it "should not inject nofollow if omit_nofollow option is given" do
expect(PrettyText.cook('cnn', omit_nofollow: true) !~ /nofollow/).to eq(true)
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("", 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 spoilers" do
expect(PrettyText.excerpt("", 100)).to match_html "[image]"
expect(PrettyText.excerpt("spoiler", 100)).to match_html "spoiler"
end
it "should keep details if too long" do
expect(PrettyText.excerpt("hello
hello
", 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("<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 = ""
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 you "
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 = ""
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
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::HTML.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 end it 'Is smart about linebreaks and IMG tags' do raw = <<~MD a a - li ``` test ``` ``` test ``` MD html = <<~HTMLa
a
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 "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(/\:slight_smile\:/)
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
") expect(PrettyText.cook("hello ๐ฉโ๐ค")).to eq("hello
") expect(PrettyText.cook("hello ๐ฉ๐พโ๐")).to eq("hello
") expect(PrettyText.cook("hello ๐คทโโ๏ธ")).to eq("hello
") 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 it "replaces skin toned emoji" do expect(PrettyText.cook("hello ๐ฑ๐ฟโโ๏ธ")).to eq("hello
") expect(PrettyText.cook("hello ๐ฉโ๐ค")).to eq("hello
") expect(PrettyText.cook("hello ๐ฉ๐พโ๐")).to eq("hello
") expect(PrettyText.cook("hello ๐คทโโ๏ธ")).to eq("hello
") 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 = '' 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 = '' 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 = '' expect(cooked).to eq(n expected) cooked2 = cook("[Steam URL Scheme](steam://+store/452530)") expected2 = '' 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 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 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("1 2
" SiteSetting.traditional_markdown_linebreaks = false expect(PrettyText.cook("1\n2")).to match_html "1
\n2
a,,b
" expect(cooked).to match(expected.strip) end it "can handle emoji by translation" do expected = "" 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 it 'can censor words correctly' do begin ['apple', 'banana'].each { |w| Fabricate(:watched_word, word: w, action: WatchedWord.actions[:censor]) } expect(PrettyText.cook('yay banana yay')).not_to include('banana') expect(PrettyText.cook('yay `banana` yay')).not_to include('banana') expect(PrettyText.cook("# banana")).not_to include('banana') expect(PrettyText.cook("# banana")).to include("\u25a0\u25a0") ensure $redis.flushall end end it 'supports typographer' do SiteSetting.enable_markdown_typographer = true expect(PrettyText.cook('(tm)')).to eq('โข
') SiteSetting.enable_markdown_typographer = false expect(PrettyText.cook('(tm)')).to eq('(tm)
') 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("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
\ncode
")
# this is fine
expect(PrettyText.cook("a\na[code]code[/code]")).to eq("a
\nacode
Tables | Are | Cool |
---|---|---|
col 3 is | right-aligned | $1600 |
Testing codified **stuff** and `more` stuff
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 = ''
expect(cooked).to eq(html)
end
it "supports onebox for decoded urls" do
cooked = PrettyText.cook "http://testing.com?a%50b"
html = ''
expect(cooked).to eq(html)
end
it "should sanitize the html" do
expect(PrettyText.cook("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("
test
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 = <<~HTMLwww.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 = <<~HTMLwww.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 = <<~HTMLwww.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("tester
tester
taco
Hello taco world
HTML expect(cooked).to eq(html.strip) end it "wraps the [wrap] tag in block" do md = <<~MD [wrap=toc] taco [/wrap] MD cooked = PrettyText.cook(md) html = <<~HTMLtaco
taco
taco
taco