diff --git a/addons/web_editor/static/src/js/editor/odoo-editor/src/utils/sanitize.js b/addons/web_editor/static/src/js/editor/odoo-editor/src/utils/sanitize.js index 73020c23a07..5dba980e148 100644 --- a/addons/web_editor/static/src/js/editor/odoo-editor/src/utils/sanitize.js +++ b/addons/web_editor/static/src/js/editor/odoo-editor/src/utils/sanitize.js @@ -20,6 +20,7 @@ import { ancestors, EMAIL_REGEX, URL_REGEX_WITH_INFOS, + PHONE_REGEX, } from './utils.js'; const NOT_A_NUMBER = /[^\d]/g; @@ -76,13 +77,17 @@ export function areSimilarElements(node, node2) { } /** -* Returns a URL if link's label is a valid email of http URL, null otherwise. +* Returns a complete URL if text is a valid email address, http URL or telephone +* number, null otherwise. +* The optional link parameter is used to prevent protocol switching between +* 'http' and 'https'. * -* @param {HTMLAnchorElement} link +* @param {String} text +* @param {HTMLAnchorElement} [link] * @returns {String|null} */ -function deduceURLfromLabel(link) { - const label = link.innerText.trim(); +export function deduceURLfromText(text, link) { + const label = text.replace(/\u200b/g, '').trim(); // Check first for e-mail. let match = label.match(EMAIL_REGEX); if (match) { @@ -95,7 +100,7 @@ function deduceURLfromLabel(link) { match = URL_REGEX_WITH_INFOS.exec(label); URL_REGEX_WITH_INFOS.lastIndex = 0; if (match && match[0] === label) { - const currentHttpProtocol = (link.href.match(/^http(s)?:\/\//gi) || [])[0]; + const currentHttpProtocol = (link?.href.match(/^http(s)?:\/\//gi) || [])[0]; if (match[2]) { return match[0]; } else if (currentHttpProtocol) { @@ -105,6 +110,11 @@ function deduceURLfromLabel(link) { return 'https://' + match[0]; } } + // Check for telephone url. + match = label.match(PHONE_REGEX); + if (match) { + return match[1] ? match[0] : 'tel://' + match[0]; + } return null; } @@ -242,7 +252,8 @@ export function sanitize(nodeToSanitize, root = nodeToSanitize) { // Update link URL if label is a new valid link. const startEl = start && closestElement(start, 'a'); if (startEl && root.contains(startEl)) { - const url = deduceURLfromLabel(startEl); + const label = startEl.innerText; + const url = deduceURLfromText(label, startEl); if (url) { startEl.setAttribute('href', url); } diff --git a/addons/web_editor/static/src/js/editor/odoo-editor/src/utils/utils.js b/addons/web_editor/static/src/js/editor/odoo-editor/src/utils/utils.js index 29d98eb33e6..f42a6754f80 100644 --- a/addons/web_editor/static/src/js/editor/odoo-editor/src/utils/utils.js +++ b/addons/web_editor/static/src/js/editor/odoo-editor/src/utils/utils.js @@ -56,6 +56,7 @@ export const URL_REGEX_WITH_INFOS = new RegExp(`((?:(?:${httpCapturedRegex}${url export const YOUTUBE_URL_GET_VIDEO_ID = /^(?:(?:https?:)?\/\/)?(?:(?:www|m)\.)?(?:youtube\.com|youtu\.be)(?:\/(?:[\w-]+\?v=|embed\/|v\/)?)([^\s?&#]+)(?:\S+)?$/i; export const EMAIL_REGEX = /^(mailto:)?[\w-.]+@(?:[\w-]+\.)+[\w-]{2,4}$/i; +export const PHONE_REGEX = /^(tel:(?:\/\/)?)?\+?[\d\s.\-()\/]{3,25}$/; export const PROTECTED_BLOCK_TAG = ['TR','TD','TABLE','TBODY','UL','OL','LI']; diff --git a/addons/web_editor/static/src/js/editor/odoo-editor/test/spec/link.test.js b/addons/web_editor/static/src/js/editor/odoo-editor/test/spec/link.test.js index 52346843fec..2dad1471ae9 100644 --- a/addons/web_editor/static/src/js/editor/odoo-editor/test/spec/link.test.js +++ b/addons/web_editor/static/src/js/editor/odoo-editor/test/spec/link.test.js @@ -680,14 +680,14 @@ describe('Link', () => { window.chai.expect(a.parentElement.isContentEditable).to.be.equal(false); await deleteBackward(editor); window.chai.expect(a.parentElement.isContentEditable).to.be.equal(false); - await insertText(editor, '1'); + await insertText(editor, 'a'); window.chai.expect(a.parentElement.isContentEditable).to.be.equal(false); - await insertText(editor, '2'); + await insertText(editor, 'b'); window.chai.expect(a.parentElement.isContentEditable).to.be.equal(false); - await insertText(editor, '3'); + await insertText(editor, 'c'); window.chai.expect(a.parentElement.isContentEditable).to.be.equal(false); }, - contentAfter: '

a123[]c

', + contentAfter: '

aabc[]c

', }); }); it('should delete the content from the link when popover is active', async () => { diff --git a/addons/web_editor/static/src/js/wysiwyg/widgets/link.js b/addons/web_editor/static/src/js/wysiwyg/widgets/link.js index 7e6bc30013a..32a9d850d12 100644 --- a/addons/web_editor/static/src/js/wysiwyg/widgets/link.js +++ b/addons/web_editor/static/src/js/wysiwyg/widgets/link.js @@ -13,10 +13,9 @@ import { useState, useRef, } from "@odoo/owl"; +import { deduceURLfromText } from "@web_editor/js/editor/odoo-editor/src/utils/sanitize"; -const getDeepRange = OdooEditorLib.getDeepRange; -const getInSelection = OdooEditorLib.getInSelection; -const EMAIL_REGEX = OdooEditorLib.EMAIL_REGEX; +const { getDeepRange, getInSelection, EMAIL_REGEX, PHONE_REGEX } = OdooEditorLib; /** * Allows to customize link content and style. @@ -249,6 +248,15 @@ export class Link extends Component { } return url; } + _deduceUrl(text) { + text = text.trim(); + if (/^(https?:|mailto:|tel:)/.test(text)) { + // Text begins with a known protocol, accept it as valid URL. + return text; + } else { + return deduceURLfromText(text, this.linkEl) || ''; + } + } /** * Abstract method: return true if the URL should be stripped of its domain. * @@ -291,17 +299,14 @@ export class Link extends Component { (type && size ? (' btn-' + size) : ''); var isNewWindow = this._isNewWindow(url); var doStripDomain = this._doStripDomain(); - const emailMatch = url.match(EMAIL_REGEX); - if (emailMatch) { - url = emailMatch[1] ? emailMatch[0] : 'mailto:' + emailMatch[0]; - } else if (url.indexOf(location.origin) === 0 && doStripDomain) { - url = url.slice(location.origin.length); + if (this.state.url.indexOf(location.origin) === 0 && doStripDomain) { + this.state.url = this.state.url.slice(location.origin.length); } var allWhitespace = /\s+/gi; var allStartAndEndSpace = /^\s+|\s+$/gi; return { content: content, - url: this._correctLink(url), + url: this._correctLink(this.state.url), classes: classes.replace(allWhitespace, ' ').replace(allStartAndEndSpace, ''), customTextColor: customTextColor, customFill: customFill, @@ -538,13 +543,7 @@ export class Link extends Component { this.state.originalText = this.state.originalText ? this.state.originalText.replace(/[ \t\r\n]+/g, ' ') : ''; } - - if (!this.state.url) { - const urls = this.state.originalText.match(OdooEditorLib.URL_REGEX_WITH_INFOS); - if (urls) { - this.state.url = urls[0]; - } - } + this.state.url ||= this._deduceUrl(this.state.originalText, this.linkEl); if (this.linkEl) { this.initialNewWindow = this.initialNewWindow || this.linkEl.target === '_blank'; @@ -625,6 +624,8 @@ export class Link extends Component { * @private */ __onURLInput() { + const inputValue = this.$el[0].querySelector('#o_link_dialog_url_input').value; + this.state.url = this._deduceUrl(inputValue, this.linkEl) || inputValue; this._onURLInput(...arguments); } /** @@ -634,7 +635,7 @@ export class Link extends Component { this._savedURLInputOnDestroy = true; var $linkUrlInput = this.$el.find('#o_link_dialog_url_input'); let value = $linkUrlInput.val(); - let isLink = !EMAIL_REGEX.test(value); + let isLink = !EMAIL_REGEX.test(value) && !PHONE_REGEX.test(value); this._getIsNewWindowFormRow().toggleClass('d-none', !isLink); this.$el.find('.o_strip_domain').toggleClass('d-none', value.indexOf(window.location.origin) !== 0); }