From 9b55c4565b2d1f09e122cd25be5a89b7c3d31e09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20K=C3=BChn?= Date: Thu, 15 Jun 2023 16:18:04 +0000 Subject: [PATCH] [FIX] mail: linkify message content MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit closes odoo/odoo#125307 X-original-commit: 406ff4727e361588bded08c6719f932a8b800b35 Signed-off-by: Sébastien Theys (seb) Signed-off-by: Alexandre Kühn (aku) --- addons/mail/static/src/utils/common/format.js | 53 ++++++++++++------- addons/mail/static/tests/mail_utils_tests.js | 10 ++-- 2 files changed, 39 insertions(+), 24 deletions(-) diff --git a/addons/mail/static/src/utils/common/format.js b/addons/mail/static/src/utils/common/format.js index e1005ee1d59..f55c7a2f083 100644 --- a/addons/mail/static/src/utils/common/format.js +++ b/addons/mail/static/src/utils/common/format.js @@ -8,6 +8,25 @@ import { url } from "@web/core/utils/urls"; const urlRegexp = /\b(?:https?:\/\/\d{1,3}(?:\.\d{1,3}){3}|(?:https?:\/\/|(?:www\.))[-a-z0-9@:%._+~#=\u00C0-\u024F\u1E00-\u1EFF]{2,256}\.[a-z]{2,13})\b(?:[-a-z0-9@:%_+~#?&[\]^|{}`\\'$//=\u00C0-\u024F\u1E00-\u1EFF]|,(?!$| )|\.(?!$| |\.)|;(?!$| ))*/gi; +/** + * Escape < > & as html entities + * + * @param {string} + * @return {string} + */ +const _escapeEntities = (function () { + const map = { "&": "&", "<": "<", ">": ">" }; + const escaper = function (match) { + return map[match]; + }; + const testRegexp = RegExp("(?:&|<|>)"); + const replaceRegexp = RegExp("(?:&|<|>)", "g"); + return function (string) { + string = string == null ? "" : "" + string; + return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string; + }; +})(); + /** * @param rawBody {string} * @param validRecords {Object} @@ -40,7 +59,7 @@ export async function prettifyMessageContent(rawBody, validRecords = []) { * @param {function} transformFunction * @returns {string} */ -function parseAndTransform(htmlString, transformFunction) { +export function parseAndTransform(htmlString, transformFunction) { const openToken = "OPEN" + Date.now(); const string = htmlString.replace(/</g, openToken); let children; @@ -80,29 +99,25 @@ function _parseAndTransform(nodes, transformFunction) { /** * @param {string} text - * @param {Object} [attrs={}] * @return {string} linkified text */ -function linkify(text, attrs) { - attrs = attrs || {}; - if (attrs.target === undefined) { - attrs.target = "_blank"; +function linkify(text) { + let curIndex = 0; + let result = ""; + let match; + while ((match = urlRegexp.exec(text)) !== null) { + result += _escapeEntities(text.slice(curIndex, match.index)); + const url = match[0]; + const href = encodeURI(!/^https?:\/\//i.test(url) ? "http://" + url : url); + result += `${_escapeEntities( + url + )}`; + curIndex = match.index + match[0].length; } - if (attrs.target === "_blank") { - attrs.rel = "noreferrer noopener"; - } - attrs = Object.keys(attrs) - .map(function (key) { - return key + '="' + escape(attrs[key]) + '"'; - }) - .join(" "); - return text.replace(urlRegexp, function (url) { - var href = !/^https?:\/\//i.test(url) ? "http://" + url : url; - return "' + url + ""; - }); + return result + _escapeEntities(text.slice(curIndex)); } -function addLink(node, transformChildren) { +export function addLink(node, transformChildren) { if (node.nodeType === 3) { // text node const linkified = linkify(node.data); diff --git a/addons/mail/static/tests/mail_utils_tests.js b/addons/mail/static/tests/mail_utils_tests.js index 6f6853953b1..df7ae3c57af 100644 --- a/addons/mail/static/tests/mail_utils_tests.js +++ b/addons/mail/static/tests/mail_utils_tests.js @@ -1,6 +1,6 @@ /* @odoo-module */ -import * as utils from "@mail/js/utils"; +import { addLink, parseAndTransform } from "@mail/utils/common/format"; import { start, startServer } from "@mail/../tests/helpers/test_utils"; QUnit.module("Mail utils"); @@ -21,7 +21,7 @@ QUnit.test("add_link utility function", function (assert) { }; for (const [content, willLinkify] of Object.entries(testInputs)) { - const output = utils.parseAndTransform(content, utils.addLink); + const output = parseAndTransform(content, addLink); if (willLinkify) { assert.strictEqual(output.indexOf(""), output.length - 4); @@ -55,14 +55,14 @@ QUnit.test("addLink: utility function and special entities", function (assert) { }; for (const [content, result] of Object.entries(testInputs)) { - const output = utils.parseAndTransform(content, utils.addLink); + const output = parseAndTransform(content, addLink); assert.strictEqual(output, result); } }); QUnit.test("addLink: linkify inside text node (1 occurrence)", function (assert) { const content = "

some text https://somelink.com

"; - const linkified = utils.parseAndTransform(content, utils.addLink); + const linkified = parseAndTransform(content, addLink); assert.ok(linkified.startsWith("

some text

")); @@ -83,7 +83,7 @@ QUnit.test("addLink: linkify inside text node (2 occurrences)", function (assert // stringified representation, we continue deeper assertion with query // selectors. const content = "

some text https://somelink.com and again https://somelink2.com ...

"; - const linkified = utils.parseAndTransform(content, utils.addLink); + const linkified = parseAndTransform(content, addLink); const fragment = document.createDocumentFragment(); const div = document.createElement("div"); fragment.appendChild(div);