UNPKG

@vlad8096/stanza

Version:

Modern XMPP in the browser, with a JSON API

191 lines (190 loc) 5.42 kB
"use strict"; /** * This file is derived from prior work. * * See NOTICE.md for full license text. * * Derived from: xhtml-im.js, Copyright © 2013 Waqas Hussain */ Object.defineProperty(exports, "__esModule", { value: true }); const ALLOWED_ELEMENTS = new Set([ 'a', 'blockquote', 'br', 'cite', 'em', 'img', 'li', 'ol', 'p', 'span', 'strong', 'ul' ]); const style = new Set(['style']); const ALLOWED_ATTRIBUTES = new Map([ ['a', new Set(['href', 'style'])], ['body', new Set(['style', 'xml:lang'])], ['blockquote', style], ['br', style], ['cite', style], ['em', style], ['img', new Set(['alt', 'height', 'src', 'style', 'width'])], ['li', style], ['ol', style], ['p', style], ['span', style], ['strong', style], ['ul', style] ]); const CSS_RULES = new Map([ ['font-style', /normal|italic|oblique|inherit/i], ['font-weight', /normal|bold|bolder|lighter|inherit|\d\d\d/i], ['text-decoration', /none|underline|overline|line-through|blink|inherit/i] // These properties are allowed by XHTML-IM, but really only cause UX issues: // background-color // color // font-family // font-size // margin-left // margin-right // text-align ]); const sanitizeCSS = (css) => { const declarations = `;${css}` // Declarations are ; delimited, not terminated .replace(/\/\*[^*]*\*+([^/*][^*]*\*+)*\//g, '') // Strip comments .replace(/\/\*.*/, '') // Strip unclosed comments .replace(/\\([a-fA-F0-9]{1,6})\s?/, (_, x) => String.fromCharCode(parseInt(x, 16))) // Decode escape sequences .match(/;\s*([a-z-]+)\s*:\s*([^;]*[^\s;])\s*/g); // Split into declarations const rules = []; if (!declarations) { return false; } for (const declaration of declarations) { const parts = declaration.match(/^;\s*([a-z-]+)\s*:\s*([^;]*[^\s])\s*$/); if (!parts) { continue; } const sanitizer = CSS_RULES.get(parts[1]); if (sanitizer) { const value = parts[2].match(sanitizer); if (value) { rules.push(`${parts[1]}:${value[0]}`); } } } if (rules.length) { return rules.join(''); } return false; }; const sanitizeURL = (url) => { return (!!url.match(/^(https?|xmpp|cid|mailto|ftps?|im|ircs?|sips?|tel|geo|bitcoin|magnet):/i) && url); }; const sanitizeNumber = (num) => { return !!num.match(/^[0-9]*$/) && num; }; const ATTRIBUTE_SANITIZERS = { alt: text => text, height: sanitizeNumber, href: sanitizeURL, src: sanitizeURL, style: sanitizeCSS, width: sanitizeNumber }; function stripElement(input) { let results = []; for (const child of input.children) { if (typeof child === 'string') { results.push(child); } else { const sanitized = sanitizeInterior(child); if (sanitized) { if (Array.isArray(sanitized)) { results = results.concat(sanitized); } else { results.push(sanitized); } } } } return results; } function sanitizeInterior(input) { if (typeof input === 'string') { return input; } if (!ALLOWED_ELEMENTS.has(input.name)) { if (input.name === 'script') { return; } return stripElement(input); } const children = input.children .map(sanitizeInterior) .filter(child => child !== undefined); const attributes = {}; for (const [key, value] of Object.entries(input.attributes)) { const allowed = ALLOWED_ATTRIBUTES.get(input.name); if (!allowed || !allowed.has(key)) { continue; } if (!value) { continue; } const sanitized = ATTRIBUTE_SANITIZERS[key](value); if (!sanitized) { continue; } attributes[key] = sanitized; } return { attributes, children, name: input.name }; } function sanitizeRoot(input) { if (typeof input === 'string') { return; } let children = []; for (const child of input.children) { if (!child) { continue; } if (typeof child === 'string') { children.push(child); continue; } const sanitized = sanitizeInterior(child); if (Array.isArray(sanitized)) { children = children.concat(sanitized); } else if (sanitized) { children.push(sanitized); } } const attributes = {}; if (input.name !== 'body') { return; } if (input.attributes.xmlns !== undefined) { attributes.xmlns = input.attributes.xmlns; } if (input.attributes.style) { attributes.style = input.attributes.style; } if (input.attributes['xml:lang'] !== undefined) { attributes['xml:lang'] = input.attributes['xml:lang']; } return { attributes, children, name: 'body' }; } exports.default = sanitizeRoot;