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**
**After**
## 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', () => {