зеркало из https://github.com/mozilla/gecko-dev.git
2152 строки
72 KiB
JavaScript
2152 строки
72 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/. */
|
|
/* This code is loaded in every child process that is started by mochitest in
|
|
* order to be used as a replacement for UniversalXPConnect
|
|
*/
|
|
|
|
"use strict";
|
|
|
|
var global = this;
|
|
|
|
var Ci = Components.interfaces;
|
|
var Cc = Components.classes;
|
|
var Cu = Components.utils;
|
|
|
|
Cu.import("chrome://specialpowers/content/MockFilePicker.jsm");
|
|
Cu.import("chrome://specialpowers/content/MockColorPicker.jsm");
|
|
Cu.import("chrome://specialpowers/content/MockPermissionPrompt.jsm");
|
|
Cu.import("resource://gre/modules/Services.jsm");
|
|
Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
|
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
Cu.import("resource://gre/modules/NetUtil.jsm");
|
|
|
|
// We're loaded with "this" not set to the global in some cases, so we
|
|
// have to play some games to get at the global object here. Normally
|
|
// we'd try "this" from a function called with undefined this value,
|
|
// but this whole file is in strict mode. So instead fall back on
|
|
// returning "this" from indirect eval, which returns the global.
|
|
if (!(function() { var e = eval; return e("this"); })().File) {
|
|
Cu.importGlobalProperties(["File"]);
|
|
}
|
|
|
|
// Allow stuff from this scope to be accessed from non-privileged scopes. This
|
|
// would crash if used outside of automation.
|
|
Cu.forcePermissiveCOWs();
|
|
|
|
function SpecialPowersAPI() {
|
|
this._consoleListeners = [];
|
|
this._encounteredCrashDumpFiles = [];
|
|
this._unexpectedCrashDumpFiles = { };
|
|
this._crashDumpDir = null;
|
|
this._mfl = null;
|
|
this._prefEnvUndoStack = [];
|
|
this._pendingPrefs = [];
|
|
this._applyingPrefs = false;
|
|
this._permissionsUndoStack = [];
|
|
this._pendingPermissions = [];
|
|
this._applyingPermissions = false;
|
|
this._observingPermissions = false;
|
|
this._fm = null;
|
|
this._cb = null;
|
|
}
|
|
|
|
function bindDOMWindowUtils(aWindow) {
|
|
if (!aWindow)
|
|
return
|
|
|
|
var util = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
|
|
.getInterface(Ci.nsIDOMWindowUtils);
|
|
return wrapPrivileged(util);
|
|
}
|
|
|
|
function getRawComponents(aWindow) {
|
|
// If we're running in automation that supports enablePrivilege, then we also
|
|
// provided access to the privileged Components.
|
|
try {
|
|
let win = Cu.waiveXrays(aWindow);
|
|
if (typeof win.netscape.security.PrivilegeManager == 'object')
|
|
Cu.forcePrivilegedComponentsForScope(aWindow);
|
|
} catch (e) {}
|
|
return Cu.getComponentsForScope(aWindow);
|
|
}
|
|
|
|
function isWrappable(x) {
|
|
if (typeof x === "object")
|
|
return x !== null;
|
|
return typeof x === "function";
|
|
};
|
|
|
|
function isWrapper(x) {
|
|
return isWrappable(x) && (typeof x.SpecialPowers_wrappedObject !== "undefined");
|
|
};
|
|
|
|
function unwrapIfWrapped(x) {
|
|
return isWrapper(x) ? unwrapPrivileged(x) : x;
|
|
};
|
|
|
|
function wrapIfUnwrapped(x) {
|
|
return isWrapper(x) ? x : wrapPrivileged(x);
|
|
}
|
|
|
|
function isObjectOrArray(obj) {
|
|
if (Object(obj) !== obj)
|
|
return false;
|
|
let arrayClasses = ['Object', 'Array', 'Int8Array', 'Uint8Array',
|
|
'Int16Array', 'Uint16Array', 'Int32Array',
|
|
'Uint32Array', 'Float32Array', 'Float64Array',
|
|
'Uint8ClampedArray'];
|
|
let className = Cu.getClassName(obj, true);
|
|
return arrayClasses.indexOf(className) != -1;
|
|
}
|
|
|
|
// In general, we want Xray wrappers for content DOM objects, because waiving
|
|
// Xray gives us Xray waiver wrappers that clamp the principal when we cross
|
|
// compartment boundaries. However, there are some exceptions where we want
|
|
// to use a waiver:
|
|
//
|
|
// * Xray adds some gunk to toString(), which has the potential to confuse
|
|
// consumers that aren't expecting Xray wrappers. Since toString() is a
|
|
// non-privileged method that returns only strings, we can just waive Xray
|
|
// for that case.
|
|
//
|
|
// * We implement Xrays to pure JS [[Object]] and [[Array]] instances that
|
|
// filter out tricky things like callables. This is the right thing for
|
|
// security in general, but tends to break tests that try to pass object
|
|
// literals into SpecialPowers. So we waive [[Object]] and [[Array]]
|
|
// instances before inspecting properties.
|
|
//
|
|
// * When we don't have meaningful Xray semantics, we create an Opaque
|
|
// XrayWrapper for security reasons. For test code, we generally want to see
|
|
// through that sort of thing.
|
|
function waiveXraysIfAppropriate(obj, propName) {
|
|
if (propName == 'toString' || isObjectOrArray(obj) ||
|
|
/Opaque/.test(Object.prototype.toString.call(obj)))
|
|
{
|
|
return XPCNativeWrapper.unwrap(obj);
|
|
}
|
|
return obj;
|
|
}
|
|
|
|
// We can't call apply() directy on Xray-wrapped functions, so we have to be
|
|
// clever.
|
|
function doApply(fun, invocant, args) {
|
|
// We implement Xrays to pure JS [[Object]] instances that filter out tricky
|
|
// things like callables. This is the right thing for security in general,
|
|
// but tends to break tests that try to pass object literals into
|
|
// SpecialPowers. So we waive [[Object]] instances when they're passed to a
|
|
// SpecialPowers-wrapped callable.
|
|
//
|
|
// Note that the transitive nature of Xray waivers means that any property
|
|
// pulled off such an object will also be waived, and so we'll get principal
|
|
// clamping for Xrayed DOM objects reached from literals, so passing things
|
|
// like {l : xoWin.location} won't work. Hopefully the rabbit hole doesn't
|
|
// go that deep.
|
|
args = args.map(x => isObjectOrArray(x) ? Cu.waiveXrays(x) : x);
|
|
return Reflect.apply(fun, invocant, args);
|
|
}
|
|
|
|
function wrapPrivileged(obj) {
|
|
|
|
// Primitives pass straight through.
|
|
if (!isWrappable(obj))
|
|
return obj;
|
|
|
|
// No double wrapping.
|
|
if (isWrapper(obj))
|
|
throw "Trying to double-wrap object!";
|
|
|
|
let dummy;
|
|
if (typeof obj === "function")
|
|
dummy = function() {};
|
|
else
|
|
dummy = Object.create(null);
|
|
|
|
return new Proxy(dummy, new SpecialPowersHandler(obj));
|
|
};
|
|
|
|
function unwrapPrivileged(x) {
|
|
|
|
// We don't wrap primitives, so sometimes we have a primitive where we'd
|
|
// expect to have a wrapper. The proxy pretends to be the type that it's
|
|
// emulating, so we can just as easily check isWrappable() on a proxy as
|
|
// we can on an unwrapped object.
|
|
if (!isWrappable(x))
|
|
return x;
|
|
|
|
// If we have a wrappable type, make sure it's wrapped.
|
|
if (!isWrapper(x))
|
|
throw "Trying to unwrap a non-wrapped object!";
|
|
|
|
var obj = x.SpecialPowers_wrappedObject;
|
|
// unwrapped.
|
|
return obj;
|
|
};
|
|
|
|
function SpecialPowersHandler(wrappedObject) {
|
|
this.wrappedObject = wrappedObject;
|
|
}
|
|
|
|
SpecialPowersHandler.prototype = {
|
|
construct(target, args) {
|
|
// The arguments may or may not be wrappers. Unwrap them if necessary.
|
|
var unwrappedArgs = Array.prototype.slice.call(args).map(unwrapIfWrapped);
|
|
|
|
// We want to invoke "obj" as a constructor, but using unwrappedArgs as
|
|
// the arguments. Make sure to wrap and re-throw exceptions!
|
|
try {
|
|
return wrapIfUnwrapped(Reflect.construct(this.wrappedObject, unwrappedArgs));
|
|
} catch (e) {
|
|
throw wrapIfUnwrapped(e);
|
|
}
|
|
},
|
|
|
|
apply(target, thisValue, args) {
|
|
// The invocant and arguments may or may not be wrappers. Unwrap
|
|
// them if necessary.
|
|
var invocant = unwrapIfWrapped(thisValue);
|
|
var unwrappedArgs = Array.prototype.slice.call(args).map(unwrapIfWrapped);
|
|
|
|
try {
|
|
return wrapIfUnwrapped(doApply(this.wrappedObject, invocant, unwrappedArgs));
|
|
} catch (e) {
|
|
// Wrap exceptions and re-throw them.
|
|
throw wrapIfUnwrapped(e);
|
|
}
|
|
},
|
|
|
|
has(target, prop) {
|
|
if (prop === "SpecialPowers_wrappedObject")
|
|
return true;
|
|
|
|
return Reflect.has(this.wrappedObject, prop);
|
|
},
|
|
|
|
get(target, prop, receiver) {
|
|
if (prop === "SpecialPowers_wrappedObject")
|
|
return this.wrappedObject;
|
|
|
|
let obj = waiveXraysIfAppropriate(this.wrappedObject, prop);
|
|
return wrapIfUnwrapped(Reflect.get(obj, prop));
|
|
},
|
|
|
|
set(target, prop, val, receiver) {
|
|
if (prop === "SpecialPowers_wrappedObject")
|
|
return false;
|
|
|
|
let obj = waiveXraysIfAppropriate(this.wrappedObject, prop);
|
|
return Reflect.set(obj, prop, unwrapIfWrapped(val));
|
|
},
|
|
|
|
delete(target, prop) {
|
|
if (prop === "SpecialPowers_wrappedObject")
|
|
return false;
|
|
|
|
return Reflect.deleteProperty(this.wrappedObject, prop);
|
|
},
|
|
|
|
defineProperty(target, prop, descriptor) {
|
|
throw "Can't call defineProperty on SpecialPowers wrapped object";
|
|
},
|
|
|
|
getOwnPropertyDescriptor(target, prop) {
|
|
// Handle our special API.
|
|
if (prop === "SpecialPowers_wrappedObject") {
|
|
return { value: this.wrappedObject, writeable: true,
|
|
configurable: true, enumerable: false };
|
|
}
|
|
|
|
let obj = waiveXraysIfAppropriate(this.wrappedObject, prop);
|
|
let desc = Reflect.getOwnPropertyDescriptor(obj, prop);
|
|
|
|
if (desc === undefined)
|
|
return undefined;
|
|
|
|
// Transitively maintain the wrapper membrane.
|
|
function wrapIfExists(key) {
|
|
if (key in desc)
|
|
desc[key] = wrapIfUnwrapped(desc[key]);
|
|
};
|
|
|
|
wrapIfExists('value');
|
|
wrapIfExists('get');
|
|
wrapIfExists('set');
|
|
|
|
// A trapping proxy's properties must always be configurable, but sometimes
|
|
// we come across non-configurable properties. Tell a white lie.
|
|
desc.configurable = true;
|
|
|
|
return desc;
|
|
},
|
|
|
|
ownKeys(target) {
|
|
// Insert our special API. It's not enumerable, but ownKeys()
|
|
// includes non-enumerable properties.
|
|
let props = ['SpecialPowers_wrappedObject'];
|
|
|
|
// Do the normal thing.
|
|
let flt = (a) => !props.includes(a);
|
|
props = props.concat(Reflect.ownKeys(this.wrappedObject).filter(flt));
|
|
|
|
// If we've got an Xray wrapper, include the expandos as well.
|
|
if ('wrappedJSObject' in this.wrappedObject) {
|
|
props = props.concat(Reflect.ownKeys(this.wrappedObject.wrappedJSObject)
|
|
.filter(flt));
|
|
}
|
|
|
|
return props;
|
|
},
|
|
|
|
preventExtensions(target) {
|
|
throw "Can't call preventExtensions on SpecialPowers wrapped object";
|
|
}
|
|
};
|
|
|
|
// SPConsoleListener reflects nsIConsoleMessage objects into JS in a
|
|
// tidy, XPCOM-hiding way. Messages that are nsIScriptError objects
|
|
// have their properties exposed in detail. It also auto-unregisters
|
|
// itself when it receives a "sentinel" message.
|
|
function SPConsoleListener(callback) {
|
|
this.callback = callback;
|
|
}
|
|
|
|
SPConsoleListener.prototype = {
|
|
observe: function(msg) {
|
|
let m = { message: msg.message,
|
|
errorMessage: null,
|
|
sourceName: null,
|
|
sourceLine: null,
|
|
lineNumber: null,
|
|
columnNumber: null,
|
|
category: null,
|
|
windowID: null,
|
|
isScriptError: false,
|
|
isWarning: false,
|
|
isException: false,
|
|
isStrict: false };
|
|
if (msg instanceof Ci.nsIScriptError) {
|
|
m.errorMessage = msg.errorMessage;
|
|
m.sourceName = msg.sourceName;
|
|
m.sourceLine = msg.sourceLine;
|
|
m.lineNumber = msg.lineNumber;
|
|
m.columnNumber = msg.columnNumber;
|
|
m.category = msg.category;
|
|
m.windowID = msg.outerWindowID;
|
|
m.innerWindowID = msg.innerWindowID;
|
|
m.isScriptError = true;
|
|
m.isWarning = ((msg.flags & Ci.nsIScriptError.warningFlag) === 1);
|
|
m.isException = ((msg.flags & Ci.nsIScriptError.exceptionFlag) === 1);
|
|
m.isStrict = ((msg.flags & Ci.nsIScriptError.strictFlag) === 1);
|
|
}
|
|
|
|
Object.freeze(m);
|
|
|
|
// Run in a separate runnable since console listeners aren't
|
|
// supposed to touch content and this one might.
|
|
Services.tm.mainThread.dispatch(() => {
|
|
this.callback.call(undefined, m);
|
|
}, Ci.nsIThread.DISPATCH_NORMAL);
|
|
|
|
if (!m.isScriptError && m.message === "SENTINEL")
|
|
Services.console.unregisterListener(this);
|
|
},
|
|
|
|
QueryInterface: XPCOMUtils.generateQI([Ci.nsIConsoleListener])
|
|
};
|
|
|
|
function wrapCallback(cb) {
|
|
return function SpecialPowersCallbackWrapper() {
|
|
var args = Array.prototype.map.call(arguments, wrapIfUnwrapped);
|
|
return cb.apply(this, args);
|
|
}
|
|
}
|
|
|
|
function wrapCallbackObject(obj) {
|
|
obj = Cu.waiveXrays(obj);
|
|
var wrapper = {};
|
|
for (var i in obj) {
|
|
if (typeof obj[i] == 'function')
|
|
wrapper[i] = wrapCallback(obj[i]);
|
|
else
|
|
wrapper[i] = obj[i];
|
|
}
|
|
return wrapper;
|
|
}
|
|
|
|
function setWrapped(obj, prop, val) {
|
|
if (!isWrapper(obj))
|
|
throw "You only need to use this for SpecialPowers wrapped objects";
|
|
|
|
obj = unwrapPrivileged(obj);
|
|
return Reflect.set(obj, prop, val);
|
|
}
|
|
|
|
SpecialPowersAPI.prototype = {
|
|
|
|
/*
|
|
* Privileged object wrapping API
|
|
*
|
|
* Usage:
|
|
* var wrapper = SpecialPowers.wrap(obj);
|
|
* wrapper.privilegedMethod(); wrapper.privilegedProperty;
|
|
* obj === SpecialPowers.unwrap(wrapper);
|
|
*
|
|
* These functions provide transparent access to privileged objects using
|
|
* various pieces of deep SpiderMagic. Conceptually, a wrapper is just an
|
|
* object containing a reference to the underlying object, where all method
|
|
* calls and property accesses are transparently performed with the System
|
|
* Principal. Moreover, objects obtained from the wrapper (including properties
|
|
* and method return values) are wrapped automatically. Thus, after a single
|
|
* call to SpecialPowers.wrap(), the wrapper layer is transitively maintained.
|
|
*
|
|
* Known Issues:
|
|
*
|
|
* - The wrapping function does not preserve identity, so
|
|
* SpecialPowers.wrap(foo) !== SpecialPowers.wrap(foo). See bug 718543.
|
|
*
|
|
* - The wrapper cannot see expando properties on unprivileged DOM objects.
|
|
* That is to say, the wrapper uses Xray delegation.
|
|
*
|
|
* - The wrapper sometimes guesses certain ES5 attributes for returned
|
|
* properties. This is explained in a comment in the wrapper code above,
|
|
* and shouldn't be a problem.
|
|
*/
|
|
wrap: wrapIfUnwrapped,
|
|
unwrap: unwrapIfWrapped,
|
|
isWrapper: isWrapper,
|
|
|
|
/*
|
|
* When content needs to pass a callback or a callback object to an API
|
|
* accessed over SpecialPowers, that API may sometimes receive arguments for
|
|
* whom it is forbidden to create a wrapper in content scopes. As such, we
|
|
* need a layer to wrap the values in SpecialPowers wrappers before they ever
|
|
* reach content.
|
|
*/
|
|
wrapCallback: wrapCallback,
|
|
wrapCallbackObject: wrapCallbackObject,
|
|
|
|
/*
|
|
* Used for assigning a property to a SpecialPowers wrapper, without unwrapping
|
|
* the value that is assigned.
|
|
*/
|
|
setWrapped: setWrapped,
|
|
|
|
/*
|
|
* Create blank privileged objects to use as out-params for privileged functions.
|
|
*/
|
|
createBlankObject: function () {
|
|
return new Object;
|
|
},
|
|
|
|
/*
|
|
* Because SpecialPowers wrappers don't preserve identity, comparing with ==
|
|
* can be hazardous. Sometimes we can just unwrap to compare, but sometimes
|
|
* wrapping the underlying object into a content scope is forbidden. This
|
|
* function strips any wrappers if they exist and compare the underlying
|
|
* values.
|
|
*/
|
|
compare: function(a, b) {
|
|
return unwrapIfWrapped(a) === unwrapIfWrapped(b);
|
|
},
|
|
|
|
get MockFilePicker() {
|
|
return MockFilePicker;
|
|
},
|
|
|
|
get MockColorPicker() {
|
|
return MockColorPicker;
|
|
},
|
|
|
|
get MockPermissionPrompt() {
|
|
return MockPermissionPrompt;
|
|
},
|
|
|
|
/*
|
|
* Load a privileged script that runs same-process. This is different from
|
|
* |loadChromeScript|, which will run in the parent process in e10s mode.
|
|
*/
|
|
loadPrivilegedScript: function (aFunction) {
|
|
var str = "(" + aFunction.toString() + ")();";
|
|
var systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
|
|
var sb = Cu.Sandbox(systemPrincipal);
|
|
var window = this.window.get();
|
|
var mc = new window.MessageChannel();
|
|
sb.port = mc.port1;
|
|
try {
|
|
sb.eval(str);
|
|
} catch (e) {
|
|
throw wrapIfUnwrapped(e);
|
|
}
|
|
|
|
return mc.port2;
|
|
},
|
|
|
|
loadChromeScript: function (urlOrFunction) {
|
|
// Create a unique id for this chrome script
|
|
let uuidGenerator = Cc["@mozilla.org/uuid-generator;1"]
|
|
.getService(Ci.nsIUUIDGenerator);
|
|
let id = uuidGenerator.generateUUID().toString();
|
|
|
|
// Tells chrome code to evaluate this chrome script
|
|
let scriptArgs = { id };
|
|
if (typeof(urlOrFunction) == "function") {
|
|
scriptArgs.function = {
|
|
body: "(" + urlOrFunction.toString() + ")();",
|
|
name: urlOrFunction.name,
|
|
};
|
|
} else {
|
|
scriptArgs.url = urlOrFunction;
|
|
}
|
|
this._sendSyncMessage("SPLoadChromeScript",
|
|
scriptArgs);
|
|
|
|
// Returns a MessageManager like API in order to be
|
|
// able to communicate with this chrome script
|
|
let listeners = [];
|
|
let chromeScript = {
|
|
addMessageListener: (name, listener) => {
|
|
listeners.push({ name: name, listener: listener });
|
|
},
|
|
|
|
promiseOneMessage: name => new Promise(resolve => {
|
|
chromeScript.addMessageListener(name, function listener(message) {
|
|
chromeScript.removeMessageListener(name, listener);
|
|
resolve(message);
|
|
});
|
|
}),
|
|
|
|
removeMessageListener: (name, listener) => {
|
|
listeners = listeners.filter(
|
|
o => (o.name != name || o.listener != listener)
|
|
);
|
|
},
|
|
|
|
sendAsyncMessage: (name, message) => {
|
|
this._sendSyncMessage("SPChromeScriptMessage",
|
|
{ id: id, name: name, message: message });
|
|
},
|
|
|
|
sendSyncMessage: (name, message) => {
|
|
return this._sendSyncMessage("SPChromeScriptMessage",
|
|
{ id, name, message });
|
|
},
|
|
|
|
destroy: () => {
|
|
listeners = [];
|
|
this._removeMessageListener("SPChromeScriptMessage", chromeScript);
|
|
this._removeMessageListener("SPChromeScriptAssert", chromeScript);
|
|
},
|
|
|
|
receiveMessage: (aMessage) => {
|
|
let messageId = aMessage.json.id;
|
|
let name = aMessage.json.name;
|
|
let message = aMessage.json.message;
|
|
// Ignore message from other chrome script
|
|
if (messageId != id)
|
|
return;
|
|
|
|
if (aMessage.name == "SPChromeScriptMessage") {
|
|
listeners.filter(o => (o.name == name))
|
|
.forEach(o => o.listener(message));
|
|
} else if (aMessage.name == "SPChromeScriptAssert") {
|
|
assert(aMessage.json);
|
|
}
|
|
}
|
|
};
|
|
this._addMessageListener("SPChromeScriptMessage", chromeScript);
|
|
this._addMessageListener("SPChromeScriptAssert", chromeScript);
|
|
|
|
let assert = json => {
|
|
// An assertion has been done in a mochitest chrome script
|
|
let {name, err, message, stack} = json;
|
|
|
|
// Try to fetch a test runner from the mochitest
|
|
// in order to properly log these assertions and notify
|
|
// all usefull log observers
|
|
let window = this.window.get();
|
|
let parentRunner, repr = o => o;
|
|
if (window) {
|
|
window = window.wrappedJSObject;
|
|
parentRunner = window.TestRunner;
|
|
if (window.repr) {
|
|
repr = window.repr;
|
|
}
|
|
}
|
|
|
|
// Craft a mochitest-like report string
|
|
var resultString = err ? "TEST-UNEXPECTED-FAIL" : "TEST-PASS";
|
|
var diagnostic =
|
|
message ? message :
|
|
("assertion @ " + stack.filename + ":" + stack.lineNumber);
|
|
if (err) {
|
|
diagnostic +=
|
|
" - got " + repr(err.actual) +
|
|
", expected " + repr(err.expected) +
|
|
" (operator " + err.operator + ")";
|
|
}
|
|
var msg = [resultString, name, diagnostic].join(" | ");
|
|
if (parentRunner) {
|
|
if (err) {
|
|
parentRunner.addFailedTest(name);
|
|
parentRunner.error(msg);
|
|
} else {
|
|
parentRunner.log(msg);
|
|
}
|
|
} else {
|
|
// When we are running only a single mochitest, there is no test runner
|
|
dump(msg + "\n");
|
|
}
|
|
};
|
|
|
|
return this.wrap(chromeScript);
|
|
},
|
|
|
|
importInMainProcess: function (importString) {
|
|
var message = this._sendSyncMessage("SPImportInMainProcess", importString)[0];
|
|
if (message.hadError) {
|
|
throw "SpecialPowers.importInMainProcess failed with error " + message.errorMessage;
|
|
}
|
|
return;
|
|
},
|
|
|
|
get Services() {
|
|
return wrapPrivileged(Services);
|
|
},
|
|
|
|
/*
|
|
* In general, any Components object created for unprivileged scopes is
|
|
* neutered (it implements nsIXPCComponentsBase, but not nsIXPCComponents).
|
|
* We override this in certain legacy automation configurations (see the
|
|
* implementation of getRawComponents() above), but don't want to support
|
|
* it in cases where it isn't already required.
|
|
*
|
|
* In scopes with neutered Components, we don't have a natural referent for
|
|
* things like SpecialPowers.Cc. So in those cases, we fall back to the
|
|
* Components object from the SpecialPowers scope. This doesn't quite behave
|
|
* the same way (in particular, SpecialPowers.Cc[foo].createInstance() will
|
|
* create an instance in the SpecialPowers scope), but SpecialPowers wrapping
|
|
* is already a YMMV / Whatever-It-Takes-To-Get-TBPL-Green sort of thing.
|
|
*
|
|
* It probably wouldn't be too much work to just make SpecialPowers.Components
|
|
* unconditionally point to the Components object in the SpecialPowers scope.
|
|
* Try will tell what needs to be fixed up.
|
|
*/
|
|
getFullComponents: function() {
|
|
if (this.Components && typeof this.Components.classes == 'object') {
|
|
return this.Components;
|
|
}
|
|
return Components;
|
|
},
|
|
|
|
/*
|
|
* Convenient shortcuts to the standard Components abbreviations. Note that
|
|
* we don't SpecialPowers-wrap Components.interfaces, because it's available
|
|
* to untrusted content, and wrapping it confuses QI and identity checks.
|
|
*/
|
|
get Cc() { return wrapPrivileged(this.getFullComponents().classes); },
|
|
get Ci() { return this.Components ? this.Components.interfaces
|
|
: Components.interfaces; },
|
|
get Cu() { return wrapPrivileged(this.getFullComponents().utils); },
|
|
get Cr() { return wrapPrivileged(this.Components.results); },
|
|
|
|
/*
|
|
* SpecialPowers.getRawComponents() allows content to get a reference to a
|
|
* naked (and, in certain automation configurations, privileged) Components
|
|
* object for its scope.
|
|
*
|
|
* SpecialPowers.getRawComponents(window) is defined as the global property
|
|
* window.SpecialPowers.Components for convenience.
|
|
*/
|
|
getRawComponents: getRawComponents,
|
|
|
|
getDOMWindowUtils: function(aWindow) {
|
|
if (aWindow == this.window.get() && this.DOMWindowUtils != null)
|
|
return this.DOMWindowUtils;
|
|
|
|
return bindDOMWindowUtils(aWindow);
|
|
},
|
|
|
|
removeExpectedCrashDumpFiles: function(aExpectingProcessCrash) {
|
|
var success = true;
|
|
if (aExpectingProcessCrash) {
|
|
var message = {
|
|
op: "delete-crash-dump-files",
|
|
filenames: this._encounteredCrashDumpFiles
|
|
};
|
|
if (!this._sendSyncMessage("SPProcessCrashService", message)[0]) {
|
|
success = false;
|
|
}
|
|
}
|
|
this._encounteredCrashDumpFiles.length = 0;
|
|
return success;
|
|
},
|
|
|
|
findUnexpectedCrashDumpFiles: function() {
|
|
var self = this;
|
|
var message = {
|
|
op: "find-crash-dump-files",
|
|
crashDumpFilesToIgnore: this._unexpectedCrashDumpFiles
|
|
};
|
|
var crashDumpFiles = this._sendSyncMessage("SPProcessCrashService", message)[0];
|
|
crashDumpFiles.forEach(function(aFilename) {
|
|
self._unexpectedCrashDumpFiles[aFilename] = true;
|
|
});
|
|
return crashDumpFiles;
|
|
},
|
|
|
|
_setTimeout: function(callback) {
|
|
// for mochitest-browser
|
|
if (typeof window != 'undefined')
|
|
setTimeout(callback, 0);
|
|
// for mochitest-plain
|
|
else
|
|
content.window.setTimeout(callback, 0);
|
|
},
|
|
|
|
_delayCallbackTwice: function(callback) {
|
|
function delayedCallback() {
|
|
function delayAgain(aCallback) {
|
|
// Using this._setTimeout doesn't work here
|
|
// It causes failures in mochtests that use
|
|
// multiple pushPrefEnv calls
|
|
// For chrome/browser-chrome mochitests
|
|
if (typeof window != 'undefined')
|
|
setTimeout(aCallback, 0);
|
|
// For mochitest-plain
|
|
else
|
|
content.window.setTimeout(aCallback, 0);
|
|
}
|
|
delayAgain(delayAgain(callback));
|
|
}
|
|
return delayedCallback;
|
|
},
|
|
|
|
/* apply permissions to the system and when the test case is finished (SimpleTest.finish())
|
|
we will revert the permission back to the original.
|
|
|
|
inPermissions is an array of objects where each object has a type, action, context, ex:
|
|
[{'type': 'SystemXHR', 'allow': 1, 'context': document},
|
|
{'type': 'SystemXHR', 'allow': Ci.nsIPermissionManager.PROMPT_ACTION, 'context': document}]
|
|
|
|
Allow can be a boolean value of true/false or ALLOW_ACTION/DENY_ACTION/PROMPT_ACTION/UNKNOWN_ACTION
|
|
*/
|
|
pushPermissions: function(inPermissions, callback) {
|
|
inPermissions = Cu.waiveXrays(inPermissions);
|
|
var pendingPermissions = [];
|
|
var cleanupPermissions = [];
|
|
|
|
for (var p in inPermissions) {
|
|
var permission = inPermissions[p];
|
|
var originalValue = Ci.nsIPermissionManager.UNKNOWN_ACTION;
|
|
var context = Cu.unwaiveXrays(permission.context); // Sometimes |context| is a DOM object on which we expect
|
|
// to be able to access .nodePrincipal, so we need to unwaive.
|
|
if (this.testPermission(permission.type, Ci.nsIPermissionManager.ALLOW_ACTION, context)) {
|
|
originalValue = Ci.nsIPermissionManager.ALLOW_ACTION;
|
|
} else if (this.testPermission(permission.type, Ci.nsIPermissionManager.DENY_ACTION, context)) {
|
|
originalValue = Ci.nsIPermissionManager.DENY_ACTION;
|
|
} else if (this.testPermission(permission.type, Ci.nsIPermissionManager.PROMPT_ACTION, context)) {
|
|
originalValue = Ci.nsIPermissionManager.PROMPT_ACTION;
|
|
} else if (this.testPermission(permission.type, Ci.nsICookiePermission.ACCESS_SESSION, context)) {
|
|
originalValue = Ci.nsICookiePermission.ACCESS_SESSION;
|
|
} else if (this.testPermission(permission.type, Ci.nsICookiePermission.ACCESS_ALLOW_FIRST_PARTY_ONLY, context)) {
|
|
originalValue = Ci.nsICookiePermission.ACCESS_ALLOW_FIRST_PARTY_ONLY;
|
|
} else if (this.testPermission(permission.type, Ci.nsICookiePermission.ACCESS_LIMIT_THIRD_PARTY, context)) {
|
|
originalValue = Ci.nsICookiePermission.ACCESS_LIMIT_THIRD_PARTY;
|
|
}
|
|
|
|
let principal = this._getPrincipalFromArg(context);
|
|
if (principal.isSystemPrincipal) {
|
|
continue;
|
|
}
|
|
|
|
let perm;
|
|
if (typeof permission.allow !== 'boolean') {
|
|
perm = permission.allow;
|
|
} else {
|
|
perm = permission.allow ? Ci.nsIPermissionManager.ALLOW_ACTION
|
|
: Ci.nsIPermissionManager.DENY_ACTION;
|
|
}
|
|
|
|
if (permission.remove == true)
|
|
perm = Ci.nsIPermissionManager.UNKNOWN_ACTION;
|
|
|
|
if (originalValue == perm) {
|
|
continue;
|
|
}
|
|
|
|
var todo = {'op': 'add',
|
|
'type': permission.type,
|
|
'permission': perm,
|
|
'value': perm,
|
|
'principal': principal,
|
|
'expireType': (typeof permission.expireType === "number") ?
|
|
permission.expireType : 0, // default: EXPIRE_NEVER
|
|
'expireTime': (typeof permission.expireTime === "number") ?
|
|
permission.expireTime : 0};
|
|
|
|
var cleanupTodo = Object.assign({}, todo);
|
|
|
|
if (permission.remove == true)
|
|
todo.op = 'remove';
|
|
|
|
pendingPermissions.push(todo);
|
|
|
|
/* Push original permissions value or clear into cleanup array */
|
|
if (originalValue == Ci.nsIPermissionManager.UNKNOWN_ACTION) {
|
|
cleanupTodo.op = 'remove';
|
|
} else {
|
|
cleanupTodo.value = originalValue;
|
|
cleanupTodo.permission = originalValue;
|
|
}
|
|
cleanupPermissions.push(cleanupTodo);
|
|
}
|
|
|
|
if (pendingPermissions.length > 0) {
|
|
// The callback needs to be delayed twice. One delay is because the pref
|
|
// service doesn't guarantee the order it calls its observers in, so it
|
|
// may notify the observer holding the callback before the other
|
|
// observers have been notified and given a chance to make the changes
|
|
// that the callback checks for. The second delay is because pref
|
|
// observers often defer making their changes by posting an event to the
|
|
// event loop.
|
|
if (!this._observingPermissions) {
|
|
this._observingPermissions = true;
|
|
// If specialpowers is in main-process, then we can add a observer
|
|
// to get all 'perm-changed' signals. Otherwise, it can't receive
|
|
// all signals, so we register a observer in specialpowersobserver(in
|
|
// main-process) and get signals from it.
|
|
if (this.isMainProcess()) {
|
|
this.permissionObserverProxy._specialPowersAPI = this;
|
|
Services.obs.addObserver(this.permissionObserverProxy, "perm-changed", false);
|
|
} else {
|
|
this.registerObservers("perm-changed");
|
|
// bind() is used to set 'this' to SpecialPowersAPI itself.
|
|
this._addMessageListener("specialpowers-perm-changed", this.permChangedProxy.bind(this));
|
|
}
|
|
}
|
|
this._permissionsUndoStack.push(cleanupPermissions);
|
|
this._pendingPermissions.push([pendingPermissions,
|
|
this._delayCallbackTwice(callback)]);
|
|
this._applyPermissions();
|
|
} else {
|
|
this._setTimeout(callback);
|
|
}
|
|
},
|
|
|
|
/*
|
|
* This function should be used when specialpowers is in content process but
|
|
* it want to get the notification from chrome space.
|
|
*
|
|
* This function will call Services.obs.addObserver in SpecialPowersObserver
|
|
* (that is in chrome process) and forward the data received to SpecialPowers
|
|
* via messageManager.
|
|
* You can use this._addMessageListener("specialpowers-YOUR_TOPIC") to fire
|
|
* the callback.
|
|
*
|
|
* To get the expected data, you should modify
|
|
* SpecialPowersObserver.prototype._registerObservers.observe. Or the message
|
|
* you received from messageManager will only contain 'aData' from Service.obs.
|
|
*
|
|
* NOTICE: there is no implementation of _addMessageListener in
|
|
* ChromePowers.js
|
|
*/
|
|
registerObservers: function(topic) {
|
|
var msg = {
|
|
'op': 'add',
|
|
'observerTopic': topic,
|
|
};
|
|
this._sendSyncMessage("SPObserverService", msg);
|
|
},
|
|
|
|
permChangedProxy: function(aMessage) {
|
|
let permission = aMessage.json.permission;
|
|
let aData = aMessage.json.aData;
|
|
this._permissionObserver.observe(permission, aData);
|
|
},
|
|
|
|
permissionObserverProxy: {
|
|
// 'this' in permChangedObserverProxy is the permChangedObserverProxy
|
|
// object itself. The '_specialPowersAPI' will be set to the 'SpecialPowersAPI'
|
|
// object to call the member function in SpecialPowersAPI.
|
|
_specialPowersAPI: null,
|
|
observe: function (aSubject, aTopic, aData)
|
|
{
|
|
if (aTopic == "perm-changed") {
|
|
var permission = aSubject.QueryInterface(Ci.nsIPermission);
|
|
this._specialPowersAPI._permissionObserver.observe(permission, aData);
|
|
}
|
|
}
|
|
},
|
|
|
|
popPermissions: function(callback) {
|
|
if (this._permissionsUndoStack.length > 0) {
|
|
// See pushPermissions comment regarding delay.
|
|
let cb = callback ? this._delayCallbackTwice(callback) : null;
|
|
/* Each pop from the stack will yield an object {op/type/permission/value/url/appid/isInIsolatedMozBrowserElement} or null */
|
|
this._pendingPermissions.push([this._permissionsUndoStack.pop(), cb]);
|
|
this._applyPermissions();
|
|
} else {
|
|
if (this._observingPermissions) {
|
|
this._observingPermissions = false;
|
|
this._removeMessageListener("specialpowers-perm-changed", this.permChangedProxy.bind(this));
|
|
}
|
|
this._setTimeout(callback);
|
|
}
|
|
},
|
|
|
|
flushPermissions: function(callback) {
|
|
while (this._permissionsUndoStack.length > 1)
|
|
this.popPermissions(null);
|
|
|
|
this.popPermissions(callback);
|
|
},
|
|
|
|
|
|
setTestPluginEnabledState: function(newEnabledState, pluginName) {
|
|
return this._sendSyncMessage("SPSetTestPluginEnabledState",
|
|
{ newEnabledState: newEnabledState, pluginName: pluginName })[0];
|
|
},
|
|
|
|
|
|
_permissionObserver: {
|
|
_self: null,
|
|
_lastPermission: {},
|
|
_callBack: null,
|
|
_nextCallback: null,
|
|
_obsDataMap: {
|
|
'deleted':'remove',
|
|
'added':'add'
|
|
},
|
|
observe: function (permission, aData)
|
|
{
|
|
if (this._self._applyingPermissions) {
|
|
if (permission.type == this._lastPermission.type) {
|
|
this._self._setTimeout(this._callback);
|
|
this._self._setTimeout(this._nextCallback);
|
|
this._callback = null;
|
|
this._nextCallback = null;
|
|
}
|
|
} else {
|
|
var found = false;
|
|
for (var i = 0; !found && i < this._self._permissionsUndoStack.length; i++) {
|
|
var undos = this._self._permissionsUndoStack[i];
|
|
for (var j = 0; j < undos.length; j++) {
|
|
var undo = undos[j];
|
|
if (undo.op == this._obsDataMap[aData] &&
|
|
undo.principal.originAttributes.appId == permission.principal.originAttributes.appId &&
|
|
undo.type == permission.type) {
|
|
// Remove this undo item if it has been done by others(not
|
|
// specialpowers itself.)
|
|
undos.splice(j,1);
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!undos.length) {
|
|
// Remove the empty row in permissionsUndoStack
|
|
this._self._permissionsUndoStack.splice(i, 1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
/*
|
|
Iterate through one atomic set of permissions actions and perform allow/deny as appropriate.
|
|
All actions performed must modify the relevant permission.
|
|
*/
|
|
_applyPermissions: function() {
|
|
if (this._applyingPermissions || this._pendingPermissions.length <= 0) {
|
|
return;
|
|
}
|
|
|
|
/* Set lock and get prefs from the _pendingPrefs queue */
|
|
this._applyingPermissions = true;
|
|
var transaction = this._pendingPermissions.shift();
|
|
var pendingActions = transaction[0];
|
|
var callback = transaction[1];
|
|
var lastPermission = pendingActions[pendingActions.length-1];
|
|
|
|
var self = this;
|
|
this._permissionObserver._self = self;
|
|
this._permissionObserver._lastPermission = lastPermission;
|
|
this._permissionObserver._callback = callback;
|
|
this._permissionObserver._nextCallback = function () {
|
|
self._applyingPermissions = false;
|
|
// Now apply any permissions that may have been queued while we were applying
|
|
self._applyPermissions();
|
|
}
|
|
|
|
for (var idx in pendingActions) {
|
|
var perm = pendingActions[idx];
|
|
this._sendSyncMessage('SPPermissionManager', perm)[0];
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Helper to resolve a promise by calling the resolve function and call an
|
|
* optional callback.
|
|
*/
|
|
_resolveAndCallOptionalCallback(resolveFn, callback = null) {
|
|
resolveFn();
|
|
|
|
if (callback) {
|
|
callback();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Take in a list of pref changes to make, then invokes |callback| and resolves
|
|
* the returned Promise once those changes have taken effect. When the test
|
|
* finishes, these changes are reverted.
|
|
*
|
|
* |inPrefs| must be an object with up to two properties: "set" and "clear".
|
|
* pushPrefEnv will set prefs as indicated in |inPrefs.set| and will unset
|
|
* the prefs indicated in |inPrefs.clear|.
|
|
*
|
|
* For example, you might pass |inPrefs| as:
|
|
*
|
|
* inPrefs = {'set': [['foo.bar', 2], ['magic.pref', 'baz']],
|
|
* 'clear': [['clear.this'], ['also.this']] };
|
|
*
|
|
* Notice that |set| and |clear| are both an array of arrays. In |set|, each
|
|
* of the inner arrays must have the form [pref_name, value] or [pref_name,
|
|
* value, iid]. (The latter form is used for prefs with "complex" values.)
|
|
*
|
|
* In |clear|, each inner array should have the form [pref_name].
|
|
*
|
|
* If you set the same pref more than once (or both set and clear a pref),
|
|
* the behavior of this method is undefined.
|
|
*
|
|
* (Implementation note: _prefEnvUndoStack is a stack of values to revert to,
|
|
* not values which have been set!)
|
|
*
|
|
* TODO: complex values for original cleanup?
|
|
*
|
|
*/
|
|
pushPrefEnv: function(inPrefs, callback = null) {
|
|
var prefs = Services.prefs;
|
|
|
|
var pref_string = [];
|
|
pref_string[prefs.PREF_INT] = "INT";
|
|
pref_string[prefs.PREF_BOOL] = "BOOL";
|
|
pref_string[prefs.PREF_STRING] = "CHAR";
|
|
|
|
var pendingActions = [];
|
|
var cleanupActions = [];
|
|
|
|
for (var action in inPrefs) { /* set|clear */
|
|
for (var idx in inPrefs[action]) {
|
|
var aPref = inPrefs[action][idx];
|
|
var prefName = aPref[0];
|
|
var prefValue = null;
|
|
var prefIid = null;
|
|
var prefType = prefs.PREF_INVALID;
|
|
var originalValue = null;
|
|
|
|
if (aPref.length == 3) {
|
|
prefValue = aPref[1];
|
|
prefIid = aPref[2];
|
|
} else if (aPref.length == 2) {
|
|
prefValue = aPref[1];
|
|
}
|
|
|
|
/* If pref is not found or invalid it doesn't exist. */
|
|
if (prefs.getPrefType(prefName) != prefs.PREF_INVALID) {
|
|
prefType = pref_string[prefs.getPrefType(prefName)];
|
|
if ((prefs.prefHasUserValue(prefName) && action == 'clear') ||
|
|
(action == 'set'))
|
|
originalValue = this._getPref(prefName, prefType);
|
|
} else if (action == 'set') {
|
|
/* prefName doesn't exist, so 'clear' is pointless */
|
|
if (aPref.length == 3) {
|
|
prefType = "COMPLEX";
|
|
} else if (aPref.length == 2) {
|
|
if (typeof(prefValue) == "boolean")
|
|
prefType = "BOOL";
|
|
else if (typeof(prefValue) == "number")
|
|
prefType = "INT";
|
|
else if (typeof(prefValue) == "string")
|
|
prefType = "CHAR";
|
|
}
|
|
}
|
|
|
|
/* PREF_INVALID: A non existing pref which we are clearing or invalid values for a set */
|
|
if (prefType == prefs.PREF_INVALID)
|
|
continue;
|
|
|
|
/* We are not going to set a pref if the value is the same */
|
|
if (originalValue == prefValue)
|
|
continue;
|
|
|
|
pendingActions.push({'action': action, 'type': prefType, 'name': prefName, 'value': prefValue, 'Iid': prefIid});
|
|
|
|
/* Push original preference value or clear into cleanup array */
|
|
var cleanupTodo = {'action': action, 'type': prefType, 'name': prefName, 'value': originalValue, 'Iid': prefIid};
|
|
if (originalValue == null) {
|
|
cleanupTodo.action = 'clear';
|
|
} else {
|
|
cleanupTodo.action = 'set';
|
|
}
|
|
cleanupActions.push(cleanupTodo);
|
|
}
|
|
}
|
|
|
|
return new Promise(resolve => {
|
|
let done = this._resolveAndCallOptionalCallback.bind(this, resolve, callback);
|
|
if (pendingActions.length > 0) {
|
|
// The callback needs to be delayed twice. One delay is because the pref
|
|
// service doesn't guarantee the order it calls its observers in, so it
|
|
// may notify the observer holding the callback before the other
|
|
// observers have been notified and given a chance to make the changes
|
|
// that the callback checks for. The second delay is because pref
|
|
// observers often defer making their changes by posting an event to the
|
|
// event loop.
|
|
this._prefEnvUndoStack.push(cleanupActions);
|
|
this._pendingPrefs.push([pendingActions,
|
|
this._delayCallbackTwice(done)]);
|
|
this._applyPrefs();
|
|
} else {
|
|
this._setTimeout(done);
|
|
}
|
|
});
|
|
},
|
|
|
|
popPrefEnv: function(callback = null) {
|
|
return new Promise(resolve => {
|
|
let done = this._resolveAndCallOptionalCallback.bind(this, resolve, callback);
|
|
if (this._prefEnvUndoStack.length > 0) {
|
|
// See pushPrefEnv comment regarding delay.
|
|
let cb = this._delayCallbackTwice(done);
|
|
/* Each pop will have a valid block of preferences */
|
|
this._pendingPrefs.push([this._prefEnvUndoStack.pop(), cb]);
|
|
this._applyPrefs();
|
|
} else {
|
|
this._setTimeout(done);
|
|
}
|
|
});
|
|
},
|
|
|
|
flushPrefEnv: function(callback = null) {
|
|
while (this._prefEnvUndoStack.length > 1)
|
|
this.popPrefEnv(null);
|
|
|
|
return new Promise(resolve => {
|
|
let done = this._resolveAndCallOptionalCallback.bind(this, resolve, callback);
|
|
this.popPrefEnv(done);
|
|
});
|
|
},
|
|
|
|
/*
|
|
Iterate through one atomic set of pref actions and perform sets/clears as appropriate.
|
|
All actions performed must modify the relevant pref.
|
|
*/
|
|
_applyPrefs: function() {
|
|
if (this._applyingPrefs || this._pendingPrefs.length <= 0) {
|
|
return;
|
|
}
|
|
|
|
/* Set lock and get prefs from the _pendingPrefs queue */
|
|
this._applyingPrefs = true;
|
|
var transaction = this._pendingPrefs.shift();
|
|
var pendingActions = transaction[0];
|
|
var callback = transaction[1];
|
|
|
|
var lastPref = pendingActions[pendingActions.length-1];
|
|
|
|
var pb = Services.prefs;
|
|
var self = this;
|
|
pb.addObserver(lastPref.name, function prefObs(subject, topic, data) {
|
|
pb.removeObserver(lastPref.name, prefObs);
|
|
|
|
self._setTimeout(callback);
|
|
self._setTimeout(function () {
|
|
self._applyingPrefs = false;
|
|
// Now apply any prefs that may have been queued while we were applying
|
|
self._applyPrefs();
|
|
});
|
|
}, false);
|
|
|
|
for (var idx in pendingActions) {
|
|
var pref = pendingActions[idx];
|
|
if (pref.action == 'set') {
|
|
this._setPref(pref.name, pref.type, pref.value, pref.Iid);
|
|
} else if (pref.action == 'clear') {
|
|
this.clearUserPref(pref.name);
|
|
}
|
|
}
|
|
},
|
|
|
|
_proxiedObservers: {
|
|
"specialpowers-http-notify-request": function(aMessage) {
|
|
let uri = aMessage.json.uri;
|
|
Services.obs.notifyObservers(null, "specialpowers-http-notify-request", uri);
|
|
},
|
|
"specialpowers-browser-fullZoom:zoomReset": function() {
|
|
Services.obs.notifyObservers(null, "specialpowers-browser-fullZoom:zoomReset", null);
|
|
},
|
|
},
|
|
|
|
_addObserverProxy: function(notification) {
|
|
if (notification in this._proxiedObservers) {
|
|
this._addMessageListener(notification, this._proxiedObservers[notification]);
|
|
}
|
|
},
|
|
_removeObserverProxy: function(notification) {
|
|
if (notification in this._proxiedObservers) {
|
|
this._removeMessageListener(notification, this._proxiedObservers[notification]);
|
|
}
|
|
},
|
|
|
|
addObserver: function(obs, notification, weak) {
|
|
this._addObserverProxy(notification);
|
|
obs = Cu.waiveXrays(obs);
|
|
if (typeof obs == 'object' && obs.observe.name != 'SpecialPowersCallbackWrapper')
|
|
obs.observe = wrapCallback(obs.observe);
|
|
Services.obs.addObserver(obs, notification, weak);
|
|
},
|
|
removeObserver: function(obs, notification) {
|
|
this._removeObserverProxy(notification);
|
|
Services.obs.removeObserver(Cu.waiveXrays(obs), notification);
|
|
},
|
|
notifyObservers: function(subject, topic, data) {
|
|
Services.obs.notifyObservers(subject, topic, data);
|
|
},
|
|
|
|
/**
|
|
* An async observer is useful if you're listening for a
|
|
* notification that normally is only used by C++ code or chrome
|
|
* code (so it runs in the SystemGroup), but we need to know about
|
|
* it for a test (which runs as web content). If we used
|
|
* addObserver, we would assert when trying to enter web content
|
|
* from a runnabled labeled by the SystemGroup. An async observer
|
|
* avoids this problem.
|
|
*/
|
|
_asyncObservers: new WeakMap(),
|
|
addAsyncObserver: function(obs, notification, weak) {
|
|
obs = Cu.waiveXrays(obs);
|
|
if (typeof obs == 'object' && obs.observe.name != 'SpecialPowersCallbackWrapper') {
|
|
obs.observe = wrapCallback(obs.observe);
|
|
}
|
|
let asyncObs = (...args) => {
|
|
Services.tm.mainThread.dispatch(() => {
|
|
if (typeof obs == 'function') {
|
|
obs.call(undefined, ...args);
|
|
} else {
|
|
obs.observe.call(undefined, ...args);
|
|
}
|
|
}, Ci.nsIThread.DISPATCH_NORMAL);
|
|
};
|
|
this._asyncObservers.set(obs, asyncObs);
|
|
Services.obs.addObserver(asyncObs, notification, weak);
|
|
},
|
|
removeAsyncObserver: function(obs, notification) {
|
|
let asyncObs = this._asyncObservers.get(Cu.waiveXrays(obs));
|
|
Services.obs.removeObserver(asyncObs, notification);
|
|
},
|
|
|
|
can_QI: function(obj) {
|
|
return obj.QueryInterface !== undefined;
|
|
},
|
|
do_QueryInterface: function(obj, iface) {
|
|
return obj.QueryInterface(Ci[iface]);
|
|
},
|
|
|
|
call_Instanceof: function (obj1, obj2) {
|
|
obj1=unwrapIfWrapped(obj1);
|
|
obj2=unwrapIfWrapped(obj2);
|
|
return obj1 instanceof obj2;
|
|
},
|
|
|
|
// Returns a privileged getter from an object. GetOwnPropertyDescriptor does
|
|
// not work here because xray wrappers don't properly implement it.
|
|
//
|
|
// This terribleness is used by dom/base/test/test_object.html because
|
|
// <object> and <embed> tags will spawn plugins if their prototype is touched,
|
|
// so we need to get and cache the getter of |hasRunningPlugin| if we want to
|
|
// call it without paradoxically spawning the plugin.
|
|
do_lookupGetter: function(obj, name) {
|
|
return Object.prototype.__lookupGetter__.call(obj, name);
|
|
},
|
|
|
|
// Mimic the get*Pref API
|
|
getBoolPref: function(aPrefName) {
|
|
return (this._getPref(aPrefName, 'BOOL'));
|
|
},
|
|
getIntPref: function(aPrefName) {
|
|
return (this._getPref(aPrefName, 'INT'));
|
|
},
|
|
getCharPref: function(aPrefName) {
|
|
return (this._getPref(aPrefName, 'CHAR'));
|
|
},
|
|
getComplexValue: function(aPrefName, aIid) {
|
|
return (this._getPref(aPrefName, 'COMPLEX', aIid));
|
|
},
|
|
|
|
// Mimic the set*Pref API
|
|
setBoolPref: function(aPrefName, aValue) {
|
|
return (this._setPref(aPrefName, 'BOOL', aValue));
|
|
},
|
|
setIntPref: function(aPrefName, aValue) {
|
|
return (this._setPref(aPrefName, 'INT', aValue));
|
|
},
|
|
setCharPref: function(aPrefName, aValue) {
|
|
return (this._setPref(aPrefName, 'CHAR', aValue));
|
|
},
|
|
setComplexValue: function(aPrefName, aIid, aValue) {
|
|
return (this._setPref(aPrefName, 'COMPLEX', aValue, aIid));
|
|
},
|
|
|
|
// Mimic the clearUserPref API
|
|
clearUserPref: function(aPrefName) {
|
|
var msg = {'op':'clear', 'prefName': aPrefName, 'prefType': ""};
|
|
this._sendSyncMessage('SPPrefService', msg);
|
|
},
|
|
|
|
// Private pref functions to communicate to chrome
|
|
_getPref: function(aPrefName, aPrefType, aIid) {
|
|
var msg = {};
|
|
if (aIid) {
|
|
// Overloading prefValue to handle complex prefs
|
|
msg = {'op':'get', 'prefName': aPrefName, 'prefType':aPrefType, 'prefValue':[aIid]};
|
|
} else {
|
|
msg = {'op':'get', 'prefName': aPrefName,'prefType': aPrefType};
|
|
}
|
|
var val = this._sendSyncMessage('SPPrefService', msg);
|
|
|
|
if (val == null || val[0] == null)
|
|
throw "Error getting pref '" + aPrefName + "'";
|
|
return val[0];
|
|
},
|
|
_setPref: function(aPrefName, aPrefType, aValue, aIid) {
|
|
var msg = {};
|
|
if (aIid) {
|
|
msg = {'op':'set','prefName':aPrefName, 'prefType': aPrefType, 'prefValue': [aIid,aValue]};
|
|
} else {
|
|
msg = {'op':'set', 'prefName': aPrefName, 'prefType': aPrefType, 'prefValue': aValue};
|
|
}
|
|
return(this._sendSyncMessage('SPPrefService', msg)[0]);
|
|
},
|
|
|
|
_getDocShell: function(window) {
|
|
return window.QueryInterface(Ci.nsIInterfaceRequestor)
|
|
.getInterface(Ci.nsIWebNavigation)
|
|
.QueryInterface(Ci.nsIDocShell);
|
|
},
|
|
_getMUDV: function(window) {
|
|
return this._getDocShell(window).contentViewer;
|
|
},
|
|
//XXX: these APIs really ought to be removed, they're not e10s-safe.
|
|
// (also they're pretty Firefox-specific)
|
|
_getTopChromeWindow: function(window) {
|
|
return window.QueryInterface(Ci.nsIInterfaceRequestor)
|
|
.getInterface(Ci.nsIWebNavigation)
|
|
.QueryInterface(Ci.nsIDocShellTreeItem)
|
|
.rootTreeItem
|
|
.QueryInterface(Ci.nsIInterfaceRequestor)
|
|
.getInterface(Ci.nsIDOMWindow)
|
|
.QueryInterface(Ci.nsIDOMChromeWindow);
|
|
},
|
|
_getAutoCompletePopup: function(window) {
|
|
return this._getTopChromeWindow(window).document
|
|
.getElementById("PopupAutoComplete");
|
|
},
|
|
addAutoCompletePopupEventListener: function(window, eventname, listener) {
|
|
this._getAutoCompletePopup(window).addEventListener(eventname,
|
|
listener);
|
|
},
|
|
removeAutoCompletePopupEventListener: function(window, eventname, listener) {
|
|
this._getAutoCompletePopup(window).removeEventListener(eventname,
|
|
listener);
|
|
},
|
|
get formHistory() {
|
|
let tmp = {};
|
|
Cu.import("resource://gre/modules/FormHistory.jsm", tmp);
|
|
return wrapPrivileged(tmp.FormHistory);
|
|
},
|
|
getFormFillController: function(window) {
|
|
return Components.classes["@mozilla.org/satchel/form-fill-controller;1"]
|
|
.getService(Components.interfaces.nsIFormFillController);
|
|
},
|
|
attachFormFillControllerTo: function(window) {
|
|
this.getFormFillController()
|
|
.attachToBrowser(this._getDocShell(window),
|
|
this._getAutoCompletePopup(window));
|
|
},
|
|
detachFormFillControllerFrom: function(window) {
|
|
this.getFormFillController().detachFromBrowser(this._getDocShell(window));
|
|
},
|
|
isBackButtonEnabled: function(window) {
|
|
return !this._getTopChromeWindow(window).document
|
|
.getElementById("Browser:Back")
|
|
.hasAttribute("disabled");
|
|
},
|
|
//XXX end of problematic APIs
|
|
|
|
addChromeEventListener: function(type, listener, capture, allowUntrusted) {
|
|
addEventListener(type, listener, capture, allowUntrusted);
|
|
},
|
|
removeChromeEventListener: function(type, listener, capture) {
|
|
removeEventListener(type, listener, capture);
|
|
},
|
|
|
|
// Note: each call to registerConsoleListener MUST be paired with a
|
|
// call to postConsoleSentinel; when the callback receives the
|
|
// sentinel it will unregister itself (_after_ calling the
|
|
// callback). SimpleTest.expectConsoleMessages does this for you.
|
|
// If you register more than one console listener, a call to
|
|
// postConsoleSentinel will zap all of them.
|
|
registerConsoleListener: function(callback) {
|
|
let listener = new SPConsoleListener(callback);
|
|
Services.console.registerListener(listener);
|
|
},
|
|
postConsoleSentinel: function() {
|
|
Services.console.logStringMessage("SENTINEL");
|
|
},
|
|
resetConsole: function() {
|
|
Services.console.reset();
|
|
},
|
|
|
|
getFullZoom: function(window) {
|
|
return this._getMUDV(window).fullZoom;
|
|
},
|
|
setFullZoom: function(window, zoom) {
|
|
this._getMUDV(window).fullZoom = zoom;
|
|
},
|
|
getTextZoom: function(window) {
|
|
return this._getMUDV(window).textZoom;
|
|
},
|
|
setTextZoom: function(window, zoom) {
|
|
this._getMUDV(window).textZoom = zoom;
|
|
},
|
|
|
|
getOverrideDPPX: function(window) {
|
|
return this._getMUDV(window).overrideDPPX;
|
|
},
|
|
setOverrideDPPX: function(window, dppx) {
|
|
this._getMUDV(window).overrideDPPX = dppx;
|
|
},
|
|
|
|
emulateMedium: function(window, mediaType) {
|
|
this._getMUDV(window).emulateMedium(mediaType);
|
|
},
|
|
stopEmulatingMedium: function(window) {
|
|
this._getMUDV(window).stopEmulatingMedium();
|
|
},
|
|
|
|
snapshotWindowWithOptions: function (win, rect, bgcolor, options) {
|
|
var el = this.window.get().document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
|
|
if (rect === undefined) {
|
|
rect = { top: win.scrollY, left: win.scrollX,
|
|
width: win.innerWidth, height: win.innerHeight };
|
|
}
|
|
if (bgcolor === undefined) {
|
|
bgcolor = "rgb(255,255,255)";
|
|
}
|
|
if (options === undefined) {
|
|
options = { };
|
|
}
|
|
|
|
el.width = rect.width;
|
|
el.height = rect.height;
|
|
var ctx = el.getContext("2d");
|
|
var flags = 0;
|
|
|
|
for (var option in options) {
|
|
flags |= options[option] && ctx[option];
|
|
}
|
|
|
|
ctx.drawWindow(win,
|
|
rect.left, rect.top, rect.width, rect.height,
|
|
bgcolor,
|
|
flags);
|
|
return el;
|
|
},
|
|
|
|
snapshotWindow: function (win, withCaret, rect, bgcolor) {
|
|
return this.snapshotWindowWithOptions(win, rect, bgcolor,
|
|
{ DRAWWINDOW_DRAW_CARET: withCaret });
|
|
},
|
|
|
|
snapshotRect: function (win, rect, bgcolor) {
|
|
return this.snapshotWindowWithOptions(win, rect, bgcolor);
|
|
},
|
|
|
|
gc: function() {
|
|
this.DOMWindowUtils.garbageCollect();
|
|
},
|
|
|
|
forceGC: function() {
|
|
Cu.forceGC();
|
|
},
|
|
|
|
forceCC: function() {
|
|
Cu.forceCC();
|
|
},
|
|
|
|
finishCC: function() {
|
|
Cu.finishCC();
|
|
},
|
|
|
|
ccSlice: function(budget) {
|
|
Cu.ccSlice(budget);
|
|
},
|
|
|
|
// Due to various dependencies between JS objects and C++ objects, an ordinary
|
|
// forceGC doesn't necessarily clear all unused objects, thus the GC and CC
|
|
// needs to run several times and when no other JS is running.
|
|
// The current number of iterations has been determined according to massive
|
|
// cross platform testing.
|
|
exactGC: function(callback) {
|
|
let count = 0;
|
|
|
|
function genGCCallback(cb) {
|
|
return function() {
|
|
Cu.forceCC();
|
|
if (++count < 2) {
|
|
Cu.schedulePreciseGC(genGCCallback(cb));
|
|
} else if (cb) {
|
|
cb();
|
|
}
|
|
}
|
|
}
|
|
|
|
Cu.schedulePreciseGC(genGCCallback(callback));
|
|
},
|
|
|
|
setGCZeal: function(zeal) {
|
|
Cu.setGCZeal(zeal);
|
|
},
|
|
|
|
isMainProcess: function() {
|
|
try {
|
|
return Cc["@mozilla.org/xre/app-info;1"].
|
|
getService(Ci.nsIXULRuntime).
|
|
processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
|
|
} catch (e) { }
|
|
return true;
|
|
},
|
|
|
|
_xpcomabi: null,
|
|
|
|
get XPCOMABI() {
|
|
if (this._xpcomabi != null)
|
|
return this._xpcomabi;
|
|
|
|
var xulRuntime = Cc["@mozilla.org/xre/app-info;1"]
|
|
.getService(Components.interfaces.nsIXULAppInfo)
|
|
.QueryInterface(Components.interfaces.nsIXULRuntime);
|
|
|
|
this._xpcomabi = xulRuntime.XPCOMABI;
|
|
return this._xpcomabi;
|
|
},
|
|
|
|
// The optional aWin parameter allows the caller to specify a given window in
|
|
// whose scope the runnable should be dispatched. If aFun throws, the
|
|
// exception will be reported to aWin.
|
|
executeSoon: function(aFun, aWin) {
|
|
// Create the runnable in the scope of aWin to avoid running into COWs.
|
|
var runnable = {};
|
|
if (aWin)
|
|
runnable = Cu.createObjectIn(aWin);
|
|
runnable.run = aFun;
|
|
Cu.dispatch(runnable, aWin);
|
|
},
|
|
|
|
_os: null,
|
|
|
|
get OS() {
|
|
if (this._os != null)
|
|
return this._os;
|
|
|
|
var xulRuntime = Cc["@mozilla.org/xre/app-info;1"]
|
|
.getService(Components.interfaces.nsIXULAppInfo)
|
|
.QueryInterface(Components.interfaces.nsIXULRuntime);
|
|
|
|
this._os = xulRuntime.OS;
|
|
return this._os;
|
|
},
|
|
|
|
get isB2G() {
|
|
#ifdef MOZ_B2G
|
|
return true;
|
|
#else
|
|
return false;
|
|
#endif
|
|
},
|
|
|
|
addSystemEventListener: function(target, type, listener, useCapture) {
|
|
Cc["@mozilla.org/eventlistenerservice;1"].
|
|
getService(Ci.nsIEventListenerService).
|
|
addSystemEventListener(target, type, listener, useCapture);
|
|
},
|
|
removeSystemEventListener: function(target, type, listener, useCapture) {
|
|
Cc["@mozilla.org/eventlistenerservice;1"].
|
|
getService(Ci.nsIEventListenerService).
|
|
removeSystemEventListener(target, type, listener, useCapture);
|
|
},
|
|
|
|
// helper method to check if the event is consumed by either default group's
|
|
// event listener or system group's event listener.
|
|
defaultPreventedInAnyGroup: function(event) {
|
|
// FYI: Event.defaultPrevented returns false in content context if the
|
|
// event is consumed only by system group's event listeners.
|
|
return event.defaultPrevented;
|
|
},
|
|
|
|
getDOMRequestService: function() {
|
|
var serv = Services.DOMRequest;
|
|
var res = {};
|
|
var props = ["createRequest", "createCursor", "fireError", "fireSuccess",
|
|
"fireDone", "fireDetailedError"];
|
|
for (var i in props) {
|
|
let prop = props[i];
|
|
res[prop] = function() { return serv[prop].apply(serv, arguments) };
|
|
}
|
|
return res;
|
|
},
|
|
|
|
setLogFile: function(path) {
|
|
this._mfl = new MozillaFileLogger(path);
|
|
},
|
|
|
|
log: function(data) {
|
|
this._mfl.log(data);
|
|
},
|
|
|
|
closeLogFile: function() {
|
|
this._mfl.close();
|
|
},
|
|
|
|
addCategoryEntry: function(category, entry, value, persists, replace) {
|
|
Components.classes["@mozilla.org/categorymanager;1"].
|
|
getService(Components.interfaces.nsICategoryManager).
|
|
addCategoryEntry(category, entry, value, persists, replace);
|
|
},
|
|
|
|
deleteCategoryEntry: function(category, entry, persists) {
|
|
Components.classes["@mozilla.org/categorymanager;1"].
|
|
getService(Components.interfaces.nsICategoryManager).
|
|
deleteCategoryEntry(category, entry, persists);
|
|
},
|
|
openDialog: function(win, args) {
|
|
return win.openDialog.apply(win, args);
|
|
},
|
|
// This is a blocking call which creates and spins a native event loop
|
|
spinEventLoop: function(win) {
|
|
// simply do a sync XHR back to our windows location.
|
|
var syncXHR = new win.XMLHttpRequest();
|
|
syncXHR.open('GET', win.location, false);
|
|
syncXHR.send();
|
|
},
|
|
|
|
// :jdm gets credit for this. ex: getPrivilegedProps(window, 'location.href');
|
|
getPrivilegedProps: function(obj, props) {
|
|
var parts = props.split('.');
|
|
for (var i = 0; i < parts.length; i++) {
|
|
var p = parts[i];
|
|
if (obj[p]) {
|
|
obj = obj[p];
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
return obj;
|
|
},
|
|
|
|
get focusManager() {
|
|
if (this._fm != null)
|
|
return this._fm;
|
|
|
|
this._fm = Components.classes["@mozilla.org/focus-manager;1"].
|
|
getService(Components.interfaces.nsIFocusManager);
|
|
|
|
return this._fm;
|
|
},
|
|
|
|
getFocusedElementForWindow: function(targetWindow, aDeep) {
|
|
var outParam = {};
|
|
this.focusManager.getFocusedElementForWindow(targetWindow, aDeep, outParam);
|
|
return outParam.value;
|
|
},
|
|
|
|
activeWindow: function() {
|
|
return this.focusManager.activeWindow;
|
|
},
|
|
|
|
focusedWindow: function() {
|
|
return this.focusManager.focusedWindow;
|
|
},
|
|
|
|
focus: function(aWindow) {
|
|
// This is called inside TestRunner._makeIframe without aWindow, because of assertions in oop mochitests
|
|
// With aWindow, it is called in SimpleTest.waitForFocus to allow popup window opener focus switching
|
|
if (aWindow)
|
|
aWindow.focus();
|
|
var mm = global;
|
|
if (aWindow) {
|
|
try {
|
|
mm = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
|
|
.getInterface(Ci.nsIDocShell)
|
|
.QueryInterface(Ci.nsIInterfaceRequestor)
|
|
.getInterface(Ci.nsIContentFrameMessageManager);
|
|
} catch (ex) {
|
|
/* Ignore exceptions for e.g. XUL chrome windows from mochitest-chrome
|
|
* which won't have a message manager */
|
|
}
|
|
}
|
|
mm.sendAsyncMessage("SpecialPowers.Focus", {});
|
|
},
|
|
|
|
getClipboardData: function(flavor, whichClipboard) {
|
|
if (this._cb == null)
|
|
this._cb = Components.classes["@mozilla.org/widget/clipboard;1"].
|
|
getService(Components.interfaces.nsIClipboard);
|
|
if (whichClipboard === undefined)
|
|
whichClipboard = this._cb.kGlobalClipboard;
|
|
|
|
var xferable = Components.classes["@mozilla.org/widget/transferable;1"].
|
|
createInstance(Components.interfaces.nsITransferable);
|
|
// in e10s b-c tests |content.window| is a CPOW whereas |window| works fine.
|
|
// for some non-e10s mochi tests, |window| is null whereas |content.window|
|
|
// works fine. So we take whatever is non-null!
|
|
xferable.init(this._getDocShell(typeof(window) == "undefined" ? content.window : window)
|
|
.QueryInterface(Components.interfaces.nsILoadContext));
|
|
xferable.addDataFlavor(flavor);
|
|
this._cb.getData(xferable, whichClipboard);
|
|
var data = {};
|
|
try {
|
|
xferable.getTransferData(flavor, data, {});
|
|
} catch (e) {}
|
|
data = data.value || null;
|
|
if (data == null)
|
|
return "";
|
|
|
|
return data.QueryInterface(Components.interfaces.nsISupportsString).data;
|
|
},
|
|
|
|
clipboardCopyString: function(str) {
|
|
Cc["@mozilla.org/widget/clipboardhelper;1"].
|
|
getService(Ci.nsIClipboardHelper).
|
|
copyString(str);
|
|
},
|
|
|
|
supportsSelectionClipboard: function() {
|
|
if (this._cb == null) {
|
|
this._cb = Components.classes["@mozilla.org/widget/clipboard;1"].
|
|
getService(Components.interfaces.nsIClipboard);
|
|
}
|
|
return this._cb.supportsSelectionClipboard();
|
|
},
|
|
|
|
swapFactoryRegistration: function(cid, contractID, newFactory, oldFactory) {
|
|
newFactory = Cu.waiveXrays(newFactory);
|
|
oldFactory = Cu.waiveXrays(oldFactory);
|
|
|
|
var componentRegistrar = Components.manager.QueryInterface(Components.interfaces.nsIComponentRegistrar);
|
|
|
|
var unregisterFactory = newFactory;
|
|
var registerFactory = oldFactory;
|
|
|
|
if (cid == null) {
|
|
if (contractID != null) {
|
|
cid = componentRegistrar.contractIDToCID(contractID);
|
|
oldFactory = Components.manager.getClassObject(Components.classes[contractID],
|
|
Components.interfaces.nsIFactory);
|
|
} else {
|
|
return {'error': "trying to register a new contract ID: Missing contractID"};
|
|
}
|
|
|
|
unregisterFactory = oldFactory;
|
|
registerFactory = newFactory;
|
|
}
|
|
componentRegistrar.unregisterFactory(cid,
|
|
unregisterFactory);
|
|
|
|
// Restore the original factory.
|
|
componentRegistrar.registerFactory(cid,
|
|
"",
|
|
contractID,
|
|
registerFactory);
|
|
return {'cid':cid, 'originalFactory':oldFactory};
|
|
},
|
|
|
|
_getElement: function(aWindow, id) {
|
|
return ((typeof(id) == "string") ?
|
|
aWindow.document.getElementById(id) : id);
|
|
},
|
|
|
|
dispatchEvent: function(aWindow, target, event) {
|
|
var el = this._getElement(aWindow, target);
|
|
return el.dispatchEvent(event);
|
|
},
|
|
|
|
get isDebugBuild() {
|
|
delete SpecialPowersAPI.prototype.isDebugBuild;
|
|
|
|
var debug = Cc["@mozilla.org/xpcom/debug;1"].getService(Ci.nsIDebug2);
|
|
return SpecialPowersAPI.prototype.isDebugBuild = debug.isDebugBuild;
|
|
},
|
|
assertionCount: function() {
|
|
var debugsvc = Cc['@mozilla.org/xpcom/debug;1'].getService(Ci.nsIDebug2);
|
|
return debugsvc.assertionCount;
|
|
},
|
|
|
|
/**
|
|
* Get the message manager associated with an <iframe mozbrowser>.
|
|
*/
|
|
getBrowserFrameMessageManager: function(aFrameElement) {
|
|
return this.wrap(aFrameElement.QueryInterface(Ci.nsIFrameLoaderOwner)
|
|
.frameLoader
|
|
.messageManager);
|
|
},
|
|
|
|
_getPrincipalFromArg: function(arg) {
|
|
let principal;
|
|
let secMan = Services.scriptSecurityManager;
|
|
|
|
if (typeof(arg) == "string") {
|
|
// It's an URL.
|
|
let uri = Services.io.newURI(arg);
|
|
principal = secMan.createCodebasePrincipal(uri, {});
|
|
} else if (arg.nodePrincipal) {
|
|
// It's a document.
|
|
// In some tests the arg is a wrapped DOM element, so we unwrap it first.
|
|
principal = unwrapIfWrapped(arg).nodePrincipal;
|
|
} else {
|
|
let uri = Services.io.newURI(arg.url);
|
|
let attrs = arg.originAttributes || {};
|
|
principal = secMan.createCodebasePrincipal(uri, attrs);
|
|
}
|
|
|
|
return principal;
|
|
},
|
|
|
|
addPermission: function(type, allow, arg, expireType, expireTime) {
|
|
let principal = this._getPrincipalFromArg(arg);
|
|
if (principal.isSystemPrincipal) {
|
|
return; // nothing to do
|
|
}
|
|
|
|
let permission;
|
|
if (typeof allow !== 'boolean') {
|
|
permission = allow;
|
|
} else {
|
|
permission = allow ? Ci.nsIPermissionManager.ALLOW_ACTION
|
|
: Ci.nsIPermissionManager.DENY_ACTION;
|
|
}
|
|
|
|
var msg = {
|
|
'op': 'add',
|
|
'type': type,
|
|
'permission': permission,
|
|
'principal': principal,
|
|
'expireType': (typeof expireType === "number") ? expireType : 0,
|
|
'expireTime': (typeof expireTime === "number") ? expireTime : 0
|
|
};
|
|
|
|
this._sendSyncMessage('SPPermissionManager', msg);
|
|
},
|
|
|
|
removePermission: function(type, arg) {
|
|
let principal = this._getPrincipalFromArg(arg);
|
|
if (principal.isSystemPrincipal) {
|
|
return; // nothing to do
|
|
}
|
|
|
|
var msg = {
|
|
'op': 'remove',
|
|
'type': type,
|
|
'principal': principal
|
|
};
|
|
|
|
this._sendSyncMessage('SPPermissionManager', msg);
|
|
},
|
|
|
|
hasPermission: function (type, arg) {
|
|
let principal = this._getPrincipalFromArg(arg);
|
|
if (principal.isSystemPrincipal) {
|
|
return true; // system principals have all permissions
|
|
}
|
|
|
|
var msg = {
|
|
'op': 'has',
|
|
'type': type,
|
|
'principal': principal
|
|
};
|
|
|
|
return this._sendSyncMessage('SPPermissionManager', msg)[0];
|
|
},
|
|
|
|
testPermission: function (type, value, arg) {
|
|
let principal = this._getPrincipalFromArg(arg);
|
|
if (principal.isSystemPrincipal) {
|
|
return true; // system principals have all permissions
|
|
}
|
|
|
|
var msg = {
|
|
'op': 'test',
|
|
'type': type,
|
|
'value': value,
|
|
'principal': principal
|
|
};
|
|
return this._sendSyncMessage('SPPermissionManager', msg)[0];
|
|
},
|
|
|
|
isContentWindowPrivate: function(win) {
|
|
return PrivateBrowsingUtils.isContentWindowPrivate(win);
|
|
},
|
|
|
|
notifyObserversInParentProcess: function(subject, topic, data) {
|
|
if (subject) {
|
|
throw new Error("Can't send subject to another process!");
|
|
}
|
|
if (this.isMainProcess()) {
|
|
this.notifyObservers(subject, topic, data);
|
|
return;
|
|
}
|
|
var msg = {
|
|
'op': 'notify',
|
|
'observerTopic': topic,
|
|
'observerData': data
|
|
};
|
|
this._sendSyncMessage('SPObserverService', msg);
|
|
},
|
|
|
|
removeAllServiceWorkerData: function() {
|
|
this.notifyObserversInParentProcess(null, "browser:purge-session-history", "");
|
|
},
|
|
|
|
removeServiceWorkerDataForExampleDomain: function() {
|
|
this.notifyObserversInParentProcess(null, "browser:purge-domain-data", "example.com");
|
|
},
|
|
|
|
cleanUpSTSData: function(origin, flags) {
|
|
return this._sendSyncMessage('SPCleanUpSTSData', {origin: origin, flags: flags || 0});
|
|
},
|
|
|
|
_nextExtensionID: 0,
|
|
_extensionListeners: null,
|
|
|
|
loadExtension: function(ext, handler) {
|
|
if (this._extensionListeners == null) {
|
|
this._extensionListeners = new Set();
|
|
|
|
this._addMessageListener("SPExtensionMessage", msg => {
|
|
for (let listener of this._extensionListeners) {
|
|
try {
|
|
listener(msg);
|
|
} catch (e) {
|
|
Cu.reportError(e);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Note, this is not the addon is as used by the AddonManager etc,
|
|
// this is just an identifier used for specialpowers messaging
|
|
// between this content process and the chrome process.
|
|
let id = this._nextExtensionID++;
|
|
|
|
let resolveStartup, resolveUnload, rejectStartup;
|
|
let startupPromise = new Promise((resolve, reject) => {
|
|
resolveStartup = resolve;
|
|
rejectStartup = reject;
|
|
});
|
|
let unloadPromise = new Promise(resolve => { resolveUnload = resolve; });
|
|
|
|
startupPromise.catch(() => {
|
|
this._extensionListeners.delete(listener);
|
|
});
|
|
|
|
handler = Cu.waiveXrays(handler);
|
|
ext = Cu.waiveXrays(ext);
|
|
|
|
let sp = this;
|
|
let state = "uninitialized";
|
|
let extension = {
|
|
get state() { return state; },
|
|
|
|
startup() {
|
|
state = "pending";
|
|
sp._sendAsyncMessage("SPStartupExtension", {id});
|
|
return startupPromise;
|
|
},
|
|
|
|
unload() {
|
|
state = "unloading";
|
|
sp._sendAsyncMessage("SPUnloadExtension", {id});
|
|
return unloadPromise;
|
|
},
|
|
|
|
sendMessage(...args) {
|
|
sp._sendAsyncMessage("SPExtensionMessage", {id, args});
|
|
},
|
|
};
|
|
|
|
this._sendAsyncMessage("SPLoadExtension", {ext, id});
|
|
|
|
let listener = (msg) => {
|
|
if (msg.data.id == id) {
|
|
if (msg.data.type == "extensionStarted") {
|
|
state = "running";
|
|
resolveStartup();
|
|
} else if (msg.data.type == "extensionSetId") {
|
|
extension.id = msg.data.args[0];
|
|
} else if (msg.data.type == "extensionFailed") {
|
|
state = "failed";
|
|
rejectStartup("startup failed");
|
|
} else if (msg.data.type == "extensionUnloaded") {
|
|
this._extensionListeners.delete(listener);
|
|
state = "unloaded";
|
|
resolveUnload();
|
|
} else if (msg.data.type in handler) {
|
|
handler[msg.data.type](...msg.data.args);
|
|
} else {
|
|
dump(`Unexpected: ${msg.data.type}\n`);
|
|
}
|
|
}
|
|
};
|
|
|
|
this._extensionListeners.add(listener);
|
|
return extension;
|
|
},
|
|
|
|
invalidateExtensionStorageCache: function() {
|
|
this.notifyObserversInParentProcess(null, "extension-invalidate-storage-cache", "");
|
|
},
|
|
|
|
allowMedia: function(window, enable) {
|
|
this._getDocShell(window).allowMedia = enable;
|
|
},
|
|
|
|
createChromeCache: function(name, url) {
|
|
let principal = this._getPrincipalFromArg(url);
|
|
return wrapIfUnwrapped(new content.window.CacheStorage(name, principal));
|
|
},
|
|
|
|
loadChannelAndReturnStatus: function(url, loadUsingSystemPrincipal) {
|
|
const BinaryInputStream =
|
|
Components.Constructor("@mozilla.org/binaryinputstream;1",
|
|
"nsIBinaryInputStream",
|
|
"setInputStream");
|
|
|
|
return new Promise(function(resolve) {
|
|
let listener = {
|
|
httpStatus : 0,
|
|
|
|
onStartRequest: function(request, context) {
|
|
request.QueryInterface(Ci.nsIHttpChannel);
|
|
this.httpStatus = request.responseStatus;
|
|
},
|
|
|
|
onDataAvailable: function(request, context, stream, offset, count) {
|
|
new BinaryInputStream(stream).readByteArray(count);
|
|
},
|
|
|
|
onStopRequest: function(request, context, status) {
|
|
/* testing here that the redirect was not followed. If it was followed
|
|
we would see a http status of 200 and status of NS_OK */
|
|
|
|
let httpStatus = this.httpStatus;
|
|
resolve({status, httpStatus});
|
|
}
|
|
};
|
|
let uri = NetUtil.newURI(url);
|
|
let channel = NetUtil.newChannel({uri, loadUsingSystemPrincipal});
|
|
|
|
channel.loadFlags |= Ci.nsIChannel.LOAD_DOCUMENT_URI;
|
|
channel.QueryInterface(Ci.nsIHttpChannelInternal);
|
|
channel.documentURI = uri;
|
|
channel.asyncOpen2(listener);
|
|
});
|
|
},
|
|
|
|
_pu: null,
|
|
|
|
get ParserUtils() {
|
|
if (this._pu != null)
|
|
return this._pu;
|
|
|
|
let pu = Cc["@mozilla.org/parserutils;1"].getService(Ci.nsIParserUtils);
|
|
// We need to create and return our own wrapper.
|
|
this._pu = {
|
|
sanitize: function(src, flags) {
|
|
return pu.sanitize(src, flags);
|
|
},
|
|
convertToPlainText: function(src, flags, wrapCol) {
|
|
return pu.convertToPlainText(src, flags, wrapCol);
|
|
},
|
|
parseFragment: function(fragment, flags, isXML, baseURL, element) {
|
|
let baseURI = baseURL ? NetUtil.newURI(baseURL) : null;
|
|
return pu.parseFragment(unwrapIfWrapped(fragment),
|
|
flags, isXML, baseURI,
|
|
unwrapIfWrapped(element));
|
|
},
|
|
};
|
|
return this._pu;
|
|
},
|
|
|
|
createDOMWalker: function(node, showAnonymousContent) {
|
|
node = unwrapIfWrapped(node);
|
|
let walker = Cc["@mozilla.org/inspector/deep-tree-walker;1"].
|
|
createInstance(Ci.inIDeepTreeWalker);
|
|
walker.showAnonymousContent = showAnonymousContent;
|
|
walker.init(node.ownerDocument, Ci.nsIDOMNodeFilter.SHOW_ALL);
|
|
walker.currentNode = node;
|
|
return {
|
|
get firstChild() {
|
|
return wrapIfUnwrapped(walker.firstChild());
|
|
},
|
|
get lastChild() {
|
|
return wrapIfUnwrapped(walker.lastChild());
|
|
},
|
|
};
|
|
},
|
|
|
|
observeMutationEvents: function(mo, node, nativeAnonymousChildList, subtree) {
|
|
unwrapIfWrapped(mo).observe(unwrapIfWrapped(node),
|
|
{nativeAnonymousChildList, subtree});
|
|
},
|
|
|
|
doCommand(window, cmd) {
|
|
return this._getDocShell(window).doCommand(cmd);
|
|
},
|
|
|
|
setCommandNode(window, node) {
|
|
return this._getDocShell(window).contentViewer
|
|
.QueryInterface(Ci.nsIContentViewerEdit)
|
|
.setCommandNode(node);
|
|
},
|
|
|
|
/* Bug 1339006 Runnables of nsIURIClassifier.classify may be labeled by
|
|
* SystemGroup, but some test cases may run as web content. That would assert
|
|
* when trying to enter web content from a runnable labeled by the
|
|
* SystemGroup. To avoid that, we run classify from SpecialPowers which is
|
|
* chrome-privileged and allowed to run inside SystemGroup
|
|
*/
|
|
|
|
doUrlClassify(principal, eventTarget, tpEnabled, callback) {
|
|
let classifierService =
|
|
Cc["@mozilla.org/url-classifier/dbservice;1"].getService(Ci.nsIURIClassifier);
|
|
|
|
let wrapCallback = (...args) => {
|
|
Services.tm.mainThread.dispatch(() => {
|
|
if (typeof callback == 'function') {
|
|
callback.call(undefined, ...args);
|
|
} else {
|
|
callback.onClassifyComplete.call(undefined, ...args);
|
|
}
|
|
}, Ci.nsIThread.DISPATCH_NORMAL);
|
|
};
|
|
|
|
return classifierService.classify(unwrapIfWrapped(principal), eventTarget,
|
|
tpEnabled, wrapCallback);
|
|
},
|
|
|
|
};
|
|
|
|
this.SpecialPowersAPI = SpecialPowersAPI;
|
|
this.bindDOMWindowUtils = bindDOMWindowUtils;
|
|
this.getRawComponents = getRawComponents;
|