diff --git a/backend/src/middleware/helpers/cleanHtml.js b/backend/src/middleware/helpers/cleanHtml.js new file mode 100644 index 000000000..c665dc8de --- /dev/null +++ b/backend/src/middleware/helpers/cleanHtml.js @@ -0,0 +1,86 @@ +import sanitizeHtml from 'sanitize-html' +import linkifyHtml from 'linkifyjs/html' + +const standardSanitizeHtmlOptions = { + allowedTags: [ + 'img', + 'p', + 'h3', + 'h4', + 'br', + 'hr', + 'b', + 'i', + 'u', + 'em', + 'strong', + 'a', + 'pre', + 'ul', + 'li', + 'ol', + 's', + 'strike', + 'span', + 'blockquote', + ], + allowedAttributes: { + a: ['href', 'class', 'target', 'data-*', 'contenteditable'], + span: ['contenteditable', 'class', 'data-*'], + img: ['src'], + }, + allowedIframeHostnames: ['www.youtube.com', 'player.vimeo.com'], + parser: { + lowerCaseTags: true, + }, + transformTags: { + h1: 'h3', + h2: 'h3', + h3: 'h3', + h4: 'h4', + h5: 'strong', + i: 'em', + a: (tagName, attribs) => { + return { + tagName: 'a', + attribs: { + ...attribs, + href: attribs.href || '', + target: '_blank', + rel: 'noopener noreferrer nofollow', + }, + } + }, + b: 'strong', + s: 'strike', + }, +} + +export function cleanHtml(dirty, sanitizeHtmlOptions = standardSanitizeHtmlOptions) { + if (!dirty) { + return dirty + } + + dirty = linkifyHtml(dirty) + dirty = sanitizeHtml(dirty, sanitizeHtmlOptions) + + // remove empty html tags and duplicated linebreaks and returns + dirty = dirty + // remove all tags with "space only" + .replace(/<[a-z-]+>[\s]+<\/[a-z-]+>/gim, '') + .replace(/[\n]{3,}/gim, '\n\n') + .replace(/(\r\n|\n\r|\r|\n)/g, '
$1') + + // replace all p tags with line breaks (and spaces) only by single linebreaks + // limit linebreaks to max 2 (equivalent to html "br" linebreak) + .replace(/(
\s*){2,}/gim, '
') + // remove additional linebreaks after p tags + .replace(/<\/(p|div|th|tr)>\s*(
\s*)+\s*<(p|div|th|tr)>/gim, '

') + // remove additional linebreaks inside p tags + .replace(/<[a-z-]+>(<[a-z-]+>)*\s*(
\s*)+\s*(<\/[a-z-]+>)*<\/[a-z-]+>/gim, '') + // remove additional linebreaks when first child inside p tags + .replace(/

(\s*
\s*)+/gim, '

') + // remove additional linebreaks when last child inside p tags + .replace(/(\s*
\s*)+<\/p+>/gim, '

') + return dirty +} diff --git a/backend/src/middleware/helpers/email/sendMail.js b/backend/src/middleware/helpers/email/sendMail.js index a25938a14..44c925482 100644 --- a/backend/src/middleware/helpers/email/sendMail.js +++ b/backend/src/middleware/helpers/email/sendMail.js @@ -1,4 +1,5 @@ import CONFIG from '../../../config' +import { cleanHtml } from '../../../middleware/helpers/cleanHtml.js' import nodemailer from 'nodemailer' import { htmlToText } from 'nodemailer-html-to-text' @@ -10,6 +11,27 @@ if (!hasEmailConfig) { if (!CONFIG.TEST) { // eslint-disable-next-line no-console console.log('Warning: Middlewares will not try to send mails.') + // TODO: disable e-mail logging on database seeding? + // TODO: implement general logging like 'log4js', see Gradido project: https://github.com/gradido/gradido/blob/master/backend/log4js-config.json + sendMailCallback = async (templateArgs) => { + // eslint-disable-next-line no-console + console.log('--- Send E-Mail ---') + // eslint-disable-next-line no-console + console.log('To: ' + templateArgs.to) + // eslint-disable-next-line no-console + console.log('From: ' + templateArgs.from) + // eslint-disable-next-line no-console + console.log('Subject: ' + templateArgs.subject) + // eslint-disable-next-line no-console + console.log('Content:') + // eslint-disable-next-line no-console + console.log( + cleanHtml(templateArgs.html, { + allowedTags: ['a'], + allowedAttributes: { a: ['href'] }, + }).replace(/&/g, '&'), + ) + } } } else { sendMailCallback = async (templateArgs) => { diff --git a/backend/src/middleware/xssMiddleware.js b/backend/src/middleware/xssMiddleware.js index 1292abb67..9c528b589 100644 --- a/backend/src/middleware/xssMiddleware.js +++ b/backend/src/middleware/xssMiddleware.js @@ -1,100 +1,15 @@ import walkRecursive from '../helpers/walkRecursive' -// import { getByDot, setByDot, getItems, replaceItems } from 'feathers-hooks-common' -import sanitizeHtml from 'sanitize-html' -// import { isEmpty, intersection } from 'lodash' -import linkifyHtml from 'linkifyjs/html' - -function clean(dirty) { - if (!dirty) { - return dirty - } - - dirty = linkifyHtml(dirty) - dirty = sanitizeHtml(dirty, { - allowedTags: [ - 'img', - 'p', - 'h3', - 'h4', - 'br', - 'hr', - 'b', - 'i', - 'u', - 'em', - 'strong', - 'a', - 'pre', - 'ul', - 'li', - 'ol', - 's', - 'strike', - 'span', - 'blockquote', - ], - allowedAttributes: { - a: ['href', 'class', 'target', 'data-*', 'contenteditable'], - span: ['contenteditable', 'class', 'data-*'], - img: ['src'], - }, - allowedIframeHostnames: ['www.youtube.com', 'player.vimeo.com'], - parser: { - lowerCaseTags: true, - }, - transformTags: { - h1: 'h3', - h2: 'h3', - h3: 'h3', - h4: 'h4', - h5: 'strong', - i: 'em', - a: (tagName, attribs) => { - return { - tagName: 'a', - attribs: { - ...attribs, - href: attribs.href || '', - target: '_blank', - rel: 'noopener noreferrer nofollow', - }, - } - }, - b: 'strong', - s: 'strike', - }, - }) - - // remove empty html tags and duplicated linebreaks and returns - dirty = dirty - // remove all tags with "space only" - .replace(/<[a-z-]+>[\s]+<\/[a-z-]+>/gim, '') - .replace(/[\n]{3,}/gim, '\n\n') - .replace(/(\r\n|\n\r|\r|\n)/g, '
$1') - - // replace all p tags with line breaks (and spaces) only by single linebreaks - // limit linebreaks to max 2 (equivalent to html "br" linebreak) - .replace(/(
\s*){2,}/gim, '
') - // remove additional linebreaks after p tags - .replace(/<\/(p|div|th|tr)>\s*(
\s*)+\s*<(p|div|th|tr)>/gim, '

