Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[IMPROVE] Filter markdown in notifications #9995

Merged
merged 9 commits into from
Apr 21, 2020
4 changes: 3 additions & 1 deletion app/lib/server/functions/notifications/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Meteor } from 'meteor/meteor';
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import s from 'underscore.string';

import { callbacks } from '../../../../callbacks';
import { settings } from '../../../../settings';

/**
Expand All @@ -22,7 +23,8 @@ export function parseMessageTextPerUser(messageText, message, receiver) {
return TAPi18n.__('Encrypted_message', { lng });
}

return messageText;
// perform processing required before sending message as notification such as markdown filtering
return callbacks.run('renderNotification', messageText);
}

/**
Expand Down
2 changes: 1 addition & 1 deletion app/lib/server/lib/sendNotificationsOnMessage.js
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ export async function sendMessageNotifications(message, room, usersInThread = []
}
});

// the find bellow is crucial. all subscription records returned will receive at least one kind of notification.
// the find below is crucial. All subscription records returned will receive at least one kind of notification.
// the query is defined by the server's default values and Notifications_Max_Room_Members setting.

const subscriptions = await Subscriptions.model.rawCollection().aggregate([
Expand Down
11 changes: 11 additions & 0 deletions app/markdown/lib/markdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ import { Blaze } from 'meteor/blaze';

import { marked } from './parser/marked/marked.js';
import { original } from './parser/original/original.js';
import { filtered } from './parser/filtered/filtered.js';
import { code } from './parser/original/code.js';
import { callbacks } from '../../callbacks';
import { settings } from '../../settings';

const parsers = {
original,
marked,
filtered,
};

class MarkdownClass {
Expand Down Expand Up @@ -76,6 +78,10 @@ class MarkdownClass {
code(...args) {
return code(...args);
}

filterMarkdownFromMessage(message) {
return parsers.filtered(message);
}
}

export const Markdown = new MarkdownClass();
Expand All @@ -89,7 +95,12 @@ const MarkdownMessage = (message) => {
return message;
};

const filterMarkdown = (message) => {
return Markdown.filterMarkdownFromMessage(message);
};
rodrigok marked this conversation as resolved.
Show resolved Hide resolved

callbacks.add('renderMessage', MarkdownMessage, callbacks.priority.HIGH, 'markdown');
callbacks.add('renderNotification', filterMarkdown, callbacks.priority.HIGH, 'filter-markdown');

if (Meteor.isClient) {
Blaze.registerHelper('RocketChatMarkdown', (text) => Markdown.parse(text));
Expand Down
46 changes: 46 additions & 0 deletions app/markdown/lib/parser/filtered/filtered.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Filter markdown tags in message
* Use case: notifications
*/
import { settings } from '../../../../settings';

