"use strict"; const expect = require("chai").expect; import {renderToString} from "@vue/server-test-utils"; import ParsedMessageTestWrapper from "../../components/ParsedMessageTestWrapper.vue"; async function getParsedMessageContents(text, message) { let contents = await renderToString(ParsedMessageTestWrapper, { propsData: { text, message, }, }); // The wrapper adds a surrounding div to the message html, so we clean that out here contents = contents.replace(/^<div data-server-rendered="true">([^]+)<\/div>$/m, "$1"); return contents; } describe("IRC formatted message parser", () => { it("should not introduce xss", async () => { const testCases = [ { input: "<img onerror='location.href=\"//youtube.com\"'>", expected: '<img onerror=\'location.href="<a href="http://youtube.com" dir="auto" target="_blank" rel="noopener">//youtube.com</a>"\'>', }, { input: '#&">bug', expected: '<span dir="auto" role="button" tabindex="0" class="inline-channel">#&">bug</span>', }, ]; const actual = await Promise.all( testCases.map((testCase) => getParsedMessageContents(testCase.input)) ); const expected = testCases.map((testCase) => testCase.expected); expect(actual).to.deep.equal(expected); }); it("should skip all <32 ASCII codes except linefeed", async () => { const testCases = [ { input: "\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1B\x1D\x1D\x1E\x1Ftext\x0Awithcontrolcodestest", expected: ' <span class="irc-bold"> </span> <span class="irc-monospace"> </span><span class="irc-underline irc-strikethrough irc-monospace">text\nwithcontrolcodestest</span>', }, ]; const actual = await Promise.all( testCases.map((testCase) => getParsedMessageContents(testCase.input)) ); const expected = testCases.map((testCase) => testCase.expected); expect(actual).to.deep.equal(expected); }); it("should find urls", async () => { const testCases = [ { input: "irc://freenode.net/thelounge", expected: '<a href="irc://freenode.net/thelounge" dir="auto" target="_blank" rel="noopener">' + "irc://freenode.net/thelounge" + "</a>", }, { input: "www.nooooooooooooooo.com", expected: '<a href="http://www.nooooooooooooooo.com" dir="auto" target="_blank" rel="noopener">' + "www.nooooooooooooooo.com" + "</a>", }, { input: "look at https://thelounge.chat/ for more information", expected: "look at " + '<a href="https://thelounge.chat/" dir="auto" target="_blank" rel="noopener">' + "https://thelounge.chat/" + "</a>" + " for more information", }, { input: "use www.duckduckgo.com for privacy reasons", expected: "use " + '<a href="http://www.duckduckgo.com" dir="auto" target="_blank" rel="noopener">' + "www.duckduckgo.com" + "</a>" + " for privacy reasons", }, { input: "svn+ssh://example.org", expected: '<a href="svn+ssh://example.org" dir="auto" target="_blank" rel="noopener">' + "svn+ssh://example.org" + "</a>", }, ]; const actual = await Promise.all( testCases.map((testCase) => getParsedMessageContents(testCase.input)) ); const expected = testCases.map((testCase) => testCase.expected); expect(actual).to.deep.equal(expected); }); it("url with a dot parsed correctly", async () => { const input = "bonuspunkt: your URL parser misparses this URL: https://msdn.microsoft.com/en-us/library/windows/desktop/ms644989(v=vs.85).aspx"; const correctResult = "bonuspunkt: your URL parser misparses this URL: " + '<a href="https://msdn.microsoft.com/en-us/library/windows/desktop/ms644989(v=vs.85).aspx" dir="auto" target="_blank" rel="noopener">' + "https://msdn.microsoft.com/en-us/library/windows/desktop/ms644989(v=vs.85).aspx" + "</a>"; const actual = await getParsedMessageContents(input); expect(actual).to.deep.equal(correctResult); }); it("should balance brackets", async () => { const testCases = [ { input: "<https://theos.kyriasis.com/~kyrias/stats/archlinux.html>", expected: "<" + '<a href="https://theos.kyriasis.com/~kyrias/stats/archlinux.html" dir="auto" target="_blank" rel="noopener">' + "https://theos.kyriasis.com/~kyrias/stats/archlinux.html" + "</a>" + ">", }, { input: "abc (www.example.com)", expected: "abc (" + '<a href="http://www.example.com" dir="auto" target="_blank" rel="noopener">' + "www.example.com" + "</a>" + ")", }, { input: "http://example.com/Test_(Page)", expected: '<a href="http://example.com/Test_(Page)" dir="auto" target="_blank" rel="noopener">' + "http://example.com/Test_(Page)" + "</a>", }, { input: "www.example.com/Test_(Page)", expected: '<a href="http://www.example.com/Test_(Page)" dir="auto" target="_blank" rel="noopener">' + "www.example.com/Test_(Page)" + "</a>", }, ]; const actual = await Promise.all( testCases.map((testCase) => getParsedMessageContents(testCase.input)) ); const expected = testCases.map((testCase) => testCase.expected); expect(actual).to.deep.equal(expected); }); it("should not find urls", async () => { const testCases = [ { input: "text www. text", expected: "text www. text", }, { input: "http://.", expected: "http://.", }, ]; const actual = await Promise.all( testCases.map((testCase) => getParsedMessageContents(testCase.input)) ); const expected = testCases.map((testCase) => testCase.expected); expect(actual).to.deep.equal(expected); }); it("should find channels", async () => { const testCases = [ { input: "#a", expected: '<span dir="auto" role="button" tabindex="0" class="inline-channel">' + "#a" + "</span>", }, { input: "#test", expected: '<span dir="auto" role="button" tabindex="0" class="inline-channel">' + "#test" + "</span>", }, { input: "#äöü", expected: '<span dir="auto" role="button" tabindex="0" class="inline-channel">' + "#äöü" + "</span>", }, { input: "inline #channel text", expected: "inline " + '<span dir="auto" role="button" tabindex="0" class="inline-channel">' + "#channel" + "</span>" + " text", }, { input: "#1,000", expected: '<span dir="auto" role="button" tabindex="0" class="inline-channel">' + "#1,000" + "</span>", }, { input: "@#a", expected: "@" + '<span dir="auto" role="button" tabindex="0" class="inline-channel">' + "#a" + "</span>", }, ]; const actual = await Promise.all( testCases.map((testCase) => getParsedMessageContents(testCase.input)) ); const expected = testCases.map((testCase) => testCase.expected); expect(actual).to.deep.equal(expected); }); it("should not find channels", async () => { const testCases = [ { input: "hi#test", expected: "hi#test", }, { input: "#", expected: "#", }, ]; const actual = await Promise.all( testCases.map((testCase) => getParsedMessageContents(testCase.input)) ); const expected = testCases.map((testCase) => testCase.expected); expect(actual).to.deep.equal(expected); }); [ { name: "bold", input: "\x02bold", expected: '<span class="irc-bold">bold</span>', }, { name: "foreground color", input: "\x038yellowText", expected: '<span class="irc-fg8">yellowText</span>', }, { name: "foreground and background colors (white)", input: "\x030,0white,white", expected: '<span class="irc-fg0 irc-bg0">white,white</span>', }, { name: "foreground and background colors", input: "\x034,8yellowBGredText", expected: '<span class="irc-fg4 irc-bg8">yellowBGredText</span>', }, { name: "hex foreground color", input: "\x04663399rebeccapurple", expected: '<span style="color:#663399;">rebeccapurple</span>', }, { name: "hex foreground and background colors", input: "\x04415364,ff9e18The Lounge", expected: '<span style="color:#415364;background-color:#FF9E18;">The Lounge</span>', }, { name: "italic", input: "\x1ditalic", expected: '<span class="irc-italic">italic</span>', }, { name: "underline", input: "\x1funderline", expected: '<span class="irc-underline">underline</span>', }, { name: "strikethrough", input: "\x1estrikethrough", expected: '<span class="irc-strikethrough">strikethrough</span>', }, { name: "monospace", input: "\x11monospace", expected: '<span class="irc-monospace">monospace</span>', }, { name: "reverse with foreground and background colors", input: "\x0313,1fg and bg \x16and reversed", expected: '<span class="irc-fg13 irc-bg1">fg and bg </span>' + '<span class="irc-fg1 irc-bg13">and reversed</span>', }, { name: "toggle reverse with foreground and background colors", input: "\x0305,11text \x16reversed and \x16back again and \x16reversed", expected: '<span class="irc-fg5 irc-bg11">text </span>' + '<span class="irc-fg11 irc-bg5">reversed and </span>' + '<span class="irc-fg5 irc-bg11">back again and </span>' + '<span class="irc-fg11 irc-bg5">reversed</span>', }, { name: "escape reverse when new colors are applied", input: "\x0311,02text \x16 reversed \x0304,05 and new style", expected: '<span class="irc-fg11 irc-bg2">text </span>' + '<span class="irc-fg2 irc-bg11"> reversed </span>' + '<span class="irc-fg4 irc-bg5"> and new style</span>', }, { name: "resets", input: "\x02bold\x038yellow\x02nonBold\x03default", expected: '<span class="irc-bold">bold</span>' + '<span class="irc-bold irc-fg8">yellow</span>' + '<span class="irc-fg8">nonBold</span>' + "default", }, { name: "duplicates", input: "\x02bold\x02 \x02bold\x02", expected: '<span class="irc-bold">bold</span>' + " " + '<span class="irc-bold">bold</span>', }, ].forEach(({name, input, expected}) => { it(`should handle style characters: ${name}`, async () => { expect(await getParsedMessageContents(input)).to.equal(expected); }); }); it("should find nicks", async () => { const testCases = [ { message: { users: ["MaxLeiter"], }, input: "test, MaxLeiter", expected: "test, " + '<span data-name="MaxLeiter" role="button" dir="auto" class="user color-12">' + "MaxLeiter" + "</span>", }, ]; const actual = await Promise.all( testCases.map((testCase) => getParsedMessageContents(testCase.input, testCase.message)) ); const expected = testCases.map((testCase) => testCase.expected); expect(actual).to.deep.equal(expected); }); it("should not find nicks", async () => { const testCases = [ { users: ["MaxLeiter, test"], input: "#test-channelMaxLeiter", expected: '<span dir="auto" role="button" tabindex="0" class="inline-channel">' + "#test-channelMaxLeiter" + "</span>", }, { users: ["MaxLeiter, test"], input: "https://www.MaxLeiter.com/test", expected: '<a href="https://www.MaxLeiter.com/test" dir="auto" target="_blank" rel="noopener">' + "https://www.MaxLeiter.com/test" + "</a>", }, ]; const actual = await Promise.all( testCases.map((testCase) => getParsedMessageContents(testCase.input)) ); const expected = testCases.map((testCase) => testCase.expected); expect(actual).to.deep.equal(expected); }); it("should go bonkers like mirc", async () => { const testCases = [ { input: "\x02irc\x0f://\x1dfreenode.net\x0f/\x034,8thelounge", expected: '<a href="irc://freenode.net/thelounge" dir="auto" target="_blank" rel="noopener">' + '<span class="irc-bold">irc</span>' + "://" + '<span class="irc-italic">freenode.net</span>' + "/" + '<span class="irc-fg4 irc-bg8">thelounge</span>' + "</a>", }, { input: "\x02#\x038,9thelounge", expected: '<span dir="auto" role="button" tabindex="0" class="inline-channel">' + '<span class="irc-bold">#</span>' + '<span class="irc-bold irc-fg8 irc-bg9">thelounge</span>' + "</span>", }, ]; const actual = await Promise.all( testCases.map((testCase) => getParsedMessageContents(testCase.input)) ); const expected = testCases.map((testCase) => testCase.expected); expect(actual).to.deep.equal(expected); }); // Emoji [ { name: "in text", input: "Hello💬", expected: 'Hello<span role="img" aria-label="Emoji: speech balloon" title="Emoji: speech balloon" class="emoji">💬</span>', }, { name: "complicated zero-join-width emoji", input: "🤦🏿♀️", expected: '<span role="img" aria-label="Emoji: woman facepalming" title="Emoji: woman facepalming" class="emoji">🤦🏿♀️</span>', }, { name: "unicode 12 emojis", input: "🧘🏿👨👨👧👧", expected: '<span role="img" aria-label="Emoji: person in lotus position" title="Emoji: person in lotus position" class="emoji">🧘🏿</span><span role="img" aria-label="Emoji: family: man, man, girl, girl" title="Emoji: family: man, man, girl, girl" class="emoji">👨👨👧👧</span>', }, { name: "unicode 12 emojis with multiple modifiers", input: "👩🏾🤝👨🏽", expected: '<span role="img" class="emoji">👩🏾🤝👨🏽</span>', }, { name: "with modifiers", input: "🤷♀️", expected: '<span role="img" aria-label="Emoji: woman shrugging" title="Emoji: woman shrugging" class="emoji">🤷♀️</span>', }, { name: "with emoji variant selector", input: "\u{2695}\u{FE0F}", expected: '<span role="img" aria-label="Emoji: medical symbol" title="Emoji: medical symbol" class="emoji">\u{2695}\u{FE0F}</span>', }, { name: "with text variant selector", input: "\u{2695}\u{FE0E}", expected: "\u{2695}\u{FE0E}", // this does not match because FE0E is specifically a text variant }, { name: "without variant selector", input: "\u{2695}", expected: "\u{2695}", // this does not match because emoji-regex expects \uFE0F as per the emoji specification }, { // FIXME: These multiple `span`s should be optimized into a single one. See https://github.com/thelounge/thelounge/issues/1783 name: "wrapped in style", input: "Super \x034💚 green!", expected: 'Super <span role="img" aria-label="Emoji: green heart" title="Emoji: green heart" class="emoji"><span class="irc-fg4">💚</span></span><span class="irc-fg4"> green!</span>', }, { name: "wrapped in URLs", input: "https://i.❤️.thelounge.chat", // FIXME: Emoji in text should be `<span class="emoji">❤️</span>`. See https://github.com/thelounge/thelounge/issues/1784 expected: '<a href="https://i.❤️.thelounge.chat" dir="auto" target="_blank" rel="noopener">https://i.❤️.thelounge.chat</a>', }, { name: "wrapped in channels", input: "#i❤️thelounge", // FIXME: Emoji in text should be `<span class="emoji">❤️</span>`. See https://github.com/thelounge/thelounge/issues/1784 expected: '<span dir="auto" role="button" tabindex="0" class="inline-channel">#i❤️thelounge</span>', }, ].forEach(({name, input, expected}) => { it(`should find emoji: ${name}`, async () => { expect(await getParsedMessageContents(input)).to.equal(expected); }); }); it("should optimize generated html", async () => { const testCases = [ { input: 'test \x0312#\x0312\x0312"te\x0312st\x0312\x0312\x0312\x0312\x0312\x0312\x0312\x0312\x0312\x0312\x0312a', expected: "test " + '<span dir="auto" role="button" tabindex="0" class="inline-channel">' + '<span class="irc-fg12">#"testa</span>' + "</span>", }, ]; const actual = await Promise.all( testCases.map((testCase) => getParsedMessageContents(testCase.input)) ); const expected = testCases.map((testCase) => testCase.expected); expect(actual).to.deep.equal(expected); }); it("should trim common protocols", async () => { const testCases = [ { input: "like..http://example.com", expected: "like.." + '<a href="http://example.com" dir="auto" target="_blank" rel="noopener">' + "http://example.com" + "</a>", }, { input: "like..HTTP://example.com", expected: "like.." + '<a href="HTTP://example.com" dir="auto" target="_blank" rel="noopener">' + "HTTP://example.com" + "</a>", }, ]; const actual = await Promise.all( testCases.map((testCase) => getParsedMessageContents(testCase.input)) ); const expected = testCases.map((testCase) => testCase.expected); expect(actual).to.deep.equal(expected); }); it("should not find channel in fragment", async () => { const testCases = [ { input: "http://example.com/#hash", expected: '<a href="http://example.com/#hash" dir="auto" target="_blank" rel="noopener">' + "http://example.com/#hash" + "</a>", }, ]; const actual = await Promise.all( testCases.map((testCase) => getParsedMessageContents(testCase.input)) ); const expected = testCases.map((testCase) => testCase.expected); expect(actual).to.deep.equal(expected); }); it("should not overlap parts", async () => { const input = "Url: http://example.com/path Channel: ##channel"; const actual = await getParsedMessageContents(input); expect(actual).to.equal( 'Url: <a href="http://example.com/path" dir="auto" target="_blank" rel="noopener">http://example.com/path</a> ' + 'Channel: <span dir="auto" role="button" tabindex="0" class="inline-channel">##channel</span>' ); }); it("should handle overlapping parts by using first starting", async () => { const input = "#test-https://example.com"; const actual = await getParsedMessageContents(input); expect(actual).to.equal( '<span dir="auto" role="button" tabindex="0" class="inline-channel">' + "#test-https://example.com" + "</span>" ); }); it("should find links separated by tab character", async () => { const input = "example.com\texample.org"; const actual = await getParsedMessageContents(input); expect(actual).to.equal( '<a href="http://example.com" dir="auto" target="_blank" rel="noopener">example.com</a>' + ' <a href="http://example.org" dir="auto" target="_blank" rel="noopener">example.org</a>' ); }); });