From b429ce0ad588b4f74efa055807a262fd455e118e Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Thu, 23 Jan 2025 18:18:14 +0530 Subject: [PATCH] fix: Prevent template variables from becoming links (#10725) # Pull Request Template ## Description **Issue** This PR fixes template variables in messages (e.g., {{customer.name}}) that were being incorrectly converted to clickable links by the `MessageFormatter's linkify` functionality. This caused formatting issues and broken links. **Solution** Added a `linkify` parameter to `MessageFormatter` to optionally disable link conversion ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? **Screenshots** **Before** image **After** image ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --- .../Campaigns/CampaignCard/CampaignCard.vue | 2 +- .../specs/useMessageFormatter.spec.js | 12 +++++ .../shared/composables/useMessageFormatter.js | 6 ++- .../shared/helpers/MessageFormatter.js | 52 +++++++++++-------- .../helpers/specs/MessageFormatter.spec.js | 7 +++ 5 files changed, 55 insertions(+), 24 deletions(-) diff --git a/app/javascript/dashboard/components-next/Campaigns/CampaignCard/CampaignCard.vue b/app/javascript/dashboard/components-next/Campaigns/CampaignCard/CampaignCard.vue index 229553253..304e55347 100644 --- a/app/javascript/dashboard/components-next/Campaigns/CampaignCard/CampaignCard.vue +++ b/app/javascript/dashboard/components-next/Campaigns/CampaignCard/CampaignCard.vue @@ -98,7 +98,7 @@ const inboxIcon = computed(() => {
diff --git a/app/javascript/shared/composables/specs/useMessageFormatter.spec.js b/app/javascript/shared/composables/specs/useMessageFormatter.spec.js index e9e3962d7..8021fc50d 100644 --- a/app/javascript/shared/composables/specs/useMessageFormatter.spec.js +++ b/app/javascript/shared/composables/specs/useMessageFormatter.spec.js @@ -30,6 +30,18 @@ describe('useMessageFormatter', () => { ' { + const message = 'Check https://example.com and {{user.id}}'; + const result = messageFormatter.formatMessage( + message, + false, + false, + false + ); + expect(result).not.toContain(' { diff --git a/app/javascript/shared/composables/useMessageFormatter.js b/app/javascript/shared/composables/useMessageFormatter.js index 16b7f25a6..974d113fd 100644 --- a/app/javascript/shared/composables/useMessageFormatter.js +++ b/app/javascript/shared/composables/useMessageFormatter.js @@ -14,11 +14,13 @@ export const useMessageFormatter = () => { * @param {boolean} isAPrivateNote - Whether the message is a private note. * @returns {string} - The formatted message. */ - const formatMessage = (message, isATweet, isAPrivateNote) => { + // TODO: ref:https://github.com/chatwoot/chatwoot/pull/10725#discussion_r1925300874 + const formatMessage = (message, isATweet, isAPrivateNote, linkify) => { const messageFormatter = new MessageFormatter( message, isATweet, - isAPrivateNote + isAPrivateNote, + linkify ); return messageFormatter.formattedMessage; }; diff --git a/app/javascript/shared/helpers/MessageFormatter.js b/app/javascript/shared/helpers/MessageFormatter.js index 06bd8bfae..c87209fd4 100644 --- a/app/javascript/shared/helpers/MessageFormatter.js +++ b/app/javascript/shared/helpers/MessageFormatter.js @@ -1,6 +1,7 @@ import mila from 'markdown-it-link-attributes'; import mentionPlugin from './markdownIt/link'; import MarkdownIt from 'markdown-it'; + const setImageHeight = inlineToken => { const imgSrc = inlineToken.attrGet('src'); if (!imgSrc) return; @@ -30,25 +31,27 @@ const imgResizeManager = md => { }); }; -const md = MarkdownIt({ - html: false, - xhtmlOut: true, - breaks: true, - langPrefix: 'language-', - linkify: true, - typographer: true, - quotes: '\u201c\u201d\u2018\u2019', - maxNesting: 20, -}) - .use(mentionPlugin) - .use(imgResizeManager) - .use(mila, { - attrs: { - class: 'link', - rel: 'noreferrer noopener nofollow', - target: '_blank', - }, - }); +const createMarkdownInstance = (linkify = true) => { + return MarkdownIt({ + html: false, + xhtmlOut: true, + breaks: true, + langPrefix: 'language-', + linkify, + typographer: true, + quotes: '\u201c\u201d\u2018\u2019', + maxNesting: 20, + }) + .use(mentionPlugin) + .use(imgResizeManager) + .use(mila, { + attrs: { + class: 'link', + rel: 'noreferrer noopener nofollow', + target: '_blank', + }, + }); +}; const TWITTER_USERNAME_REGEX = /(^|[^@\w])@(\w{1,15})\b/g; const TWITTER_USERNAME_REPLACEMENT = '$1[@$2](http://twitter.com/$2)'; @@ -56,10 +59,17 @@ const TWITTER_HASH_REGEX = /(^|\s)#(\w+)/g; const TWITTER_HASH_REPLACEMENT = '$1[#$2](https://twitter.com/hashtag/$2)'; class MessageFormatter { - constructor(message, isATweet = false, isAPrivateNote = false) { + constructor( + message, + isATweet = false, + isAPrivateNote = false, + linkify = true + ) { this.message = message || ''; this.isAPrivateNote = isAPrivateNote; this.isATweet = isATweet; + this.linkify = linkify; + this.md = createMarkdownInstance(linkify); } formatMessage() { @@ -74,7 +84,7 @@ class MessageFormatter { TWITTER_HASH_REPLACEMENT ); } - return md.render(updatedMessage); + return this.md.render(updatedMessage); } get formattedMessage() { diff --git a/app/javascript/shared/helpers/specs/MessageFormatter.spec.js b/app/javascript/shared/helpers/specs/MessageFormatter.spec.js index 47760f7a8..a685cb0da 100644 --- a/app/javascript/shared/helpers/specs/MessageFormatter.spec.js +++ b/app/javascript/shared/helpers/specs/MessageFormatter.spec.js @@ -16,6 +16,13 @@ describe('#MessageFormatter', () => { '

Chatwoot is an opensource tool. https://www.chatwoot.com

' ); }); + it('should not convert template variables to links when linkify is disabled', () => { + const message = 'Hey {{customer.name}}, check https://chatwoot.com'; + const formatter = new MessageFormatter(message, false, false, false); + expect(formatter.formattedMessage).toMatch( + '

Hey {{customer.name}}, check https://chatwoot.com

' + ); + }); }); describe('parses heading to strong', () => {