') - // remove additional linebreaks inside p tags - .replace(/<[a-z-]+>(<[a-z-]+>)*\s*(
\s*)+\s*(<\/[a-z-]+>)*<\/[a-z-]+>/gim, '') - // remove additional linebreaks when first child inside p tags - .replace(/

(\s*
\s*)+/gim, '

') - // remove additional linebreaks when last child inside p tags - .replace(/(\s*
\s*)+<\/p+>/gim, '

') - return dirty -} +import { cleanHtml } from '../middleware/helpers/cleanHtml.js' const fields = ['content', 'contentExcerpt', 'reasonDescription'] export default { Mutation: async (resolve, root, args, context, info) => { - args = walkRecursive(args, fields, clean) + args = walkRecursive(args, fields, cleanHtml) return resolve(root, args, context, info) }, Query: async (resolve, root, args, context, info) => { const result = await resolve(root, args, context, info) - return walkRecursive(result, fields, clean) + return walkRecursive(result, fields, cleanHtml) }, } diff --git a/cypress/integration/Notification.Mention.feature b/cypress/integration/Notification.Mention.feature index cadfe11dd..0d3726492 100644 --- a/cypress/integration/Notification.Mention.feature +++ b/cypress/integration/Notification.Mention.feature @@ -5,9 +5,9 @@ Feature: Notification for a mention Background: Given the following "users" are in the database: - | slug | email | password | id | name | termsAndConditionsAgreedVersion | - | wolle-aus-hamburg | wolle@example.org | 1234 | wolle | Wolle aus Hamburg | 0.0.4 | - | matt-rider | matt@example.org | 4321 | matt | Matt Rider | 0.0.4 | + | slug | email | password | id | name | termsAndConditionsAgreedVersion | + | wolle-aus-hamburg | wolle@example.org | 1234 | wolle | Wolfgang aus Hamburg | 0.0.4 | + | matt-rider | matt@example.org | 4321 | matt | Matt Rider | 0.0.4 | Scenario: Mention another user, re-login as this user and see notifications Given I am logged in as "wolle-aus-hamburg" diff --git a/webapp/components/Registration/RegistrationSlider.story.js b/webapp/components/Registration/RegistrationSlider.story.js index 9fb1c347f..ef048db47 100644 --- a/webapp/components/Registration/RegistrationSlider.story.js +++ b/webapp/components/Registration/RegistrationSlider.story.js @@ -97,7 +97,7 @@ storiesOf('RegistrationSlider', module) email: 'wolle.huss@pjannto.com', emailSend: false, nonce: '47539', - name: 'Wolle', + name: 'Wolfgang', password: 'Hello', passwordConfirmation: 'Hello', termsAndConditionsConfirmed: true, @@ -127,7 +127,7 @@ storiesOf('RegistrationSlider', module) email: 'wolle.huss@pjannto.com', emailSend: false, nonce: '47539', - name: 'Wolle', + name: 'Wolfgang', password: 'Hello', passwordConfirmation: 'Hello', termsAndConditionsConfirmed: true, @@ -171,7 +171,7 @@ storiesOf('RegistrationSlider', module) email: 'wolle.huss@pjannto.com', emailSend: true, nonce: '47539', - name: 'Wolle', + name: 'Wolfgang', password: 'Hello', passwordConfirmation: 'Hello', termsAndConditionsConfirmed: true,