2012-05-30 20:48:24 +04:00
|
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
2012-03-13 05:14:45 +04:00
|
|
|
|
2018-02-01 01:25:50 +03:00
|
|
|
ChromeUtils.import("resource:///modules/imServices.jsm");
|
2018-04-21 12:52:24 +03:00
|
|
|
Cu.importGlobalProperties(["DOMParser"]);
|
2012-03-13 05:14:45 +04:00
|
|
|
|
2015-10-08 16:54:02 +03:00
|
|
|
this.EXPORTED_SYMBOLS = [
|
2012-03-13 05:14:45 +04:00
|
|
|
"cleanupImMarkup", // used to clean up incoming IMs.
|
|
|
|
// This will use the global ruleset of acceptable stuff
|
|
|
|
// except if another (custom one) is provided
|
|
|
|
"createDerivedRuleset", // used to create a ruleset that inherits from the
|
|
|
|
// default one
|
|
|
|
// useful if you want to allow or forbid
|
2018-03-20 07:21:00 +03:00
|
|
|
// an additional thing in a specific
|
2012-03-13 05:14:45 +04:00
|
|
|
// conversation but take into account all
|
|
|
|
// the other global settings.
|
|
|
|
"addGlobalAllowedTag",
|
|
|
|
"removeGlobalAllowedTag",
|
|
|
|
"addGlobalAllowedAttribute",
|
|
|
|
"removeGlobalAllowedAttribute",
|
|
|
|
"addGlobalAllowedStyleRule",
|
|
|
|
"removeGlobalAllowedStyleRule"
|
|
|
|
];
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Structure of a ruleset:
|
|
|
|
* A ruleset is a JS object containing 3 sub-objects: attrs, tags and styles.
|
|
|
|
* - attrs: an object containing a list of attributes allowed for all tags.
|
|
|
|
* example: attrs: { 'style': true }
|
|
|
|
*
|
|
|
|
* - tags: an object with the allowed tags. each tag can allow specific attributes.
|
|
|
|
* example: 'a': {'href': true}
|
|
|
|
*
|
|
|
|
* each attribute can have a function returning a boolean indicating if
|
|
|
|
* the attribute is accepted.
|
2015-08-13 23:43:41 +03:00
|
|
|
* example: 'href': aValue => aValue == 'about:blank'
|
2012-03-13 05:14:45 +04:00
|
|
|
*
|
|
|
|
* - styles: an object with the allowed CSS style rule.
|
|
|
|
* example: 'font-size': true
|
|
|
|
* FIXME: make this accept functions to filter the CSS values too.
|
|
|
|
*
|
|
|
|
* See the 3 examples of rulesets below.
|
|
|
|
*/
|
|
|
|
|
2015-10-11 00:23:56 +03:00
|
|
|
var kAllowedURLs = aValue => /^(https?|ftp|mailto):/.test(aValue);
|
|
|
|
var kAllowedMozClasses =
|
2015-08-13 23:43:41 +03:00
|
|
|
aClassName => aClassName == "moz-txt-underscore" ||
|
2015-10-30 02:18:29 +03:00
|
|
|
aClassName == "moz-txt-tag" ||
|
|
|
|
aClassName == "ib-person";
|
2016-11-21 22:18:00 +03:00
|
|
|
var kAllowedAnchorClasses = aClassName => aClassName == "ib-person";
|
2012-03-13 05:14:45 +04:00
|
|
|
|
2013-02-20 03:43:50 +04:00
|
|
|
/* Tags whose content should be fully removed, and reported in the Error Console. */
|
2015-10-11 00:23:56 +03:00
|
|
|
var kForbiddenTags = {
|
2013-02-20 03:43:50 +04:00
|
|
|
script: true,
|
|
|
|
style: true
|
|
|
|
};
|
|
|
|
|
2012-03-13 05:14:45 +04:00
|
|
|
// in strict mode, remove all formatings. Keep only links and line breaks.
|
2015-10-11 00:23:56 +03:00
|
|
|
var kStrictMode = {
|
2012-03-13 05:14:45 +04:00
|
|
|
attrs: { },
|
|
|
|
|
|
|
|
tags: {
|
|
|
|
'a': {
|
|
|
|
'title': true,
|
2016-11-21 22:18:00 +03:00
|
|
|
'href': kAllowedURLs,
|
|
|
|
'class': kAllowedAnchorClasses
|
2012-03-13 05:14:45 +04:00
|
|
|
},
|
|
|
|
'br': true,
|
|
|
|
'p': true
|
|
|
|
},
|
|
|
|
|
|
|
|
styles: { }
|
|
|
|
};
|
|
|
|
|
|
|
|
// standard mode allows basic formattings (bold, italic, underlined)
|
2015-10-11 00:23:56 +03:00
|
|
|
var kStandardMode = {
|
2012-03-13 05:14:45 +04:00
|
|
|
attrs: {
|
|
|
|
'style': true
|
|
|
|
},
|
|
|
|
|
|
|
|
tags: {
|
|
|
|
'div': true,
|
|
|
|
'a': {
|
|
|
|
'title': true,
|
2016-11-21 22:18:00 +03:00
|
|
|
'href': kAllowedURLs,
|
|
|
|
'class': kAllowedAnchorClasses
|
2012-03-13 05:14:45 +04:00
|
|
|
},
|
|
|
|
'em': true,
|
|
|
|
'strong': true,
|
|
|
|
'b': true,
|
|
|
|
'i': true,
|
|
|
|
'u': true,
|
|
|
|
'span': {
|
|
|
|
'class': kAllowedMozClasses
|
|
|
|
},
|
|
|
|
'br': true,
|
|
|
|
'code': true,
|
|
|
|
'ul': true,
|
|
|
|
'li': true,
|
|
|
|
'ol': true,
|
|
|
|
'cite': true,
|
|
|
|
'blockquote': true,
|
|
|
|
'p': true
|
|
|
|
},
|
|
|
|
|
|
|
|
styles: {
|
|
|
|
'font-style': true,
|
|
|
|
'font-weight': true,
|
2014-11-19 04:20:00 +03:00
|
|
|
'text-decoration-line': true
|
2012-03-13 05:14:45 +04:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// permissive mode allows about anything that isn't going to mess up the chat window
|
2015-10-11 00:23:56 +03:00
|
|
|
var kPermissiveMode = {
|
2012-03-13 05:14:45 +04:00
|
|
|
attrs: {
|
|
|
|
'style': true
|
|
|
|
},
|
|
|
|
|
|
|
|
tags : {
|
|
|
|
'div': true,
|
|
|
|
'a': {
|
|
|
|
'title': true,
|
2016-11-21 22:18:00 +03:00
|
|
|
'href': kAllowedURLs,
|
|
|
|
'class': kAllowedAnchorClasses
|
2012-03-13 05:14:45 +04:00
|
|
|
},
|
|
|
|
'font': {
|
|
|
|
'face': true,
|
|
|
|
'color': true,
|
|
|
|
'size': true
|
|
|
|
},
|
|
|
|
'em': true,
|
|
|
|
'strong': true,
|
|
|
|
'b': true,
|
|
|
|
'i': true,
|
|
|
|
'u': true,
|
|
|
|
'span': {
|
|
|
|
'class': kAllowedMozClasses
|
|
|
|
},
|
|
|
|
'br': true,
|
|
|
|
'hr': true,
|
|
|
|
'code': true,
|
|
|
|
'ul': true,
|
|
|
|
'li': true,
|
|
|
|
'ol': true,
|
|
|
|
'cite': true,
|
|
|
|
'blockquote': true,
|
|
|
|
'p': true
|
|
|
|
},
|
|
|
|
|
|
|
|
// FIXME: should be possible to use functions to filter values
|
|
|
|
styles : {
|
|
|
|
'color': true,
|
|
|
|
'font': true,
|
|
|
|
'font-family': true,
|
|
|
|
'font-size': true,
|
|
|
|
'font-style': true,
|
|
|
|
'font-weight': true,
|
2014-11-19 04:20:00 +03:00
|
|
|
'text-decoration-color': true,
|
|
|
|
'text-decoration-style': true,
|
|
|
|
'text-decoration-line': true
|
2012-03-13 05:14:45 +04:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2015-10-11 00:23:56 +03:00
|
|
|
var kModePref = "messenger.options.filterMode";
|
|
|
|
var kModes = [kStrictMode, kStandardMode, kPermissiveMode];
|
2012-03-13 05:14:45 +04:00
|
|
|
|
|
|
|
var gGlobalRuleset = null;
|
|
|
|
|
|
|
|
function initGlobalRuleset()
|
|
|
|
{
|
|
|
|
gGlobalRuleset = newRuleset();
|
|
|
|
|
2017-11-19 12:54:56 +03:00
|
|
|
Services.prefs.addObserver(kModePref, styleObserver);
|
2012-03-13 05:14:45 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
var styleObserver = {
|
|
|
|
observe: function so_observe(aObject, aTopic, aMsg) {
|
|
|
|
if (aTopic != "nsPref:changed" || aMsg != kModePref)
|
|
|
|
throw "bad notification";
|
|
|
|
|
|
|
|
if (!gGlobalRuleset)
|
|
|
|
throw "gGlobalRuleset not initialized";
|
|
|
|
|
|
|
|
setBaseRuleset(getModePref(), gGlobalRuleset);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
function getModePref()
|
|
|
|
{
|
|
|
|
let baseNum = Services.prefs.getIntPref(kModePref);
|
|
|
|
if (baseNum < 0 || baseNum > 2)
|
|
|
|
baseNum = 1;
|
|
|
|
|
|
|
|
return kModes[baseNum];
|
|
|
|
}
|
|
|
|
|
|
|
|
function setBaseRuleset(aBase, aResult)
|
|
|
|
{
|
2014-07-06 17:26:05 +04:00
|
|
|
for (let property in aBase)
|
|
|
|
aResult[property] = Object.create(aBase[property], aResult[property]);
|
2012-03-13 05:14:45 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
function newRuleset(aBase)
|
|
|
|
{
|
2012-09-04 18:56:08 +04:00
|
|
|
let result = {
|
|
|
|
tags: {},
|
|
|
|
attrs: {},
|
|
|
|
styles: {}
|
|
|
|
};
|
|
|
|
setBaseRuleset(aBase || getModePref(), result);
|
2012-03-13 05:14:45 +04:00
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
function createDerivedRuleset()
|
|
|
|
{
|
|
|
|
if (!gGlobalRuleset)
|
|
|
|
initGlobalRuleset();
|
|
|
|
return newRuleset(gGlobalRuleset);
|
|
|
|
}
|
|
|
|
|
2014-05-20 04:32:53 +04:00
|
|
|
function addGlobalAllowedTag(aTag, aAttrs = true)
|
2012-03-13 05:14:45 +04:00
|
|
|
{
|
2014-05-20 04:32:53 +04:00
|
|
|
gGlobalRuleset.tags[aTag] = aAttrs;
|
2012-03-13 05:14:45 +04:00
|
|
|
}
|
|
|
|
function removeGlobalAllowedTag(aTag)
|
|
|
|
{
|
|
|
|
delete gGlobalRuleset.tags[aTag];
|
|
|
|
}
|
|
|
|
|
2014-05-20 04:32:53 +04:00
|
|
|
function addGlobalAllowedAttribute(aAttr, aRule = true)
|
2012-03-13 05:14:45 +04:00
|
|
|
{
|
2014-05-20 04:32:53 +04:00
|
|
|
gGlobalRuleset.attrs[aAttr] = aRule;
|
2012-03-13 05:14:45 +04:00
|
|
|
}
|
|
|
|
function removeGlobalAllowedAttribute(aAttr)
|
|
|
|
{
|
|
|
|
delete gGlobalRuleset.attrs[aAttr];
|
|
|
|
}
|
|
|
|
|
2014-05-20 04:32:53 +04:00
|
|
|
function addGlobalAllowedStyleRule(aStyle, aRule = true)
|
2012-03-13 05:14:45 +04:00
|
|
|
{
|
2014-05-20 04:32:53 +04:00
|
|
|
gGlobalRuleset.styles[aStyle] = aRule;
|
2012-03-13 05:14:45 +04:00
|
|
|
}
|
|
|
|
function removeGlobalAllowedStyleRule(aStyle)
|
|
|
|
{
|
|
|
|
delete gGlobalRuleset.styles[aStyle];
|
|
|
|
}
|
|
|
|
|
|
|
|
function cleanupNode(aNode, aRules, aTextModifiers)
|
|
|
|
{
|
|
|
|
for (let i = 0; i < aNode.childNodes.length; ++i) {
|
|
|
|
let node = aNode.childNodes[i];
|
2018-01-30 13:19:59 +03:00
|
|
|
if (node.nodeType == node.ELEMENT_NODE &&
|
|
|
|
node.namespaceURI == "http://www.w3.org/1999/xhtml") {
|
2012-03-13 05:14:45 +04:00
|
|
|
// check if node allowed
|
2016-09-26 15:37:45 +03:00
|
|
|
let nodeName = node.localName;
|
2012-03-13 05:14:45 +04:00
|
|
|
if (!(nodeName in aRules.tags)) {
|
2013-02-20 03:43:50 +04:00
|
|
|
if (nodeName in kForbiddenTags) {
|
2018-02-28 20:33:34 +03:00
|
|
|
Cu.reportError("removing a " + nodeName +
|
|
|
|
" tag from a message before display");
|
2013-02-20 03:43:50 +04:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
// this node is not allowed, replace it with its children
|
|
|
|
while (node.hasChildNodes())
|
|
|
|
aNode.insertBefore(node.removeChild(node.firstChild), node);
|
|
|
|
}
|
2012-03-13 05:14:45 +04:00
|
|
|
aNode.removeChild(node);
|
|
|
|
// We want to process again the node at the index i which is
|
|
|
|
// now the first child of the node we removed
|
|
|
|
--i;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// we are going to keep this child node, clean up its children
|
|
|
|
cleanupNode(node, aRules, aTextModifiers);
|
|
|
|
|
|
|
|
// cleanup attributes
|
|
|
|
let attrs = node.attributes;
|
|
|
|
let acceptFunction = function(aAttrRules, aAttr) {
|
2018-03-20 07:21:00 +03:00
|
|
|
// an attribute is always accepted if its rule is true, or conditionally
|
2012-03-13 05:14:45 +04:00
|
|
|
// accepted if its rule is a function that evaluates to true
|
|
|
|
// if its rule does not exist, it is refused
|
|
|
|
let localName = aAttr.localName;
|
|
|
|
let rule = localName in aAttrRules && aAttrRules[localName];
|
|
|
|
return (rule === true ||
|
2012-09-04 18:56:08 +04:00
|
|
|
(typeof rule == "function" && rule(aAttr.value)));
|
2012-03-13 05:14:45 +04:00
|
|
|
};
|
|
|
|
for (let j = 0; j < attrs.length; ++j) {
|
|
|
|
let attr = attrs[j];
|
|
|
|
// we check both the list of accepted attributes for all tags
|
|
|
|
// and the list of accepted attributes for this specific tag.
|
|
|
|
if (!(acceptFunction(aRules.attrs, attr) ||
|
2012-09-04 18:56:08 +04:00
|
|
|
((typeof aRules.tags[nodeName] == "object") &&
|
|
|
|
acceptFunction(aRules.tags[nodeName], attr)))) {
|
2012-03-13 05:14:45 +04:00
|
|
|
node.removeAttribute(attr.name);
|
|
|
|
--j;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// cleanup style
|
|
|
|
let style = node.style;
|
|
|
|
for (let j = 0; j < style.length; ++j) {
|
|
|
|
if (!(style[j] in aRules.styles)) {
|
|
|
|
style.removeProperty(style[j]);
|
|
|
|
--j;
|
|
|
|
}
|
|
|
|
}
|
2017-11-26 12:33:11 +03:00
|
|
|
// Sort the style attributes for easier checking/comparing later.
|
|
|
|
if (node.hasAttribute("style")) {
|
|
|
|
let trailingSemi = false;
|
|
|
|
let attrs = node.getAttribute("style").trim();
|
|
|
|
if (attrs.endsWith(";")) {
|
|
|
|
attrs = attrs.slice(0, -1);
|
|
|
|
trailingSemi = true;
|
|
|
|
}
|
|
|
|
attrs = attrs.split(";").map(a => a.trim());
|
|
|
|
attrs.sort();
|
|
|
|
node.setAttribute("style", attrs.join("; ") + (trailingSemi?";":""));
|
|
|
|
}
|
2012-03-13 05:14:45 +04:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
// We are on a text node, we need to apply the functions
|
|
|
|
// provided in the aTextModifiers array.
|
|
|
|
|
|
|
|
// Each of these function should return the number of nodes added:
|
|
|
|
// * -1 if the current textnode was deleted
|
|
|
|
// * 0 if the node count is unchanged
|
|
|
|
// * positive value if nodes were added.
|
|
|
|
// For instance, adding an <img> tag for a smiley adds 2 nodes:
|
|
|
|
// - the img tag
|
|
|
|
// - the new text node after the img tag.
|
|
|
|
|
|
|
|
// This is the number of nodes we need to process. If new nodes
|
|
|
|
// are created, the next text modifier functions have more nodes
|
|
|
|
// to process.
|
|
|
|
let textNodeCount = 1;
|
2016-01-23 09:53:04 +03:00
|
|
|
for (let modifier of aTextModifiers)
|
2012-03-13 05:14:45 +04:00
|
|
|
for (let n = 0; n < textNodeCount; ++n) {
|
|
|
|
let textNode = aNode.childNodes[i + n];
|
|
|
|
|
|
|
|
// If we are processing nodes created by one of the previous
|
|
|
|
// text modifier function, some of the nodes are likely not
|
|
|
|
// text node, skip them.
|
2018-03-13 19:07:00 +03:00
|
|
|
if (textNode.nodeType != textNode.TEXT_NODE &&
|
|
|
|
textNode.nodeType != textNode.CDATA_SECTION_NODE)
|
2012-03-13 05:14:45 +04:00
|
|
|
continue;
|
|
|
|
|
|
|
|
let result = modifier(textNode);
|
|
|
|
textNodeCount += result;
|
|
|
|
n += result;
|
|
|
|
}
|
|
|
|
|
|
|
|
// newly created nodes should not be filtered, be sure we skip them!
|
|
|
|
i += textNodeCount - 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-05-20 04:32:53 +04:00
|
|
|
function cleanupImMarkup(aText, aRuleset, aTextModifiers = [])
|
2012-03-13 05:14:45 +04:00
|
|
|
{
|
|
|
|
if (!gGlobalRuleset)
|
|
|
|
initGlobalRuleset();
|
|
|
|
|
2018-04-21 12:52:24 +03:00
|
|
|
let parser = new DOMParser();
|
2013-02-19 03:07:29 +04:00
|
|
|
// Wrap the text to be parsed in a <span> to avoid losing leading whitespace.
|
|
|
|
let doc = parser.parseFromString("<span>" + aText + "</span>", "text/html");
|
|
|
|
let span = doc.querySelector("span");
|
2014-05-20 04:32:53 +04:00
|
|
|
cleanupNode(span, aRuleset || gGlobalRuleset, aTextModifiers);
|
2013-02-19 03:07:29 +04:00
|
|
|
return span.innerHTML;
|
2012-03-13 05:14:45 +04:00
|
|
|
}
|