/*
* This file is part of Adblock Plus ,
* Copyright (C) 2006-2017 eyeo GmbH
*
* Adblock Plus is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* Adblock Plus is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Adblock Plus. If not, see .
*/
/**
* @fileOverview Stores Adblock Plus data to be attached to a window.
*/
let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
let {port} = require("messaging");
let {Utils} = require("utils");
let {Flasher} = require("child/flasher");
let nodeData = new WeakMap();
let windowStats = new WeakMap();
let windowData = new WeakMap();
let requestEntryMaxId = 0;
/**
* Active RequestNotifier instances by their ID
* @type Map.
*/
let notifiers = new Map();
port.on("startWindowScan", onStartScan);
port.on("shutdownNotifier", onNotifierShutdown);
port.on("flashNodes", onFlashNodes);
port.on("retrieveNodeSize", onRetrieveNodeSize);
port.on("storeNodesForEntries", onStoreNodes);
port.on("retrieveWindowStats", onRetrieveWindowStats);
port.on("storeWindowData", onStoreWindowData);
port.on("retrieveWindowData", onRetrieveWindowData);
function onStartScan({notifierID, outerWindowID})
{
let window = Services.wm.getOuterWindowWithId(outerWindowID);
if (window)
new RequestNotifier(window, notifierID);
}
function onNotifierShutdown(notifierID)
{
let notifier = notifiers.get(notifierID);
if (notifier)
notifier.shutdown();
}
function onFlashNodes({notifierID, requests, scrollToItem})
{
let notifier = notifiers.get(notifierID);
if (notifier)
notifier.flashNodes(requests, scrollToItem);
}
function onRetrieveNodeSize({notifierID, requests})
{
let notifier = notifiers.get(notifierID);
if (notifier)
return notifier.retrieveNodeSize(requests);
}
function onStoreNodes({notifierID, requests})
{
let notifier = notifiers.get(notifierID);
if (notifier)
return notifier.storeNodesForEntries(requests);
}
function onRetrieveWindowStats(outerWindowID)
{
let window = Services.wm.getOuterWindowWithId(outerWindowID);
if (window)
return RequestNotifier.getWindowStatistics(window);
}
function onStoreWindowData({outerWindowID, data})
{
let window = Services.wm.getOuterWindowWithId(outerWindowID);
if (window)
windowData.set(window.document, data);
};
function onRetrieveWindowData(outerWindowID)
{
let window = Services.wm.getOuterWindowWithId(outerWindowID);
if (window)
return windowData.get(window.document) || null;
};
/**
* Creates a notifier object for a particular window. After creation the window
* will first be scanned for previously saved requests. Once that scan is
* complete only new requests for this window will be reported.
* @param {Window} window window to attach the notifier to
* @param {Integer} notifierID Parent notifier ID to be messaged
*/
function RequestNotifier(window, notifierID)
{
this.window = window;
this.id = notifierID;
notifiers.set(this.id, this);
this.nodes = new Map();
this.startScan(window);
}
exports.RequestNotifier = RequestNotifier;
RequestNotifier.prototype =
{
/**
* Parent notifier ID to be messaged
* @type Integer
*/
id: null,
/**
* The window this notifier is associated with.
* @type Window
*/
window: null,
/**
* Nodes associated with a particular request ID.
* @type Map.
*/
nodes: null,
/**
* Shuts down the notifier once it is no longer used. The listener
* will no longer be called after that.
*/
shutdown: function()
{
delete this.window;
delete this.nodes;
this.stopFlashing();
notifiers.delete(this.id);
},
/**
* Notifies the parent about a new request.
* @param {Node} node DOM node that the request is associated with
* @param {Object} entry
*/
notifyListener: function(node, entry)
{
if (this.nodes)
this.nodes.set(entry.id, node);
port.emit("foundNodeData", {
notifierID: this.id,
data: entry
});
},
onComplete: function()
{
port.emit("scanComplete", this.id);
},
/**
* Number of currently posted scan events (will be 0 when the scan finishes
* running).
*/
eventsPosted: 0,
/**
* Starts the initial scan of the window (will recurse into frames).
* @param {Window} wnd the window to be scanned
*/
startScan: function(wnd)
{
let doc = wnd.document;
let walker = doc.createTreeWalker(doc, Ci.nsIDOMNodeFilter.SHOW_ELEMENT, null, false);
let process = function()
{
// Don't do anything if the notifier was shut down already.
if (!this.window)
return;
let node = walker.currentNode;
let data = nodeData.get(node);
if (typeof data != "undefined")
for (let k in data)
this.notifyListener(node, data[k]);
if (walker.nextNode())
Utils.runAsync(process);
else
{
// Done with the current window, start the scan for its frames
for (let i = 0; i < wnd.frames.length; i++)
this.startScan(wnd.frames[i]);
this.eventsPosted--;
if (!this.eventsPosted)
{
this.scanComplete = true;
this.onComplete();
}
}
}.bind(this);
// Process each node in a separate event to allow other events to process
this.eventsPosted++;
Utils.runAsync(process);
},
/**
* Makes the nodes associated with the given requests blink.
* @param {number[]} requests list of request IDs that were previously
* reported by this notifier.
* @param {boolean} scrollToItem if true, scroll to first node
*/
flashNodes: function(requests, scrollToItem)
{
this.stopFlashing();
let nodes = [];
for (let id of requests)
{
if (!this.nodes.has(id))
continue;
let node = this.nodes.get(id);
if (Cu.isDeadWrapper(node))
this.nodes.delete(node);
else if (node.nodeType == Ci.nsIDOMNode.ELEMENT_NODE)
nodes.push(node);
}
if (nodes.length)
this.flasher = new Flasher(nodes, scrollToItem);
},
/**
* Stops flashing nodes after a previous flashNodes() call.
*/
stopFlashing: function()
{
if (this.flasher)
this.flasher.stop();
this.flasher = null;
},
/**
* Attempts to calculate the size of the nodes associated with the requests.
* @param {number[]} requests list of request IDs that were previously
* reported by this notifier.
* @return {number[]|null} either an array containing width and height or
* null if the size could not be calculated.
*/
retrieveNodeSize: function(requests)
{
function getNodeSize(node)
{
if (node instanceof Ci.nsIDOMHTMLImageElement && (node.naturalWidth || node.naturalHeight))
return [node.naturalWidth, node.naturalHeight];
else if (node instanceof Ci.nsIDOMHTMLElement && (node.offsetWidth || node.offsetHeight))
return [node.offsetWidth, node.offsetHeight];
else
return null;
}
let size = null;
for (let id of requests)
{
if (!this.nodes.has(id))
continue;
let node = this.nodes.get(id);
if (Cu.isDeadWrapper(node))
this.nodes.delete(node);
else
{
size = getNodeSize(node);
if (size)
break;
}
}
return size;
},
/**
* Stores the nodes associated with the requests and generates a unique ID
* for them that can be used with Policy.refilterNodes().
* @param {number[]} requests list of request IDs that were previously
* reported by this notifier.
* @return {string} unique identifiers associated with the nodes.
*/
storeNodesForEntries: function(requests)
{
let nodes = [];
for (let id of requests)
{
if (!this.nodes.has(id))
continue;
let node = this.nodes.get(id);
if (Cu.isDeadWrapper(node))
this.nodes.delete(node);
else
nodes.push(node);
}
let {storeNodes} = require("child/contentPolicy");
return storeNodes(nodes);
}
};
/**
* Attaches request data to a DOM node.
* @param {Node} node node to attach data to
* @param {Window} topWnd top-level window the node belongs to
* @param {Object} hitData
* @param {String} hitData.contentType request type, e.g. "IMAGE"
* @param {String} hitData.docDomain domain of the document that initiated the request
* @param {Boolean} hitData.thirdParty will be true if a third-party server has been requested
* @param {String} hitData.location the address that has been requested
* @param {String} hitData.filter filter applied to the request or null if none
* @param {String} hitData.filterType type of filter applied to the request
*/
RequestNotifier.addNodeData = function(node, topWnd, {contentType, docDomain, thirdParty, location, filter, filterType})
{
let entry = {
id: ++requestEntryMaxId,
type: contentType,
docDomain, thirdParty, location, filter
};
let existingData = nodeData.get(node);
if (typeof existingData == "undefined")
{
existingData = {};
nodeData.set(node, existingData);
}
// Add this request to the node data
existingData[contentType + " " + location] = entry;
// Update window statistics
if (!windowStats.has(topWnd.document))
{
windowStats.set(topWnd.document, {
items: 0,
hidden: 0,
blocked: 0,
whitelisted: 0,
filters: {}
});
}
let stats = windowStats.get(topWnd.document);
if (filterType != "elemhide" && filterType != "elemhideexception" && filterType != "elemhideemulation")
stats.items++;
if (filter)
{
if (filterType == "blocking")
stats.blocked++;
else if (filterType == "whitelist" || filterType == "elemhideexception")
stats.whitelisted++;
else if (filterType == "elemhide" || filterType == "elemhideemulation")
stats.hidden++;
if (filter in stats.filters)
stats.filters[filter]++;
else
stats.filters[filter] = 1;
}
// Notify listeners
for (let notifier of notifiers.values())
if (!notifier.window || notifier.window == topWnd)
notifier.notifyListener(node, entry);
}
/**
* Retrieves the statistics for a window.
* @return {Object} Object with the properties items, blocked, whitelisted, hidden, filters containing statistics for the window (might be null)
*/
RequestNotifier.getWindowStatistics = function(/**Window*/ wnd)
{
if (windowStats.has(wnd.document))
return windowStats.get(wnd.document);
else
return null;
}
/**
* Retrieves the request data associated with a DOM node.
* @param {Node} node
* @param {Boolean} noParent if missing or false, the search will extend to the parent nodes until one is found that has data associated with it
* @param {Integer} [type] request type to be looking for
* @param {String} [location] request location to be looking for
* @result {[Node, Object]}
* @static
*/
RequestNotifier.getDataForNode = function(node, noParent, type, location)
{
while (node)
{
let data = nodeData.get(node);
if (typeof data != "undefined")
{
let entry = null;
// Look for matching entry
for (let k in data)
{
if ((!entry || entry.id < data[k].id) &&
(typeof type == "undefined" || data[k].type == type) &&
(typeof location == "undefined" || data[k].location == location))
{
entry = data[k];
}
}
if (entry)
return [node, entry];
}
// If we don't have any match on this node then maybe its parent will do
if ((typeof noParent != "boolean" || !noParent) &&
node.parentNode instanceof Ci.nsIDOMElement)
{
node = node.parentNode;
}
else
{
node = null;
}
}
return null;
};