Ocelot-Social/backend/src/middleware/xssMiddleware.js
2019-05-23 19:23:03 +02:00

155 lines
4.3 KiB
JavaScript

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 cheerio from 'cheerio'
import linkifyHtml from 'linkifyjs/html'
const embedToAnchor = content => {
const $ = cheerio.load(content)
$('div[data-url-embed]').each((i, el) => {
let url = el.attribs['data-url-embed']
let aTag = $(`<a href="${url}" target="_blank" data-url-embed="">${url}</a>`)
$(el).replaceWith(aTag)
})
return $('body').html()
}
function clean(dirty) {
if (!dirty) {
return dirty
}
// Convert embeds to a-tags
dirty = embedToAnchor(dirty)
dirty = linkifyHtml(dirty)
dirty = sanitizeHtml(dirty, {
allowedTags: [
'iframe',
'img',
'p',
'h3',
'h4',
'br',
'hr',
'b',
'i',
'em',
'strong',
'a',
'pre',
'ul',
'li',
'ol',
's',
'strike',
'span',
'blockquote',
],
allowedAttributes: {
a: ['href', 'class', 'target', 'data-*', 'contenteditable'],
span: ['contenteditable', 'class', 'data-*'],
img: ['src'],
iframe: ['src', 'class', 'frameborder', 'allowfullscreen'],
},
allowedIframeHostnames: ['www.youtube.com', 'player.vimeo.com'],
parser: {
lowerCaseTags: true,
},
transformTags: {
iframe: function(tagName, attribs) {
return {
tagName: 'a',
text: attribs.src,
attribs: {
href: attribs.src,
target: '_blank',
'data-url-embed': '',
},
}
},
h1: 'h3',
h2: 'h3',
h3: 'h3',
h4: 'h4',
h5: 'strong',
i: 'em',
a: function(tagName, attribs) {
return {
tagName: 'a',
attribs: {
href: attribs.href,
target: '_blank',
rel: 'noopener noreferrer nofollow',
},
}
},
b: 'strong',
s: 'strike',
img: function(tagName, attribs) {
let src = attribs.src
if (!src) {
// remove broken images
return {}
}
// if (isEmpty(hook.result)) {
// const config = hook.app.get('thumbor')
// if (config && src.indexOf(config < 0)) {
// // download image
// // const ThumborUrlHelper = require('../helper/thumbor-helper')
// // const Thumbor = new ThumborUrlHelper(config.key || null, config.url || null)
// // src = Thumbor
// // .setImagePath(src)
// // .buildUrl('740x0')
// }
// }
return {
tagName: 'img',
attribs: {
// TODO: use environment variables
src: `http://localhost:3050/images?url=${src}`,
},
}
},
},
})
// remove empty html tags and duplicated linebreaks and returns
dirty = dirty
// remove all tags with "space only"
.replace(/<[a-z-]+>[\s]+<\/[a-z-]+>/gim, '')
// remove all iframes
.replace(/(<iframe(?!.*?src=(['"]).*?\2)[^>]*)(>)[^>]*\/*>/gim, '')
.replace(/[\n]{3,}/gim, '\n\n')
.replace(/(\r\n|\n\r|\r|\n)/g, '<br>$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(/(<br ?\/?>\s*){2,}/gim, '<br>')
// remove additional linebreaks after p tags
.replace(/<\/(p|div|th|tr)>\s*(<br ?\/?>\s*)+\s*<(p|div|th|tr)>/gim, '</p><p>')
// remove additional linebreaks inside p tags
.replace(/<[a-z-]+>(<[a-z-]+>)*\s*(<br ?\/?>\s*)+\s*(<\/[a-z-]+>)*<\/[a-z-]+>/gim, '')
// remove additional linebreaks when first child inside p tags
.replace(/<p>(\s*<br ?\/?>\s*)+/gim, '<p>')
// remove additional linebreaks when last child inside p tags
.replace(/(\s*<br ?\/?>\s*)+<\/p+>/gim, '</p>')
return dirty
}
const fields = ['content', 'contentExcerpt']
export default {
Mutation: async (resolve, root, args, context, info) => {
args = walkRecursive(args, fields, clean)
const result = await resolve(root, args, context, info)
return result
},
Query: async (resolve, root, args, context, info) => {
const result = await resolve(root, args, context, info)
return walkRecursive(result, fields, clean)
},
}