diff options
Diffstat (limited to 'testing/specialpowers/content/specialpowersAPI.js')
-rw-r--r-- | testing/specialpowers/content/specialpowersAPI.js | 2100 |
1 files changed, 2100 insertions, 0 deletions
diff --git a/testing/specialpowers/content/specialpowersAPI.js b/testing/specialpowers/content/specialpowersAPI.js new file mode 100644 index 000000000..ee94e84a3 --- /dev/null +++ b/testing/specialpowers/content/specialpowersAPI.js @@ -0,0 +1,2100 @@ +/* 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.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); + + this.callback.call(undefined, m); + + 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); + }, + + 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, + false); + }, + removeAutoCompletePopupEventListener: function(window, eventname, listener) { + this._getAutoCompletePopup(window).removeEventListener(eventname, + listener, + false); + }, + 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, null, null); + principal = secMan.createCodebasePrincipal(uri, {}); + } else if (arg.manifestURL) { + // It's a thing representing an app. + let appsSvc = Cc["@mozilla.org/AppsService;1"] + .getService(Ci.nsIAppsService) + let app = appsSvc.getAppByManifestURL(arg.manifestURL); + if (!app) { + throw "No app for this manifest!"; + } + + principal = app.principal; + } 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, null, null); + 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); + }, +}; + +this.SpecialPowersAPI = SpecialPowersAPI; +this.bindDOMWindowUtils = bindDOMWindowUtils; +this.getRawComponents = getRawComponents; |