Skip to content
Permalink
rss_feed_displ…
Switch branches/tags
Go to file
 
 
Cannot retrieve contributors at this time
312 lines (266 sloc) 10.5 KB
"use strict";
// This constructs a nested Mithril object with only specific HTML tags allowed
// No attributes are allowed.
// A css class (from a short approved list) can be set on a tag using a ".className" after the opening tag name.
// For example: <span.narrafirma-special-warning>Warning!!!<span>
// 1 is normal tag that needs to be closed; 2 is self-closing tag (br and hr)
var allowedHTMLTags = {
// a: 1,
address: 1,
article: 1,
b: 1,
big: 1,
blockquote: 1,
br: 2,
caption: 1,
cite: 1,
code: 1,
del: 1,
div: 1,
dd: 1,
d1: 1,
dt: 1,
em: 1,
h1: 1,
h2: 1,
h3: 1,
h4: 1,
h5: 1,
h6: 1,
hr: 2,
i: 1,
// img
kbd: 1,
li: 1,
ol: 1,
p: 1,
pre: 1,
s: 1,
small: 1,
span: 1,
sup: 1,
sub: 1,
strong: 1,
strike: 1,
table: 1,
td: 1,
th: 1,
tr: 1,
u: 1,
ul: 1
};
var allowedCSSClasses = {
"narrafirma-special-warning": 1
};
var m;
function isURLAcceptable(url) {
if (!url) return false;
return url.substring(0, 5) === "http:" || url.substring(0, 6) === "https";
}
function generateVDOM(nodes: NodeList, configuration) {
if (!nodes) return [];
// console.log("generateVDOM nodes length", nodes.length);
var result = [];
for (var i = 0; i < nodes.length; i++) {
var node = <HTMLElement>nodes[i];
// console.log("nodeType", node.nodeType);
switch (node.nodeType) {
case 1:
var tagName = node.tagName.toLowerCase();
// console.log("element", node);
if (!allowedHTMLTags[tagName] &&
((tagName === "a" && !configuration.allowLinks) ||
(tagName === "img" && !configuration.allowImages))
) {
console.log("disallowed tag", tagName);
tagName = "span";
}
// TODO: Allow more attributes maybe
var attributes = {};
for (var j = 0; j < node.attributes.length; j++) {
var attribute = node.attributes[j];
if (attribute.name === "class") {
var theClassOrClasses = attribute.value;
if (theClassOrClasses in allowedCSSClasses) {
attributes["class"] = theClassOrClasses;
} else {
console.log("WARN: CSS class not allowed", theClassOrClasses);
}
}
if (configuration.allowLinks && attribute.name === "href") {
var url = attribute.value;
if (url && url.substring(0, 2) === "//") {
url = window.location.protocol + url;
}
if (isURLAcceptable(url)) {
attributes["href"] = url;
}
}
if (configuration.allowImages && attribute.name === "src") {
var url = attribute.value;
if (url && url.substring(0, 2) === "//") {
url = window.location.protocol + url;
}
if (isURLAcceptable(url)) {
attributes["src"] = url;
}
}
if (configuration.allowImages && (attribute.name === "width" || attribute.name === "height")) {
var size = parseInt(attribute.value);
if (size !== NaN && size > 1) {
attributes[attribute.name] = size;
} else {
// Tracking image
console.log("discarding likely tracking image");
tagName = "span";
attributes = {};
break;
}
}
}
if (tagName === "a") {
attributes["rel"] = "nofollow";
}
if (tagName === "img") {
if (!attributes["width"] || !attributes["height"]) {
console.log("discarding likely tracking image (2)", attributes["src"]);
tagName = "span";
attributes = {};
}
}
var children = generateVDOM(node.childNodes, configuration)
var vdom = m(tagName, attributes, children);
result.push(vdom);
break;
case 3:
if ((<any>node).data) {
// console.log("adding text node", node.data);
result.push((<any>node).data);
}
break;
default:
console.log("WARN: Unhandled node type", node.nodeType, node);
}
}
return result;
}
export function generateSanitizedHTMLForMithrilWithAttributes(mithril, DOMParser, html, configuration = {}) {
m = mithril;
// console.log("generateSanitized html", html);
if (html === undefined || html === null) {
console.log("generateSanitizedHTMLForMithrilWithAttributes: Undefined or null html", html);
html = "";
// throw new Error("Undefined or null html");
}
// Handle case where is already a Mithril object
if (html.tag) return html;
var hasMarkup = html.indexOf("<") !== -1;
// console.log("has markup", hasMarkup);
if (!hasMarkup) return html;
var htmlDoc = new DOMParser().parseFromString(html, 'text/html');
// console.log("htmlDoc", htmlDoc);
var vdom = generateVDOM(htmlDoc.childNodes, configuration);
// console.log("generateSanitizedHTMLForMithrilWithAttributes vdom", vdom);
return vdom;
}
export function generateSanitizedHTMLForMithrilWithoutAttributes(mithril, html) {
m = mithril;
// console.log("html", html);
if (html === undefined || html === null) {
console.log("Undefined or null html", html);
html = "";
// throw new Error("Undefined or null html");
}
// Handle case where is already a Mithril object
if (html.tag) return html;
var hasMarkup = html.indexOf("<") !== -1;
// console.log("has markup", hasMarkup);
if (!hasMarkup) return html;
try {
// Use a fake div tag as a conceptual placeholder
var tags = [{tagName: "div", cssClass: undefined}];
var output = [[]];
var text = "";
for (var i = 0, l = html.length; i < l; i++) {
var tagDisallowed = false;
var c = html.charAt(i);
if (c === "<") {
if (text !== "") {
output[output.length - 1].push(text);
text = "";
}
var closing = html.charAt(i + 1) === "/";
if (closing) i++;
// Simple approach will cause parse errors ">" in parameter strings, but OK
var pos = html.indexOf(">", i + 1);
if (pos < 0) {
throw new Error("no closing angle bracket found after position: " + i);
}
var tagEnd = pos;
var spacePos = html.indexOf(" ", i + 1);
if (spacePos > -1 && spacePos < pos) tagEnd = spacePos;
var tagName = html.substring(i + 1, tagEnd);
i = pos;
var closedTag = html.substring(pos - 1, pos) === "/";
console.log("tagName", tagName, closedTag);
// Special support for Mithril-like class names inline with tags
var cssClass;
var parts = tagName.split(".");
if (parts.length > 1) {
tagName = parts[0];
cssClass = parts[1];
} else {
cssClass = undefined;
}
if (/[^A-Za-z0-9]/.test(tagName)) {
throw new Error("tag is not alphanumeric or has attributes: " + tagName);
}
if (cssClass && !allowedCSSClasses[cssClass]) {
throw new Error("css class is not allowed: " + cssClass);
}
if (closing) {
var startTag = tags.pop();
if (startTag.tagName !== tagName) {
throw new Error("closing tag does not match opening tag for: " + tagName);
}
cssClass = startTag.cssClass;
}
if (!allowedHTMLTags[tagName]) {
// throw new Error("tag is not allowed: " + tagName);
tagDisallowed = true;
// tagName = "span";
}
if (allowedHTMLTags[tagName] === 2) {
// self-closing tag like BR
output.push([]);
closing = true;
}
if (closedTag) output.push([]);
if (closing || closedTag) {
var newTag;
if (tagDisallowed) tagName = "span";
if (cssClass) {
newTag = m(tagName, {"class": cssClass}, output.pop());
} else {
newTag = m(tagName, output.pop());
}
output[output.length - 1].push(newTag);
} else {
tags.push({tagName: tagName, cssClass: cssClass});
output.push([]);
}
} else {
text = text + c;
}
}
if (text) output[output.length - 1].push(text);
if (tags.length !== 1 || output.length !== 1) {
var unmatched = tags.pop();
throw new Error("Unmatched start tag: " + unmatched.tagName);
}
// Don't return the fake div tag, just the contents
return output.pop();
} catch (exception) {
return [m("div", "Strict sanitization issue: " + exception)];
}
}