releases-comm-central/chat/modules/imContentSink.jsm

371 строка
10 KiB
JavaScript

/* 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/. */
ChromeUtils.import("resource:///modules/imServices.jsm");
Cu.importGlobalProperties(["DOMParser"]);
this.EXPORTED_SYMBOLS = [
"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
// an additional thing in a specific
// 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.
* example: 'href': aValue => aValue == 'about:blank'
*
* - 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.
*/
var kAllowedURLs = aValue => /^(https?|ftp|mailto):/.test(aValue);
var kAllowedMozClasses =
aClassName => aClassName == "moz-txt-underscore" ||
aClassName == "moz-txt-tag" ||
aClassName == "ib-person";
var kAllowedAnchorClasses = aClassName => aClassName == "ib-person";
/* Tags whose content should be fully removed, and reported in the Error Console. */
var kForbiddenTags = {
script: true,
style: true
};
// in strict mode, remove all formatings. Keep only links and line breaks.
var kStrictMode = {
attrs: { },
tags: {
'a': {
'title': true,
'href': kAllowedURLs,
'class': kAllowedAnchorClasses
},
'br': true,
'p': true
},
styles: { }
};
// standard mode allows basic formattings (bold, italic, underlined)
var kStandardMode = {
attrs: {
'style': true
},
tags: {
'div': true,
'a': {
'title': true,
'href': kAllowedURLs,
'class': kAllowedAnchorClasses
},
'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,
'text-decoration-line': true
}
};
// permissive mode allows about anything that isn't going to mess up the chat window
var kPermissiveMode = {
attrs: {
'style': true
},
tags : {
'div': true,
'a': {
'title': true,
'href': kAllowedURLs,
'class': kAllowedAnchorClasses
},
'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,
'text-decoration-color': true,
'text-decoration-style': true,
'text-decoration-line': true
}
};
var kModePref = "messenger.options.filterMode";
var kModes = [kStrictMode, kStandardMode, kPermissiveMode];
var gGlobalRuleset = null;
function initGlobalRuleset()
{
gGlobalRuleset = newRuleset();
Services.prefs.addObserver(kModePref, styleObserver);
}
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)
{
for (let property in aBase)
aResult[property] = Object.create(aBase[property], aResult[property]);
}
function newRuleset(aBase)
{
let result = {
tags: {},
attrs: {},
styles: {}
};
setBaseRuleset(aBase || getModePref(), result);
return result;
}
function createDerivedRuleset()
{
if (!gGlobalRuleset)
initGlobalRuleset();
return newRuleset(gGlobalRuleset);
}
function addGlobalAllowedTag(aTag, aAttrs = true)
{
gGlobalRuleset.tags[aTag] = aAttrs;
}
function removeGlobalAllowedTag(aTag)
{
delete gGlobalRuleset.tags[aTag];
}
function addGlobalAllowedAttribute(aAttr, aRule = true)
{
gGlobalRuleset.attrs[aAttr] = aRule;
}
function removeGlobalAllowedAttribute(aAttr)
{
delete gGlobalRuleset.attrs[aAttr];
}
function addGlobalAllowedStyleRule(aStyle, aRule = true)
{
gGlobalRuleset.styles[aStyle] = aRule;
}
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];
if (node.nodeType == node.ELEMENT_NODE &&
node.namespaceURI == "http://www.w3.org/1999/xhtml") {
// check if node allowed
let nodeName = node.localName;
if (!(nodeName in aRules.tags)) {
if (nodeName in kForbiddenTags) {
Cu.reportError("removing a " + nodeName +
" tag from a message before display");
}
else {
// this node is not allowed, replace it with its children
while (node.hasChildNodes())
aNode.insertBefore(node.removeChild(node.firstChild), node);
}
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) {
// an attribute is always accepted if its rule is true, or conditionally
// 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 ||
(typeof rule == "function" && rule(aAttr.value)));
};
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) ||
((typeof aRules.tags[nodeName] == "object") &&
acceptFunction(aRules.tags[nodeName], attr)))) {
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;
}
}
// 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?";":""));
}
}
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;
for (let modifier of aTextModifiers)
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.
if (textNode.nodeType != textNode.TEXT_NODE &&
textNode.nodeType != textNode.CDATA_SECTION_NODE)
continue;
let result = modifier(textNode);
textNodeCount += result;
n += result;
}
// newly created nodes should not be filtered, be sure we skip them!
i += textNodeCount - 1;
}
}
}
function cleanupImMarkup(aText, aRuleset, aTextModifiers = [])
{
if (!gGlobalRuleset)
initGlobalRuleset();
let parser = new DOMParser();
// 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");
cleanupNode(span, aRuleset || gGlobalRuleset, aTextModifiers);
return span.innerHTML;
}