/*
* 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 Serves CSS for element hiding and processes hits.
*/
try
{
// Hack: SDK loader masks our Components object with a getter.
let proto = Object.getPrototypeOf(this);
let property = Object.getOwnPropertyDescriptor(proto, "Components");
if (property && property.get)
delete proto.Components;
}
catch (e)
{
Cu.reportError(e);
}
let {XPCOMUtils} = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
let {shouldAllowAsync} = require("child/contentPolicy");
let {getFrames, isPrivate, getRequestWindow} = require("child/utils");
let {RequestNotifier} = require("child/requestNotifier");
let {port} = require("messaging");
let {Utils} = require("utils");
const notImplemented = () => Cr.NS_ERROR_NOT_IMPLEMENTED;
/**
* about: URL module used to count hits.
* @class
*/
let AboutHandler =
{
classID: Components.ID("{55fb7be0-1dd2-11b2-98e6-9e97caf8ba67}"),
classDescription: "Element hiding hit registration protocol handler",
aboutPrefix: "abp-elemhide",
/**
* Registers handler on startup.
*/
init: function()
{
let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
registrar.registerFactory(this.classID, this.classDescription,
"@mozilla.org/network/protocol/about;1?what=" + this.aboutPrefix, this);
onShutdown.add(function()
{
registrar.unregisterFactory(this.classID, this);
}.bind(this));
},
//
// Factory implementation
//
createInstance: function(outer, iid)
{
if (outer != null)
throw Cr.NS_ERROR_NO_AGGREGATION;
return this.QueryInterface(iid);
},
//
// About module implementation
//
getURIFlags: function(uri)
{
return Ci.nsIAboutModule.HIDE_FROM_ABOUTABOUT;
},
newChannel: function(uri, loadInfo)
{
let match = /\?hit(\d+)$/.exec(uri.path);
if (match)
return new HitRegistrationChannel(uri, loadInfo, match[1]);
match = /\?css(?:=(.*?))?(&specificonly)?$/.exec(uri.path);
if (match)
{
return new StyleDataChannel(uri, loadInfo,
match[1] ? decodeURIComponent(match[1]) : null, !!match[2]);
}
throw Cr.NS_ERROR_FAILURE;
},
QueryInterface: XPCOMUtils.generateQI([Ci.nsIFactory, Ci.nsIAboutModule])
};
AboutHandler.init();
/**
* Base class for channel implementations, subclasses usually only need to
* override BaseChannel._getResponse() method.
* @constructor
*/
function BaseChannel(uri, loadInfo)
{
this.URI = this.originalURI = uri;
this.loadInfo = loadInfo;
}
BaseChannel.prototype = {
URI: null,
originalURI: null,
contentCharset: "utf-8",
contentLength: 0,
contentType: null,
owner: Utils.systemPrincipal,
securityInfo: null,
notificationCallbacks: null,
loadFlags: 0,
loadGroup: null,
name: null,
status: Cr.NS_OK,
_getResponse: notImplemented,
_checkSecurity: function()
{
if (!this.loadInfo.triggeringPrincipal.equals(Utils.systemPrincipal))
throw Cr.NS_ERROR_FAILURE;
},
asyncOpen: function(listener, context)
{
Promise.resolve(this._getResponse()).then(data =>
{
let stream = Cc["@mozilla.org/io/string-input-stream;1"]
.createInstance(Ci.nsIStringInputStream);
stream.setData(data, data.length);
try
{
listener.onStartRequest(this, context);
}
catch(e)
{
// Listener failing isn't our problem
}
try
{
listener.onDataAvailable(this, context, stream, 0, stream.available());
}
catch(e)
{
// Listener failing isn't our problem
}
try
{
listener.onStopRequest(this, context, Cr.NS_OK);
}
catch(e)
{
// Listener failing isn't our problem
}
});
},
asyncOpen2: function(listener)
{
this._checkSecurity();
this.asyncOpen(listener, null);
},
open: function()
{
let data = this._getResponse();
if (typeof data.then == "function")
throw Cr.NS_ERROR_NOT_IMPLEMENTED;
let stream = Cc["@mozilla.org/io/string-input-stream;1"]
.createInstance(Ci.nsIStringInputStream);
stream.setData(data, data.length);
return stream;
},
open2: function()
{
this._checkSecurity();
return this.open();
},
isPending: () => false,
cancel: notImplemented,
suspend: notImplemented,
resume: notImplemented,
QueryInterface: XPCOMUtils.generateQI([Ci.nsIChannel, Ci.nsIRequest])
};
/**
* Channel returning CSS data for the global as well as site-specific stylesheet.
* @constructor
*/
function StyleDataChannel(uri, loadInfo, domain, specificOnly)
{
BaseChannel.call(this, uri, loadInfo);
this._domain = domain;
this._specificOnly = specificOnly;
}
StyleDataChannel.prototype = {
__proto__: BaseChannel.prototype,
contentType: "text/css",
_domain: null,
_getResponse: function()
{
function escapeChar(match)
{
return "\\" + match.charCodeAt(0).toString(16) + " ";
}
// Would be great to avoid sync messaging here but nsIStyleSheetService
// insists on opening channels synchronously.
let [selectors, keys] = (this._domain ?
port.emitSync("getSelectorsForDomain", [this._domain, this._specificOnly]) :
port.emitSync("getUnconditionalSelectors"));
let cssPrefix = "{-moz-binding: url(about:abp-elemhide?hit";
let cssSuffix = "#dummy) !important;}\n";
let result = [];
for (let i = 0; i < selectors.length; i++)
{
let selector = selectors[i];
let key = keys[i];
result.push(selector.replace(/[^\x01-\x7F]/g, escapeChar),
cssPrefix, key, cssSuffix);
}
return result.join("");
}
};
/**
* Channel returning data for element hiding hits.
* @constructor
*/
function HitRegistrationChannel(uri, loadInfo, key)
{
BaseChannel.call(this, uri, loadInfo);
this.key = key;
}
HitRegistrationChannel.prototype = {
__proto__: BaseChannel.prototype,
key: null,
contentType: "text/xml",
_getResponse: function()
{
let window = getRequestWindow(this);
port.emitWithResponse("registerElemHideHit", {
key: this.key,
frames: getFrames(window),
isPrivate: isPrivate(window)
}).then(hit =>
{
if (hit)
RequestNotifier.addNodeData(window.document, window.top, hit);
});
return "";
}
};
let observer = {
QueryInterface: XPCOMUtils.generateQI([
Ci.nsIObserver, Ci.nsISupportsWeakReference
]),
topic: "document-element-inserted",
styleURL: Utils.makeURI("about:abp-elemhide?css"),
sheet: null,
init: function()
{
Services.obs.addObserver(this, this.topic, true);
onShutdown.add(() =>
{
Services.obs.removeObserver(this, this.topic);
});
port.on("elemhideupdate", () =>
{
this.sheet = null;
});
},
observe: function(subject, topic, data)
{
if (topic != this.topic)
return;
let window = subject.defaultView;
if (!window)
{
// This is typically XBL bindings and SVG images, but also real
// documents occasionally - probably due to speculative loading?
return;
}
let type = window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShellTreeItem)
.itemType;
if (type != Ci.nsIDocShellTreeItem.typeContent)
return;
port.emitWithResponse("elemhideEnabled", {
frames: getFrames(window),
isPrivate: isPrivate(window)
}).then(({
enabled, contentType, docDomain, thirdParty, location, filter,
filterType
}) =>
{
if (Cu.isDeadWrapper(window))
{
// We are too late, the window is gone already.
return;
}
if (enabled)
{
let utils = window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
// If we have a filter hit at this point then it must be a $generichide
// filter - apply only specific element hiding filters.
let specificOnly = !!filter;
if (!specificOnly)
{
if (!this.sheet)
{
this.sheet = Utils.styleService.preloadSheet(this.styleURL,
Ci.nsIStyleSheetService.USER_SHEET);
}
try
{
utils.addSheet(this.sheet, Ci.nsIStyleSheetService.USER_SHEET);
}
catch (e)
{
// Ignore NS_ERROR_ILLEGAL_VALUE - it will be thrown if we try to add
// the stylesheet multiple times to the same document (the observer
// will be notified twice for some documents).
if (e.result != Cr.NS_ERROR_ILLEGAL_VALUE)
throw e;
}
}
let host = window.location.hostname;
if (host)
{
try
{
let suffix = "=" + encodeURIComponent(host);
if (specificOnly)
suffix += "&specificonly";
utils.loadSheetUsingURIString(this.styleURL.spec + suffix,
Ci.nsIStyleSheetService.USER_SHEET);
}
catch (e)
{
// Ignore NS_ERROR_ILLEGAL_VALUE - it will be thrown if we try to add
// the stylesheet multiple times to the same document (the observer
// will be notified twice for some documents).
if (e.result != Cr.NS_ERROR_ILLEGAL_VALUE)
throw e;
}
}
}
if (filter)
{
RequestNotifier.addNodeData(window.document, window.top, {
contentType, docDomain, thirdParty, location, filter, filterType
});
}
});
}
};
observer.init();