const filterMarkdownTags = function(message) {
const schemes = settings.get('Markdown_SupportSchemesForLink').split(',').join('|');

// Remove block code backticks
message = message.replace(/```/g, '');

// Remove inline code backticks
message = message.replace(new RegExp(/`([^`\r\n]+)\`/gm), (match) => match.substr(1, match.length - 2));

// Filter [text](url), ![alt_text](image_url)
message = message.replace(new RegExp(`!?\\[([^\\]]+)\\]\\((?:${ schemes }):\\/\\/[^\\)]+\\)`, 'gm'), (match, title) => title);

// Filter <http://link|Text>
message = message.replace(new RegExp(`(?:<|&lt;)(?:${ schemes }):\\/\\/[^\\|]+\\|(.+?)(?=>|&gt;)(?:>|&gt;)`, 'gm'), (match, title) => title);

// Filter headings
message = message.replace(/(^#{1,4}) (([\S\w\d-_\/\*\.,\\][ \u00a0\u1680\u180e\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]?)+)/gm, '$2');

// Filter bold
message = message.replace(/(^|>|[ >_~`])\*{1,2}([^\*\r\n]+)\*{1,2}([<_~`]|\B|\b|$)/gm, '$1$2$3');

// Filter italics
message = message.replace(/(^|>|[ >*~`])\_{1,2}([^\_\r\n]+)\_{1,2}([<*~`]|\B|\b|$)/gm, '$1$2$3');

// Filter strike-through text
message = message.replace(/(^|>|[ >_*`])\~{1,2}([^~\r\n]+)\~{1,2}([<_*`]|\B|\b|$)/gm, '$1$2$3');

// Filter block quotes
message = message.replace(/(?:>){3}\n+([\s\S]*?)\n+(?:<){3}/g, '$1');

// Filter > quote
message = message.replace(/^>(.*)$/gm, '$1');

return message;
};

export const filtered = function(message) {
message = filterMarkdownTags(message);
return message;
};
131 changes: 127 additions & 4 deletions app/markdown/tests/client.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import s from 'underscore.string';

import './client.mocks.js';
import { original } from '../lib/parser/original/original';
import { filtered } from '../lib/parser/filtered/filtered';
import { Markdown } from '../lib/markdown';

const wrapper = (text, tag) => `<span class="copyonly">${ tag }</span>${ text }<span class="copyonly">${ tag }</span>`;
Expand Down Expand Up @@ -230,15 +231,119 @@ const nested = {
'> some quote\n`window.location.reload();`': `${ quoteWrapper(' some quote') }${ inlinecodeWrapper('window.location.reload();') }`,
};

/*
* Markdown Filters
*/
const boldFiltered = {
'*Hello*': 'Hello',
'**Hello**': 'Hello',
'*Hello**': 'Hello',
'He*llo': 'He*llo',
'*Hello': '*Hello',
'Hello*': 'Hello*',
'***Hello***': '***Hello***',
'***Hello**': '***Hello**',
'*Hello* there': 'Hello there',
'**Hello** there': 'Hello there',
'Hi, *Hello*': 'Hi, Hello',
'Hi, **Hello**': 'Hi, Hello',
'Hi, *Hello* how are you?': 'Hi, Hello how are you?',
'Hi, **Hello** how are you?': 'Hi, Hello how are you?',
};

const italicFiltered = {
_Hello_: 'Hello',
__Hello__: 'Hello',
_Hello__: 'Hello',
He_llo: 'He_llo',
_Hello: '_Hello',
__Hello: '__Hello',
Hello_: 'Hello_',
___Hello___: '___Hello___',
___Hello__: '___Hello__',
'_Hello_ there': 'Hello there',
'__Hello__ there': 'Hello there',
'Hi, _Hello_': 'Hi, Hello',
'Hi, __Hello__': 'Hi, Hello',
'Hi, _Hello_ how are you?': 'Hi, Hello how are you?',
'Hi, __Hello__ how are you?': 'Hi, Hello how are you?',
};

const strikeFiltered = {
'~Hello~': 'Hello',
'~~Hello~~': 'Hello',
'~~Hello': '~~Hello',
'~Hello~~': 'Hello',
'He~llo': 'He~llo',
'~Hello': '~Hello',
'Hello~': 'Hello~',
'~~~Hello~~~': '~~~Hello~~~',
'~~~Hello~~': '~~~Hello~~',
'~Hello~ there': 'Hello there',
'~~Hello~~ there': 'Hello there',
'Hi, ~Hello~': 'Hi, Hello',
'Hi, ~~Hello~~': 'Hi, Hello',
'Hi, ~Hello~ how are you?': 'Hi, Hello how are you?',
'Hi, ~~Hello~~ how are you?': 'Hi, Hello how are you?',
};

const headingFiltered = {
'# Hello': 'Hello',
'## Hello': 'Hello',
'### Hello': 'Hello',
'#### Hello': 'Hello',
'#Hello': '#Hello',
'##Hello': '##Hello',
'###Hello': '###Hello',
'####Hello': '####Hello',
'He#llo': 'He#llo',
'# Hello there': 'Hello there',
'Hi, # Hello': 'Hi, # Hello',
'Hi, # Hello there': 'Hi, # Hello there',
};

const quoteFiltered = {
'>Hello': 'Hello',
'> Hello': ' Hello',
'>>>\nHello\n<<<': 'Hello',
'>>>\nHello there!\n<<<': 'Hello there!',
'>>>\n Hello there! \n<<<': ' Hello there! ',
};

const linkFiltered = {
'[Text](http://link)': 'Text',
'[Open Site For Rocket.Chat](https://open.rocket.chat/)': 'Open Site For Rocket.Chat',
'[ Open Site For Rocket.Chat](https://open.rocket.chat/ )': ' Open Site For Rocket.Chat',
'[Rocket.Chat Site](https://rocket.chat/)': 'Rocket.Chat Site',
'<http://link|Text>': 'Text',
'<http://link|Text for test>': 'Text for test',
};

const inlinecodeFiltered = {
'`code`': 'code',
'`code` begin': 'code begin',
'End `code`': 'End code',
'Middle `code` middle': 'Middle code middle',
'`code`begin': 'codebegin',
'End`code`': 'Endcode',
'Middle`code`middle': 'Middlecodemiddle',
};

const blockcodeFiltered = {
'```code```': 'code',
'```code': 'code',
'code```': 'code',
'Here ```code``` lies': 'Here code lies',
'Here```code```lies': 'Herecodelies',
};

const defaultObjectTest = (result, object, objectKey) => assert.equal(result.html, object[objectKey]);

const testObject = (object, parser = original, test = defaultObjectTest) => {
Object.keys(object).forEach((objectKey) => {
describe(objectKey, () => {
const message = {
html: s.escapeHTML(objectKey),
};
const result = Markdown.mountTokensBack(parser(message));
const message = parser === original ? { html: s.escapeHTML(objectKey) } : objectKey;
const result = parser === original ? Markdown.mountTokensBack(parser(message)) : { html: parser(message) };
it(`should be equal to ${ object[objectKey] }`, () => {
test(result, object, objectKey);
});
Expand Down Expand Up @@ -274,6 +379,24 @@ describe('Original', function() {
describe('Nested', () => testObject(nested));
});

describe('Filtered', function() {
describe('BoldFilter', () => testObject(boldFiltered, filtered));

describe('Italic', () => testObject(italicFiltered, filtered));

describe('StrikeFilter', () => testObject(strikeFiltered, filtered));

describe('HeadingFilter', () => testObject(headingFiltered, filtered));

describe('QuoteFilter', () => testObject(quoteFiltered, filtered));

describe('LinkFilter', () => testObject(linkFiltered, filtered));

describe('inlinecodeFilter', () => testObject(inlinecodeFiltered, filtered));

describe('blockcodeFilter', () => testObject(blockcodeFiltered, filtered));
});

// describe.only('Marked', function() {
// describe('Bold', () => testObject(bold, marked));

Expand Down