CanvasBlocker/lib/extension.js

330 lines
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/. */
(function(){
"use strict";
let scope;
if ((typeof exports) !== "undefined"){
scope = exports;
}
else {
scope = require.register("./extension", {});
}
const browserAvailable = typeof browser !== "undefined";
const logging = require("./logging");
scope.inBackgroundScript = !!(
browserAvailable &&
browser.extension.getBackgroundPage &&
browser.extension.getBackgroundPage() === window
);
scope.getTranslation = browserAvailable? function getTranslation(id){
return browser.i18n.getMessage(id);
}: function(id){
return id;
};
scope.parseTranslation = function parseTranslation(message, parameters = {}){
const container = document.createDocumentFragment();
message.split(/(\{[^}]+\})/).forEach(function(part){
if (part.startsWith("{") && part.endsWith("}")){
part = part.substring(1, part.length - 1);
const args = part.split(":");
switch (args[0]){
case "image": {
const image = document.createElement("img");
image.className = "noticeImage";
image.src = args.slice(1).join(":");
container.appendChild(image);
break;
}
case "link": {
const link = document.createElement("a");
link.target = "_blank";
link.textContent = args[1];
link.href = args.slice(2).join(":");
container.appendChild(link);
break;
}
default:
if (parameters[args[0]]){
const parameter = parameters[args[0]];
if ((typeof parameter) === "function"){
container.appendChild(parameter(args.slice(1).join(":")));
}
else {
container.appendChild(document.createTextNode(parameter));
}
}
else {
container.appendChild(document.createTextNode(part));
}
}
}
else {
container.appendChild(document.createTextNode(part));
}
});
return container;
};
scope.getURL = function getURL(str){
return browser.runtime.getURL(str);
};
scope.extensionID = browserAvailable? scope.getURL(""): "extensionID";
scope.inIncognitoContext = browserAvailable? browser.extension.inIncognitoContext: false;
scope.message = {
on: browserAvailable? function(callback){
return browser.runtime.onMessage.addListener(callback);
}: function(){
return false;
},
send: browserAvailable? function(data){
return browser.runtime.sendMessage(data);
}: function(){
return false;
}
};
Object.seal(scope.message);
scope.getWrapped = function getWrapped(obj){
return obj && (obj.wrappedJSObject || obj);
};
scope.exportFunctionWithName = function exportFunctionWithName(func, context, name){
const targetObject = scope.getWrapped(context).Object.create(null);
const exportedTry = exportFunction(func, targetObject, {allowCrossOriginArguments: true, defineAs: name});
if (exportedTry.name === name){
return exportedTry;
}
else {
if (func.name === name){
logging.message(
"FireFox bug: Need to change name in exportFunction from",
exportedTry.name,
"(originally correct) to",
name
);
}
else {
logging.error("Wrong name specified for", name, new Error());
}
const wrappedContext = scope.getWrapped(context);
const options = {
allowCrossOriginArguments: true,
defineAs: name
};
const oldDescriptor = Object.getOwnPropertyDescriptor(wrappedContext, name);
if (oldDescriptor && !oldDescriptor.configurable){
logging.error(
"Unable to export function with the correct name", name,
"instead we have to use", exportedTry.name
);
return exportedTry;
}
const exported = exportFunction(func, context, options);
if (oldDescriptor){
Object.defineProperty(wrappedContext, name, oldDescriptor);
}
else {
delete wrappedContext[name];
}
return exported;
}
};
const proxies = new Map();
const changedWindowsForProxies = new WeakMap();
function setupWindowForProxies(window){
if (changedWindowsForProxies.get(window)){
return;
}
const wrappedWindow = scope.getWrapped(window);
const functionPrototype = wrappedWindow.Function.prototype;
const originalToString = functionPrototype.toString;
changedWindowsForProxies.set(window, originalToString);
const alteredToString = scope.createProxyFunction(
window,
originalToString,
function toString(){
if (proxies.has(this)){
return proxies.get(this).string;
}
return originalToString.call(scope.getWrapped(this));
}
);
scope.changeProperty(window, "toString", {
object: functionPrototype,
name: "toString",
type: "value",
changed: alteredToString
});
const wrappedReflect = wrappedWindow.Reflect;
const originalReflectSetPrototypeOf = wrappedReflect.setPrototypeOf;
const alteredReflectSetPrototypeOf = scope.exportFunctionWithName(
function setPrototypeOf(target, prototype){
target = scope.getWrapped(target);
if (proxies.has(target)){
target = proxies.get(target).wrappedOriginal;
}
if (proxies.has(prototype)){
prototype = proxies.get(prototype).wrappedOriginal;
}
if (prototype){
const grandPrototype = wrappedReflect.getPrototypeOf(prototype);
if (proxies.has(grandPrototype)){
const testPrototype = wrappedWindow.Object.create(proxies.get(grandPrototype).wrappedOriginal);
const value = originalReflectSetPrototypeOf.call(wrappedReflect, target, testPrototype);
if (!value){
return false;
}
}
}
const value = originalReflectSetPrototypeOf.call(wrappedReflect, target, scope.getWrapped(prototype));
return value;
}, window, "setPrototypeOf"
);
scope.changeProperty(window, "toString", {
object: wrappedWindow.Reflect,
name: "setPrototypeOf",
type: "value",
changed: alteredReflectSetPrototypeOf
});
}
scope.createProxyFunction = function createProxyFunction(window, original, replacement){
setupWindowForProxies(window);
const wrappedObject = scope.getWrapped(window).Object;
const handler = wrappedObject.create(null);
handler.apply = scope.exportFunctionWithName(function(target, thisArg, args){
try {
return args.length?
replacement.call(thisArg, ...args):
replacement.call(thisArg);
}
catch (error){
try {
return original.apply(thisArg, args);
}
catch (error){
return scope.getWrapped(target).apply(thisArg, args);
}
}
}, window, "");
handler.setPrototypeOf = scope.exportFunctionWithName(function(target, prototype){
target = scope.getWrapped(target);
if (proxies.has(target)){
target = proxies.get(target).wrappedOriginal;
}
if (proxies.has(prototype)){
prototype = proxies.get(prototype).wrappedOriginal;
}
if (prototype){
const grandPrototype = wrappedObject.getPrototypeOf(prototype);
if (proxies.has(grandPrototype)){
const testPrototype = wrappedObject.create(proxies.get(grandPrototype).wrappedOriginal);
wrappedObject.setPrototypeOf(target, testPrototype);
}
}
return wrappedObject.setPrototypeOf(target, scope.getWrapped(prototype));
}, window, "");
const proxy = new window.Proxy(original, handler);
const proxyData = {
original: original,
wrappedOriginal: scope.getWrapped(original),
string: changedWindowsForProxies.get(window).call(original),
};
proxies.set(proxy, proxyData);
proxies.set(scope.getWrapped(proxy), proxyData);
return scope.getWrapped(proxy);
};
const changedPropertiesByWindow = new WeakMap();
scope.changeProperty = function(window, group, {object, name, type, changed}){
let changedProperties = changedPropertiesByWindow.get(scope.getWrapped(window));
if (!changedProperties){
changedProperties = [];
changedPropertiesByWindow.set(scope.getWrapped(window), changedProperties);
}
const descriptor = Object.getOwnPropertyDescriptor(object, name);
const original = descriptor[type];
descriptor[type] = changed;
Object.defineProperty(object, name, descriptor);
changedProperties.push({group, object, name, type, original});
};
scope.revertProperties = function(window, group){
window = scope.getWrapped(window);
let changedProperties = changedPropertiesByWindow.get(window);
if (!changedProperties){
return;
}
if (group){
const remainingProperties = changedProperties.filter(function(changedProperty){
return changedProperty.group !== group;
});
changedPropertiesByWindow.set(window, remainingProperties);
changedProperties = changedProperties.filter(function(changedProperty){
return changedProperty.group === group;
});
}
else {
changedPropertiesByWindow.delete(window);
}
for (let i = changedProperties.length - 1; i >= 0; i -= 1){
const {object, name, type, original} = changedProperties[i];
logging.verbose("reverting", name, "on", object);
const descriptor = Object.getOwnPropertyDescriptor(object, name);
descriptor[type] = original;
Object.defineProperty(object, name, descriptor);
}
};
scope.waitSync = function waitSync(reason = "for no reason"){
logging.message(`Starting synchronous request ${reason}.`);
try {
let xhr = new XMLHttpRequest();
xhr.open("GET", "https://[::]", false);
xhr.send();
xhr = null;
}
catch (error){
logging.verbose("Error in XHR:", error);
}
};
scope.displayVersion = async function displayVersion(node, displayRefresh = false){
if ("string" === typeof node){
node = document.getElementById(node);
}
if (!node){
throw "display node not found";
}
fetch(scope.getURL("manifest.json")).then(function(response){
return response.json();
}).then(function(manifest){
node.textContent = "Version " + manifest.version;
return manifest.version;
}).catch(function(error){
node.textContent = "Unable to get version: " + error;
});
if (displayRefresh){
// Workaround to hide the scroll bars
window.setTimeout(function(){
node.style.display = "none";
node.style.display = "";
}, displayRefresh);
}
};
Object.seal(scope);
}());