Addfeature: Enable HTML email content for richer comment

This commit is contained in:
Sam X. Chen 2019-08-12 17:41:49 -04:00
parent db1cf5bb64
commit 8d76db91b8
3 changed files with 37 additions and 18 deletions

View file

@ -1,4 +1,5 @@
import _sanitizeXss from 'xss'; import _sanitizeXss from 'xss';
const ASIS = 'asis';
const sanitizeXss = (input, options) => { const sanitizeXss = (input, options) => {
const defaultAllowedIframeSrc = /^(https:){0,1}\/\/.*?(youtube|vimeo|dailymotion|youku)/i; const defaultAllowedIframeSrc = /^(https:){0,1}\/\/.*?(youtube|vimeo|dailymotion|youku)/i;
const allowedIframeSrcRegex = (function() { const allowedIframeSrcRegex = (function() {
@ -17,28 +18,39 @@ const sanitizeXss = (input, options) => {
return reg; return reg;
})(); })();
const targetWindow = '_blank'; const targetWindow = '_blank';
const getHtmlDOM = html => {
const i = document.createElement('i');
i.innerHTML = html;
return i.firstChild;
};
options = { options = {
onTag(tag, html, options) { onTag(tag, html, options) {
const htmlDOM = getHtmlDOM(html);
const getAttr = attr => {
return htmlDOM && attr && htmlDOM.getAttribute(attr);
};
if (tag === 'iframe') { if (tag === 'iframe') {
const clipCls = 'note-vide-clip'; const clipCls = 'note-vide-clip';
if (!options.isClosing) { if (!options.isClosing) {
const srcp = /src=(['"]{0,1})(\S*)(\1)/; const iframeCls = getAttr('class');
let safe = html.indexOf(`class="${clipCls}"`) > -1; let safe = iframeCls.indexOf(clipCls) > -1;
if (srcp.exec(html)) { const src = getAttr('src');
const src = RegExp.$2; if (allowedIframeSrcRegex.exec(src)) {
if (allowedIframeSrcRegex.exec(src)) { safe = true;
safe = true;
}
if (safe)
return `<iframe src='${src}' class="${clipCls}" width=100% height=auto allowfullscreen></iframe>`;
} }
if (safe)
return `<iframe src='${src}' class="${clipCls}" width=100% height=auto allowfullscreen></iframe>`;
} else { } else {
// remove </iframe> tag
return ''; return '';
} }
} else if (tag === 'a') { } else if (tag === 'a') {
if (!options.isClosing) { if (!options.isClosing) {
if (/href=(['"]{0,1})(\S*)(\1)/.exec(html)) { if (getAttr(ASIS) === 'true') {
const href = RegExp.$2; // if has a ASIS attribute, don't do anything, it's a member id
return html;
} else {
const href = getAttr('href');
if (href.match(/^((http(s){0,1}:){0,1}\/\/|\/)/)) { if (href.match(/^((http(s){0,1}:){0,1}\/\/|\/)/)) {
// a valid url // a valid url
return `<a href=${href} target=${targetWindow}>`; return `<a href=${href} target=${targetWindow}>`;
@ -47,8 +59,8 @@ const sanitizeXss = (input, options) => {
} }
} else if (tag === 'img') { } else if (tag === 'img') {
if (!options.isClosing) { if (!options.isClosing) {
if (new RegExp('src=([\'"]{0,1})(\\S*)(\\1)').exec(html)) { const src = getAttr('src');
const src = RegExp.$2; if (src) {
return `<a href='${src}' class='swipebox'><img src='${src}' class="attachment-image-preview mCS_img_loaded"></a>`; return `<a href='${src}' class='swipebox'><img src='${src}' class="attachment-image-preview mCS_img_loaded"></a>`;
} }
} }
@ -203,7 +215,9 @@ Template.editor.onRendered(() => {
// even though uploaded event fired, attachment.url() is still null somehow //TODO // even though uploaded event fired, attachment.url() is still null somehow //TODO
const url = attachment.url(); const url = attachment.url();
if (url) { if (url) {
insertImage(url); insertImage(
`${location.protocol}//${location.host}${url}`,
);
} else { } else {
retry++; retry++;
if (retry < maxTry) { if (retry < maxTry) {
@ -334,6 +348,7 @@ Blaze.Template.registerHelper(
// `userId` to the popup as usual, and we need to store it in the DOM // `userId` to the popup as usual, and we need to store it in the DOM
// using a data attribute. // using a data attribute.
'data-userId': knowedUser.userId, 'data-userId': knowedUser.userId,
[ASIS]: 'true',
}, },
linkValue, linkValue,
); );

View file

@ -110,7 +110,9 @@ if (Meteor.isServer) {
if (activity.userId) { if (activity.userId) {
// No need send notification to user of activity // No need send notification to user of activity
// participants = _.union(participants, [activity.userId]); // participants = _.union(participants, [activity.userId]);
params.user = activity.user().getName(); const user = activity.user();
params.user = user.getName();
params.userEmails = user.emails;
params.userId = activity.userId; params.userId = activity.userId;
} }
if (activity.boardId) { if (activity.boardId) {
@ -172,7 +174,7 @@ if (Meteor.isServer) {
const comment = activity.comment(); const comment = activity.comment();
params.comment = comment.text; params.comment = comment.text;
if (board) { if (board) {
const atUser = /(?:^|\s+)@(\S+)(?:\s+|$)/g; const atUser = /(?:^|>|\b|\s)@(\S+)(?:\s|$|<|\b)/g;
const comment = params.comment; const comment = params.comment;
if (comment.match(atUser)) { if (comment.match(atUser)) {
const commenter = params.user; const commenter = params.user;
@ -184,6 +186,8 @@ if (Meteor.isServer) {
} }
const user = Users.findOne(username) || Users.findOne({ username }); const user = Users.findOne(username) || Users.findOne({ username });
const uid = user && user._id; const uid = user && user._id;
params.atUsername = username;
params.atEmails = user.emails;
if (board.hasMember(uid)) { if (board.hasMember(uid)) {
title = 'act-atUserComment'; title = 'act-atUserComment';
watchers = _.union(watchers, [uid]); watchers = _.union(watchers, [uid]);

View file

@ -32,14 +32,14 @@ Meteor.startup(() => {
if (texts.length === 0) return; if (texts.length === 0) return;
// merge the cached content into single email and flush // merge the cached content into single email and flush
const text = texts.join('\n\n'); const html = texts.join('<br/>\n\n');
user.clearEmailBuffer(); user.clearEmailBuffer();
try { try {
Email.send({ Email.send({
to: user.emails[0].address.toLowerCase(), to: user.emails[0].address.toLowerCase(),
from: Accounts.emailTemplates.from, from: Accounts.emailTemplates.from,
subject, subject,
text, html,
}); });
} catch (e) { } catch (e) {
return; return;