From da522d42d4f09e5914fa16f1c0cbb079bcd5f4cf Mon Sep 17 00:00:00 2001 From: Brendan Dahl Date: Fri, 11 Apr 2014 10:40:15 -0700 Subject: [PATCH 1/4] Fix special powers add-on for firefox. --- .../chrome.manifest | 7 +- .../specialpowers/content/MozillaLogger.js | 121 ++ .../content/SpecialPowersObserverAPI.js | 414 ++++ .../specialpowers/content/specialpowers.js | 378 +--- .../specialpowers/content/specialpowersAPI.js | 1744 +++++++++++++++++ .../chrome/specialpowers/modules/Assert.jsm | 442 +++++ .../specialpowers/modules/MockColorPicker.jsm | 125 ++ .../specialpowers/modules/MockFilePicker.jsm | 236 +++ .../modules/MockPermissionPrompt.jsm | 97 + .../components/SpecialPowersObserver.js | 303 +-- test/resources/firefox/prefs.js | 1 + 11 files changed, 3348 insertions(+), 520 deletions(-) create mode 100644 test/resources/firefox/extensions/special-powers@mozilla.org/chrome/specialpowers/content/MozillaLogger.js create mode 100644 test/resources/firefox/extensions/special-powers@mozilla.org/chrome/specialpowers/content/SpecialPowersObserverAPI.js create mode 100644 test/resources/firefox/extensions/special-powers@mozilla.org/chrome/specialpowers/content/specialpowersAPI.js create mode 100644 test/resources/firefox/extensions/special-powers@mozilla.org/chrome/specialpowers/modules/Assert.jsm create mode 100644 test/resources/firefox/extensions/special-powers@mozilla.org/chrome/specialpowers/modules/MockColorPicker.jsm create mode 100644 test/resources/firefox/extensions/special-powers@mozilla.org/chrome/specialpowers/modules/MockFilePicker.jsm create mode 100644 test/resources/firefox/extensions/special-powers@mozilla.org/chrome/specialpowers/modules/MockPermissionPrompt.jsm mode change 100755 => 100644 test/resources/firefox/extensions/special-powers@mozilla.org/components/SpecialPowersObserver.js diff --git a/test/resources/firefox/extensions/special-powers@mozilla.org/chrome.manifest b/test/resources/firefox/extensions/special-powers@mozilla.org/chrome.manifest index 614f31c3a..cac9fd65b 100644 --- a/test/resources/firefox/extensions/special-powers@mozilla.org/chrome.manifest +++ b/test/resources/firefox/extensions/special-powers@mozilla.org/chrome.manifest @@ -1,4 +1,5 @@ -content specialpowers chrome/specialpowers/content/ -component {59a52458-13e0-4d93-9d85-a637344f29a1} components/SpecialPowersObserver.js -contract @mozilla.org/special-powers-observer;1 {59a52458-13e0-4d93-9d85-a637344f29a1} category profile-after-change @mozilla.org/special-powers-observer;1 @mozilla.org/special-powers-observer;1 +component {59a52458-13e0-4d93-9d85-a637344f29a1} components/SpecialPowersObserver.js +content specialpowers chrome/specialpowers/content/ +contract @mozilla.org/special-powers-observer;1 {59a52458-13e0-4d93-9d85-a637344f29a1} +resource specialpowers chrome/specialpowers/modules/ diff --git a/test/resources/firefox/extensions/special-powers@mozilla.org/chrome/specialpowers/content/MozillaLogger.js b/test/resources/firefox/extensions/special-powers@mozilla.org/chrome/specialpowers/content/MozillaLogger.js new file mode 100644 index 000000000..96ad8a2b2 --- /dev/null +++ b/test/resources/firefox/extensions/special-powers@mozilla.org/chrome/specialpowers/content/MozillaLogger.js @@ -0,0 +1,121 @@ +/** + * MozillaLogger, a base class logger that just logs to stdout. + */ + +function MozillaLogger(aPath) { +} + +MozillaLogger.prototype = { + + init : function(path) {}, + + getLogCallback : function() { + return function (msg) { + var data = msg.num + " " + msg.level + " " + msg.info.join(' ') + "\n"; + dump(data); + } + }, + + log : function(msg) { + dump(msg); + }, + + close : function() {} +}; + + +/** + * SpecialPowersLogger, inherits from MozillaLogger and utilizes SpecialPowers. + * intented to be used in content scripts to write to a file + */ +function SpecialPowersLogger(aPath) { + // Call the base constructor + MozillaLogger.call(this); + this.prototype = new MozillaLogger(aPath); + this.init(aPath); +} + +SpecialPowersLogger.prototype = { + init : function (path) { + SpecialPowers.setLogFile(path); + }, + + getLogCallback : function () { + return function (msg) { + var data = msg.num + " " + msg.level + " " + msg.info.join(' ') + "\n"; + SpecialPowers.log(data); + + if (data.indexOf("SimpleTest FINISH") >= 0) { + SpecialPowers.closeLogFile(); + } + } + }, + + log : function (msg) { + SpecialPowers.log(msg); + }, + + close : function () { + SpecialPowers.closeLogFile(); + } +}; + + +/** + * MozillaFileLogger, a log listener that can write to a local file. + * intended to be run from chrome space + */ + +/** Init the file logger with the absolute path to the file. + It will create and append if the file already exists **/ +function MozillaFileLogger(aPath) { + // Call the base constructor + MozillaLogger.call(this); + this.prototype = new MozillaLogger(aPath); + this.init(aPath); +} + +MozillaFileLogger.prototype = { + + init : function (path) { + var PR_WRITE_ONLY = 0x02; // Open for writing only. + var PR_CREATE_FILE = 0x08; + var PR_APPEND = 0x10; + this._file = Components.classes["@mozilla.org/file/local;1"]. + createInstance(Components.interfaces.nsILocalFile); + this._file.initWithPath(path); + this._foStream = Components.classes["@mozilla.org/network/file-output-stream;1"]. + createInstance(Components.interfaces.nsIFileOutputStream); + this._foStream.init(this._file, PR_WRITE_ONLY | PR_CREATE_FILE | PR_APPEND, + 0664, 0); + }, + + getLogCallback : function() { + return function (msg) { + var data = msg.num + " " + msg.level + " " + msg.info.join(' ') + "\n"; + if (MozillaFileLogger._foStream) + this._foStream.write(data, data.length); + + if (data.indexOf("SimpleTest FINISH") >= 0) { + MozillaFileLogger.close(); + } + } + }, + + log : function(msg) { + if (this._foStream) + this._foStream.write(msg, msg.length); + }, + + close : function() { + if(this._foStream) + this._foStream.close(); + + this._foStream = null; + this._file = null; + } +}; + +this.MozillaLogger = MozillaLogger; +this.SpecialPowersLogger = SpecialPowersLogger; +this.MozillaFileLogger = MozillaFileLogger; diff --git a/test/resources/firefox/extensions/special-powers@mozilla.org/chrome/specialpowers/content/SpecialPowersObserverAPI.js b/test/resources/firefox/extensions/special-powers@mozilla.org/chrome/specialpowers/content/SpecialPowersObserverAPI.js new file mode 100644 index 000000000..cb871138a --- /dev/null +++ b/test/resources/firefox/extensions/special-powers@mozilla.org/chrome/specialpowers/content/SpecialPowersObserverAPI.js @@ -0,0 +1,414 @@ +/* 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/. */ +"use strict"; + +Components.utils.import("resource://gre/modules/Services.jsm"); + +if (typeof(Ci) == 'undefined') { + var Ci = Components.interfaces; +} + +if (typeof(Cc) == 'undefined') { + var Cc = Components.classes; +} + +/** + * Special Powers Exception - used to throw exceptions nicely + **/ +function SpecialPowersException(aMsg) { + this.message = aMsg; + this.name = "SpecialPowersException"; +} + +SpecialPowersException.prototype.toString = function() { + return this.name + ': "' + this.message + '"'; +}; + +this.SpecialPowersObserverAPI = function SpecialPowersObserverAPI() { + this._crashDumpDir = null; + this._processCrashObserversRegistered = false; + this._chromeScriptListeners = []; +} + +function parseKeyValuePairs(text) { + var lines = text.split('\n'); + var data = {}; + for (let i = 0; i < lines.length; i++) { + if (lines[i] == '') + continue; + + // can't just .split() because the value might contain = characters + let eq = lines[i].indexOf('='); + if (eq != -1) { + let [key, value] = [lines[i].substring(0, eq), + lines[i].substring(eq + 1)]; + if (key && value) + data[key] = value.replace(/\\n/g, "\n").replace(/\\\\/g, "\\"); + } + } + return data; +} + +function parseKeyValuePairsFromFile(file) { + var fstream = Cc["@mozilla.org/network/file-input-stream;1"]. + createInstance(Ci.nsIFileInputStream); + fstream.init(file, -1, 0, 0); + var is = Cc["@mozilla.org/intl/converter-input-stream;1"]. + createInstance(Ci.nsIConverterInputStream); + is.init(fstream, "UTF-8", 1024, Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER); + var str = {}; + var contents = ''; + while (is.readString(4096, str) != 0) { + contents += str.value; + } + is.close(); + fstream.close(); + return parseKeyValuePairs(contents); +} + +SpecialPowersObserverAPI.prototype = { + + _observe: function(aSubject, aTopic, aData) { + function addDumpIDToMessage(propertyName) { + var id = aSubject.getPropertyAsAString(propertyName); + if (id) { + message.dumpIDs.push({id: id, extension: "dmp"}); + message.dumpIDs.push({id: id, extension: "extra"}); + } + } + + switch(aTopic) { + case "plugin-crashed": + case "ipc:content-shutdown": + var message = { type: "crash-observed", dumpIDs: [] }; + aSubject = aSubject.QueryInterface(Ci.nsIPropertyBag2); + if (aTopic == "plugin-crashed") { + addDumpIDToMessage("pluginDumpID"); + addDumpIDToMessage("browserDumpID"); + + let pluginID = aSubject.getPropertyAsAString("pluginDumpID"); + let extra = this._getExtraData(pluginID); + if (extra && ("additional_minidumps" in extra)) { + let dumpNames = extra.additional_minidumps.split(','); + for (let name of dumpNames) { + message.dumpIDs.push({id: pluginID + "-" + name, extension: "dmp"}); + } + } + } else { // ipc:content-shutdown + addDumpIDToMessage("dumpID"); + } + this._sendAsyncMessage("SPProcessCrashService", message); + break; + } + }, + + _getCrashDumpDir: function() { + if (!this._crashDumpDir) { + this._crashDumpDir = Services.dirsvc.get("ProfD", Ci.nsIFile); + this._crashDumpDir.append("minidumps"); + } + return this._crashDumpDir; + }, + + _getExtraData: function(dumpId) { + let extraFile = this._getCrashDumpDir().clone(); + extraFile.append(dumpId + ".extra"); + if (!extraFile.exists()) { + return null; + } + return parseKeyValuePairsFromFile(extraFile); + }, + + _deleteCrashDumpFiles: function(aFilenames) { + var crashDumpDir = this._getCrashDumpDir(); + if (!crashDumpDir.exists()) { + return false; + } + + var success = aFilenames.length != 0; + aFilenames.forEach(function(crashFilename) { + var file = crashDumpDir.clone(); + file.append(crashFilename); + if (file.exists()) { + file.remove(false); + } else { + success = false; + } + }); + return success; + }, + + _findCrashDumpFiles: function(aToIgnore) { + var crashDumpDir = this._getCrashDumpDir(); + var entries = crashDumpDir.exists() && crashDumpDir.directoryEntries; + if (!entries) { + return []; + } + + var crashDumpFiles = []; + while (entries.hasMoreElements()) { + var file = entries.getNext().QueryInterface(Ci.nsIFile); + var path = String(file.path); + if (path.match(/\.(dmp|extra)$/) && !aToIgnore[path]) { + crashDumpFiles.push(path); + } + } + return crashDumpFiles.concat(); + }, + + _getURI: function (url) { + return Services.io.newURI(url, null, null); + }, + + _readUrlAsString: function(aUrl) { + // Fetch script content as we can't use scriptloader's loadSubScript + // to evaluate http:// urls... + var scriptableStream = Cc["@mozilla.org/scriptableinputstream;1"] + .getService(Ci.nsIScriptableInputStream); + var channel = Services.io.newChannel(aUrl, null, null); + var input = channel.open(); + scriptableStream.init(input); + + var str; + var buffer = []; + + while ((str = scriptableStream.read(4096))) { + buffer.push(str); + } + + var output = buffer.join(""); + + scriptableStream.close(); + input.close(); + + var status; + try { + channel.QueryInterface(Ci.nsIHttpChannel); + status = channel.responseStatus; + } catch(e) { + /* The channel is not a nsIHttpCHannel, but that's fine */ + dump("-*- _readUrlAsString: Got an error while fetching " + + "chrome script '" + aUrl + "': (" + e.name + ") " + e.message + ". " + + "Ignoring.\n"); + } + + if (status == 404) { + throw new SpecialPowersException( + "Error while executing chrome script '" + aUrl + "':\n" + + "The script doesn't exists. Ensure you have registered it in " + + "'support-files' in your mochitest.ini."); + } + + return output; + }, + + /** + * messageManager callback function + * This will get requests from our API in the window and process them in chrome for it + **/ + _receiveMessageAPI: function(aMessage) { + // We explicitly return values in the below code so that this function + // doesn't trigger a flurry of warnings about "does not always return + // a value". + switch(aMessage.name) { + case "SPPrefService": + var prefs = Services.prefs; + var prefType = aMessage.json.prefType.toUpperCase(); + var prefName = aMessage.json.prefName; + var prefValue = "prefValue" in aMessage.json ? aMessage.json.prefValue : null; + + if (aMessage.json.op == "get") { + if (!prefName || !prefType) + throw new SpecialPowersException("Invalid parameters for get in SPPrefService"); + + // return null if the pref doesn't exist + if (prefs.getPrefType(prefName) == prefs.PREF_INVALID) + return null; + } else if (aMessage.json.op == "set") { + if (!prefName || !prefType || prefValue === null) + throw new SpecialPowersException("Invalid parameters for set in SPPrefService"); + } else if (aMessage.json.op == "clear") { + if (!prefName) + throw new SpecialPowersException("Invalid parameters for clear in SPPrefService"); + } else { + throw new SpecialPowersException("Invalid operation for SPPrefService"); + } + + // Now we make the call + switch(prefType) { + case "BOOL": + if (aMessage.json.op == "get") + return(prefs.getBoolPref(prefName)); + else + return(prefs.setBoolPref(prefName, prefValue)); + case "INT": + if (aMessage.json.op == "get") + return(prefs.getIntPref(prefName)); + else + return(prefs.setIntPref(prefName, prefValue)); + case "CHAR": + if (aMessage.json.op == "get") + return(prefs.getCharPref(prefName)); + else + return(prefs.setCharPref(prefName, prefValue)); + case "COMPLEX": + if (aMessage.json.op == "get") + return(prefs.getComplexValue(prefName, prefValue[0])); + else + return(prefs.setComplexValue(prefName, prefValue[0], prefValue[1])); + case "": + if (aMessage.json.op == "clear") { + prefs.clearUserPref(prefName); + return undefined; + } + } + return undefined; // See comment at the beginning of this function. + + case "SPProcessCrashService": + switch (aMessage.json.op) { + case "register-observer": + this._addProcessCrashObservers(); + break; + case "unregister-observer": + this._removeProcessCrashObservers(); + break; + case "delete-crash-dump-files": + return this._deleteCrashDumpFiles(aMessage.json.filenames); + case "find-crash-dump-files": + return this._findCrashDumpFiles(aMessage.json.crashDumpFilesToIgnore); + default: + throw new SpecialPowersException("Invalid operation for SPProcessCrashService"); + } + return undefined; // See comment at the beginning of this function. + + case "SPPermissionManager": + let msg = aMessage.json; + + let secMan = Services.scriptSecurityManager; + let principal = secMan.getAppCodebasePrincipal(this._getURI(msg.url), msg.appId, msg.isInBrowserElement); + + switch (msg.op) { + case "add": + Services.perms.addFromPrincipal(principal, msg.type, msg.permission); + break; + case "remove": + Services.perms.removeFromPrincipal(principal, msg.type); + break; + case "has": + let hasPerm = Services.perms.testPermissionFromPrincipal(principal, msg.type); + if (hasPerm == Ci.nsIPermissionManager.ALLOW_ACTION) + return true; + return false; + break; + case "test": + let testPerm = Services.perms.testPermissionFromPrincipal(principal, msg.type, msg.value); + if (testPerm == msg.value) { + return true; + } + return false; + break; + default: + throw new SpecialPowersException("Invalid operation for " + + "SPPermissionManager"); + } + return undefined; // See comment at the beginning of this function. + + case "SPWebAppService": + let Webapps = {}; + Components.utils.import("resource://gre/modules/Webapps.jsm", Webapps); + switch (aMessage.json.op) { + case "set-launchable": + let val = Webapps.DOMApplicationRegistry.allAppsLaunchable; + Webapps.DOMApplicationRegistry.allAppsLaunchable = aMessage.json.launchable; + return val; + default: + throw new SpecialPowersException("Invalid operation for SPWebAppsService"); + } + return undefined; // See comment at the beginning of this function. + + case "SPObserverService": + switch (aMessage.json.op) { + case "notify": + let topic = aMessage.json.observerTopic; + let data = aMessage.json.observerData + Services.obs.notifyObservers(null, topic, data); + break; + default: + throw new SpecialPowersException("Invalid operation for SPObserverervice"); + } + return undefined; // See comment at the beginning of this function. + + case "SPLoadChromeScript": + var url = aMessage.json.url; + var id = aMessage.json.id; + + var jsScript = this._readUrlAsString(url); + + // Setup a chrome sandbox that has access to sendAsyncMessage + // and addMessageListener in order to communicate with + // the mochitest. + var systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal(); + var sb = Components.utils.Sandbox(systemPrincipal); + var mm = aMessage.target + .QueryInterface(Ci.nsIFrameLoaderOwner) + .frameLoader + .messageManager; + sb.sendAsyncMessage = (name, message) => { + mm.sendAsyncMessage("SPChromeScriptMessage", + { id: id, name: name, message: message }); + }; + sb.addMessageListener = (name, listener) => { + this._chromeScriptListeners.push({ id: id, name: name, listener: listener }); + }; + + // Also expose assertion functions + let reporter = function (err, message, stack) { + // Pipe assertions back to parent process + mm.sendAsyncMessage("SPChromeScriptAssert", + { id: id, url: url, err: err, message: message, + stack: stack }); + }; + Object.defineProperty(sb, "assert", { + get: function () { + let scope = Components.utils.createObjectIn(sb); + Services.scriptloader.loadSubScript("resource://specialpowers/Assert.jsm", + scope); + + let assert = new scope.Assert(reporter); + delete sb.assert; + return sb.assert = assert; + }, + configurable: true + }); + + // Evaluate the chrome script + try { + Components.utils.evalInSandbox(jsScript, sb, "1.8", url, 1); + } catch(e) { + throw new SpecialPowersException("Error while executing chrome " + + "script '" + url + "':\n" + e + "\n" + + e.fileName + ":" + e.lineNumber); + } + return undefined; // See comment at the beginning of this function. + + case "SPChromeScriptMessage": + var id = aMessage.json.id; + var name = aMessage.json.name; + var message = aMessage.json.message; + this._chromeScriptListeners + .filter(o => (o.name == name && o.id == id)) + .forEach(o => o.listener(message)); + return undefined; // See comment at the beginning of this function. + + default: + throw new SpecialPowersException("Unrecognized Special Powers API"); + } + + // We throw an exception before reaching this explicit return because + // we should never be arriving here anyway. + throw new SpecialPowersException("Unreached code"); + return undefined; + } +}; + diff --git a/test/resources/firefox/extensions/special-powers@mozilla.org/chrome/specialpowers/content/specialpowers.js b/test/resources/firefox/extensions/special-powers@mozilla.org/chrome/specialpowers/content/specialpowers.js index 538b104eb..31ff09303 100644 --- a/test/resources/firefox/extensions/special-powers@mozilla.org/chrome/specialpowers/content/specialpowers.js +++ b/test/resources/firefox/extensions/special-powers@mozilla.org/chrome/specialpowers/content/specialpowers.js @@ -1,332 +1,101 @@ -/* ***** BEGIN LICENSE BLOCK ***** - * Version: MPL 1.1/GPL 2.0/LGPL 2.1 - * - * The contents of this file are subject to the Mozilla Public License Version - * 1.1 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * http://www.mozilla.org/MPL/ - * - * Software distributed under the License is distributed on an "AS IS" basis, - * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License - * for the specific language governing rights and limitations under the - * License. - * - * The Original Code is Special Powers code - * - * The Initial Developer of the Original Code is - * Mozilla Foundation. - * Portions created by the Initial Developer are Copyright (C) 2010 - * the Initial Developer. All Rights Reserved. - * - * Contributor(s): - * Clint Talbert cmtalbert@gmail.com - * - * Alternatively, the contents of this file may be used under the terms of - * either the GNU General Public License Version 2 or later (the "GPL"), or - * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), - * in which case the provisions of the GPL or the LGPL are applicable instead - * of those above. If you wish to allow use of your version of this file only - * under the terms of either the GPL or the LGPL, and not to allow others to - * use your version of this file under the terms of the MPL, indicate your - * decision by deleting the provisions above and replace them with the notice - * and other provisions required by the GPL or the LGPL. If you do not delete - * the provisions above, a recipient may use your version of this file under - * the terms of any one of the MPL, the GPL or the LGPL. - * - * ***** END LICENSE BLOCK *****/ +/* 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 */ -var Ci = Components.interfaces; -var Cc = Components.classes; - function SpecialPowers(window) { - this.window = window; - bindDOMWindowUtils(this, window); + this.window = Components.utils.getWeakReference(window); this._encounteredCrashDumpFiles = []; this._unexpectedCrashDumpFiles = { }; this._crashDumpDir = null; + this.DOMWindowUtils = bindDOMWindowUtils(window); + Object.defineProperty(this, 'Components', { + configurable: true, enumerable: true, get: function() { + var win = this.window.get(); + if (!win) + return null; + return getRawComponents(win); + }}); this._pongHandlers = []; this._messageListener = this._messageReceived.bind(this); addMessageListener("SPPingService", this._messageListener); } -function bindDOMWindowUtils(sp, window) { - var util = window.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindowUtils); - // This bit of magic brought to you by the letters - // B Z, and E, S and the number 5. - // - // Take all of the properties on the nsIDOMWindowUtils-implementing - // object, and rebind them onto a new object with a stub that uses - // apply to call them from this privileged scope. This way we don't - // have to explicitly stub out new methods that appear on - // nsIDOMWindowUtils. - var proto = Object.getPrototypeOf(util); - var target = {}; - function rebind(desc, prop) { - if (prop in desc && typeof(desc[prop]) == "function") { - var oldval = desc[prop]; - desc[prop] = function() { return oldval.apply(util, arguments); }; - } - } - for (var i in proto) { - var desc = Object.getOwnPropertyDescriptor(proto, i); - rebind(desc, "get"); - rebind(desc, "set"); - rebind(desc, "value"); - Object.defineProperty(target, i, desc); - } - sp.DOMWindowUtils = target; -} +SpecialPowers.prototype = new SpecialPowersAPI(); -SpecialPowers.prototype = { - toString: function() { return "[SpecialPowers]"; }, - sanityCheck: function() { return "foo"; }, +SpecialPowers.prototype.toString = function() { return "[SpecialPowers]"; }; +SpecialPowers.prototype.sanityCheck = function() { return "foo"; }; - // This gets filled in in the constructor. - DOMWindowUtils: undefined, +// This gets filled in in the constructor. +SpecialPowers.prototype.DOMWindowUtils = undefined; +SpecialPowers.prototype.Components = undefined; - // 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)); - }, +SpecialPowers.prototype._sendSyncMessage = function(msgname, msg) { + return sendSyncMessage(msgname, msg); +}; - // 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)); - }, +SpecialPowers.prototype._sendAsyncMessage = function(msgname, msg) { + sendAsyncMessage(msgname, msg); +}; - // Mimic the clearUserPref API - clearUserPref: function(aPrefName) { - var msg = {'op':'clear', 'prefName': aPrefName, 'prefType': ""}; - sendSyncMessage('SPPrefService', msg); - }, +SpecialPowers.prototype._addMessageListener = function(msgname, listener) { + addMessageListener(msgname, listener); +}; - // 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}; - } - return(sendSyncMessage('SPPrefService', msg)[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(sendSyncMessage('SPPrefService', msg)[0]); - }, +SpecialPowers.prototype._removeMessageListener = function(msgname, listener) { + removeMessageListener(msgname, listener); +}; - //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); - }, - _getDocShell: function(window) { - return window.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIWebNavigation) - .QueryInterface(Ci.nsIDocShell); - }, - _getMUDV: function(window) { - return this._getDocShell(window).contentViewer - .QueryInterface(Ci.nsIMarkupDocumentViewer); - }, - _getAutoCompletePopup: function(window) { - return this._getTopChromeWindow(window).document - .getElementById("PopupAutoComplete"); - }, - addAutoCompletePopupEventListener: function(window, listener) { - this._getAutoCompletePopup(window).addEventListener("popupshowing", - listener, - false); - }, - removeAutoCompletePopupEventListener: function(window, listener) { - this._getAutoCompletePopup(window).removeEventListener("popupshowing", - listener, - false); - }, - isBackButtonEnabled: function(window) { - return !this._getTopChromeWindow(window).document - .getElementById("Browser:Back") - .hasAttribute("disabled"); - }, +SpecialPowers.prototype.registerProcessCrashObservers = function() { + addMessageListener("SPProcessCrashService", this._messageListener); + sendSyncMessage("SPProcessCrashService", { op: "register-observer" }); +}; - addChromeEventListener: function(type, listener, capture, allowUntrusted) { - addEventListener(type, listener, capture, allowUntrusted); - }, - removeChromeEventListener: function(type, listener, capture) { - removeEventListener(type, listener, capture); - }, +SpecialPowers.prototype.unregisterProcessCrashObservers = function() { + addMessageListener("SPProcessCrashService", this._messageListener); + sendSyncMessage("SPProcessCrashService", { op: "unregister-observer" }); +}; - 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; - }, - - createSystemXHR: function() { - return Cc["@mozilla.org/xmlextras/xmlhttprequest;1"] - .createInstance(Ci.nsIXMLHttpRequest); - }, - - gc: function() { - this.DOMWindowUtils.garbageCollect(); - }, - - hasContentProcesses: function() { - try { - var rt = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime); - return rt.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; - } catch (e) { - return true; - } - }, - - registerProcessCrashObservers: function() { - addMessageListener("SPProcessCrashService", this._messageListener); - sendSyncMessage("SPProcessCrashService", { op: "register-observer" }); - }, - - _messageReceived: function(aMessage) { - switch (aMessage.name) { - case "SPProcessCrashService": - if (aMessage.json.type == "crash-observed") { - var self = this; - aMessage.json.dumpIDs.forEach(function(id) { - self._encounteredCrashDumpFiles.push(id + ".dmp"); - self._encounteredCrashDumpFiles.push(id + ".extra"); - }); +SpecialPowers.prototype._messageReceived = function(aMessage) { + switch (aMessage.name) { + case "SPProcessCrashService": + if (aMessage.json.type == "crash-observed") { + for (let e of aMessage.json.dumpIDs) { + this._encounteredCrashDumpFiles.push(e.id + "." + e.extension); } - break; + } + break; - case "SPPingService": - if (aMessage.json.op == "pong") { - var handler = this._pongHandlers.shift(); - if (handler) { - handler(); - } + case "SPPingService": + if (aMessage.json.op == "pong") { + var handler = this._pongHandlers.shift(); + if (handler) { + handler(); } - break; - } - return true; - }, - - removeExpectedCrashDumpFiles: function(aExpectingProcessCrash) { - var success = true; - if (aExpectingProcessCrash) { - var message = { - op: "delete-crash-dump-files", - filenames: this._encounteredCrashDumpFiles - }; - if (!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 = sendSyncMessage("SPProcessCrashService", message)[0]; - crashDumpFiles.forEach(function(aFilename) { - self._unexpectedCrashDumpFiles[aFilename] = true; - }); - return crashDumpFiles; - }, - - executeAfterFlushingMessageQueue: function(aCallback) { - this._pongHandlers.push(aCallback); - sendAsyncMessage("SPPingService", { op: "ping" }); - }, - - executeSoon: function(aFunc) { - var tm = Cc["@mozilla.org/thread-manager;1"].getService(Ci.nsIThreadManager); - tm.mainThread.dispatch({ - run: function() { - aFunc(); - } - }, Ci.nsIThread.DISPATCH_NORMAL); - }, - - /* from http://mxr.mozilla.org/mozilla-central/source/testing/mochitest/tests/SimpleTest/quit.js - * by Bob Clary, Jeff Walden, and Robert Sayre. - */ - quitApplication: function() { - function canQuitApplication() - { - var os = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService); - if (!os) - return true; - - try { - var cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool); - os.notifyObservers(cancelQuit, "quit-application-requested", null); - - // Something aborted the quit process. - if (cancelQuit.data) - return false; - } catch (ex) {} - return true; - } - - if (!canQuitApplication()) - return false; - - var appService = Cc['@mozilla.org/toolkit/app-startup;1'].getService(Ci.nsIAppStartup); - appService.quit(Ci.nsIAppStartup.eForceQuit); - return true; + break; } + return true; +}; + +SpecialPowers.prototype.quit = function() { + sendAsyncMessage("SpecialPowers.Quit", {}); +}; + +SpecialPowers.prototype.executeAfterFlushingMessageQueue = function(aCallback) { + this._pongHandlers.push(aCallback); + sendAsyncMessage("SPPingService", { op: "ping" }); }; // Expose everything but internal APIs (starting with underscores) to -// web content. +// web content. We cannot use Object.keys to view SpecialPowers.prototype since +// we are using the functions from SpecialPowersAPI.prototype SpecialPowers.prototype.__exposedProps__ = {}; -for each (i in Object.keys(SpecialPowers.prototype).filter(function(v) {return v.charAt(0) != "_";})) { - SpecialPowers.prototype.__exposedProps__[i] = "r"; +for (var i in SpecialPowers.prototype) { + if (i.charAt(0) != "_") + SpecialPowers.prototype.__exposedProps__[i] = "r"; } // Attach our API to the window. @@ -355,18 +124,11 @@ function SpecialPowersManager() { SpecialPowersManager.prototype = { handleEvent: function handleEvent(aEvent) { var window = aEvent.target.defaultView; - - // Need to make sure we are called on what we care about - - // content windows. DOMWindowCreated is called on *all* HTMLDocuments, - // some of which belong to chrome windows or other special content. - // - var uri = window.document.documentURIObject; - if (uri.scheme === "chrome" || uri.spec.split(":")[0] == "about") { - return; - } - attachSpecialPowersToWindow(window); } }; var specialpowersmanager = new SpecialPowersManager(); + +this.SpecialPowers = SpecialPowers; +this.attachSpecialPowersToWindow = attachSpecialPowersToWindow; diff --git a/test/resources/firefox/extensions/special-powers@mozilla.org/chrome/specialpowers/content/specialpowersAPI.js b/test/resources/firefox/extensions/special-powers@mozilla.org/chrome/specialpowers/content/specialpowersAPI.js new file mode 100644 index 000000000..940bb01a8 --- /dev/null +++ b/test/resources/firefox/extensions/special-powers@mozilla.org/chrome/specialpowers/content/specialpowersAPI.js @@ -0,0 +1,1744 @@ +/* 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 + */ + +var Ci = Components.interfaces; +var Cc = Components.classes; +var Cu = Components.utils; + +Cu.import("resource://specialpowers/MockFilePicker.jsm"); +Cu.import("resource://specialpowers/MockColorPicker.jsm"); +Cu.import("resource://specialpowers/MockPermissionPrompt.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +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._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 isXrayWrapper(x) { + return Cu.isXrayWrapper(x); +} + +function callGetOwnPropertyDescriptor(obj, name) { + // Quickstubbed getters and setters are propertyOps, and don't get reified + // until someone calls __lookupGetter__ or __lookupSetter__ on them (note + // that there are special version of those functions for quickstubs, so + // apply()ing Object.prototype.__lookupGetter__ isn't good enough). Try to + // trigger reification before calling Object.getOwnPropertyDescriptor. + // + // See bug 764315. + try { + obj.__lookupGetter__(name); + obj.__lookupSetter__(name); + } catch(e) { } + return Object.getOwnPropertyDescriptor(obj, name); +} + +// We can't call apply() directy on Xray-wrapped functions, so we have to be +// clever. +function doApply(fun, invocant, args) { + return Function.prototype.apply.call(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!"; + + // Make our core wrapper object. + var handler = new SpecialPowersHandler(obj); + + // If the object is callable, make a function proxy. + if (typeof obj === "function") { + var callTrap = function() { + // The invocant and arguments may or may not be wrappers. Unwrap them if necessary. + var invocant = unwrapIfWrapped(this); + var unwrappedArgs = Array.prototype.slice.call(arguments).map(unwrapIfWrapped); + + try { + return wrapPrivileged(doApply(obj, invocant, unwrappedArgs)); + } catch (e) { + // Wrap exceptions and re-throw them. + throw wrapIfUnwrapped(e); + } + }; + var constructTrap = function() { + // The arguments may or may not be wrappers. Unwrap them if necessary. + var unwrappedArgs = Array.prototype.slice.call(arguments).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 wrapPrivileged(new obj(...unwrappedArgs)); + } catch (e) { + throw wrapIfUnwrapped(e); + } + }; + + return Proxy.createFunction(handler, callTrap, constructTrap); + } + + // Otherwise, just make a regular object proxy. + return Proxy.create(handler); +}; + +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!"; + + // Unwrap. + return x.SpecialPowers_wrappedObject; +}; + +function crawlProtoChain(obj, fn) { + var rv = fn(obj); + if (rv !== undefined) + return rv; + if (Object.getPrototypeOf(obj)) + return crawlProtoChain(Object.getPrototypeOf(obj), fn); +}; + +/* + * We want to waive the __exposedProps__ security check for SpecialPowers-wrapped + * objects. We do this by creating a proxy singleton that just always returns 'rw' + * for any property name. + */ +function ExposedPropsWaiverHandler() { + // NB: XPConnect denies access if the relevant member of __exposedProps__ is not + // enumerable. + var _permit = { value: 'rw', writable: false, configurable: false, enumerable: true }; + return { + getOwnPropertyDescriptor: function(name) { return _permit; }, + getPropertyDescriptor: function(name) { return _permit; }, + getOwnPropertyNames: function() { throw Error("Can't enumerate ExposedPropsWaiver"); }, + getPropertyNames: function() { throw Error("Can't enumerate ExposedPropsWaiver"); }, + enumerate: function() { throw Error("Can't enumerate ExposedPropsWaiver"); }, + defineProperty: function(name) { throw Error("Can't define props on ExposedPropsWaiver"); }, + delete: function(name) { throw Error("Can't delete props from ExposedPropsWaiver"); } + }; +}; +ExposedPropsWaiver = Proxy.create(ExposedPropsWaiverHandler()); + +function SpecialPowersHandler(obj) { + this.wrappedObject = obj; +}; + +// Allow us to transitively maintain the membrane by wrapping descriptors +// we return. +SpecialPowersHandler.prototype.doGetPropertyDescriptor = function(name, own) { + + // Handle our special API. + if (name == "SpecialPowers_wrappedObject") + return { value: this.wrappedObject, writeable: false, configurable: false, enumerable: false }; + + // Handle __exposedProps__. + if (name == "__exposedProps__") + return { value: ExposedPropsWaiver, writable: false, configurable: false, enumerable: false }; + + // 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, 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. + var obj = name == 'toString' ? XPCNativeWrapper.unwrap(this.wrappedObject) + : this.wrappedObject; + + // + // Call through to the wrapped object. + // + // Note that we have several cases here, each of which requires special handling. + // + var desc; + + // Case 1: Own Properties. + // + // This one is easy, thanks to Object.getOwnPropertyDescriptor(). + if (own) + desc = callGetOwnPropertyDescriptor(obj, name); + + // Case 2: Not own, not Xray-wrapped. + // + // Here, we can just crawl the prototype chain, calling + // Object.getOwnPropertyDescriptor until we find what we want. + // + // NB: Make sure to check this.wrappedObject here, rather than obj, because + // we may have waived Xray on obj above. + else if (!isXrayWrapper(this.wrappedObject)) + desc = crawlProtoChain(obj, function(o) {return callGetOwnPropertyDescriptor(o, name);}); + + // Case 3: Not own, Xray-wrapped. + // + // This one is harder, because we Xray wrappers are flattened and don't have + // a prototype. Xray wrappers are proxies themselves, so we'd love to just call + // through to XrayWrapper::getPropertyDescriptor(). Unfortunately though, + // we don't have any way to do that. :-( + // + // So we first try with a call to getOwnPropertyDescriptor(). If that fails, + // we make up a descriptor, using some assumptions about what kinds of things + // tend to live on the prototypes of Xray-wrapped objects. + else { + desc = Object.getOwnPropertyDescriptor(obj, name); + if (!desc) { + var getter = Object.prototype.__lookupGetter__.call(obj, name); + var setter = Object.prototype.__lookupSetter__.call(obj, name); + if (getter || setter) + desc = {get: getter, set: setter, configurable: true, enumerable: true}; + else if (name in obj) + desc = {value: obj[name], writable: false, configurable: true, enumerable: true}; + } + } + + // Bail if we've got nothing. + if (typeof desc === 'undefined') + return undefined; + + // When accessors are implemented as JSPropertyOps rather than JSNatives (ie, + // QuickStubs), the js engine does the wrong thing and treats it as a value + // descriptor rather than an accessor descriptor. Jorendorff suggested this + // little hack to work around it. See bug 520882. + if (desc && 'value' in desc && desc.value === undefined) + desc.value = obj[name]; + + // A trapping proxy's properties must always be configurable, but sometimes + // this we get non-configurable properties from Object.getOwnPropertyDescriptor(). + // Tell a white lie. + desc.configurable = true; + + // Transitively maintain the wrapper membrane. + function wrapIfExists(key) { if (key in desc) desc[key] = wrapPrivileged(desc[key]); }; + wrapIfExists('value'); + wrapIfExists('get'); + wrapIfExists('set'); + + return desc; +}; + +SpecialPowersHandler.prototype.getOwnPropertyDescriptor = function(name) { + return this.doGetPropertyDescriptor(name, true); +}; + +SpecialPowersHandler.prototype.getPropertyDescriptor = function(name) { + return this.doGetPropertyDescriptor(name, false); +}; + +function doGetOwnPropertyNames(obj, props) { + + // Insert our special API. It's not enumerable, but getPropertyNames() + // includes non-enumerable properties. + var specialAPI = 'SpecialPowers_wrappedObject'; + if (props.indexOf(specialAPI) == -1) + props.push(specialAPI); + + // Do the normal thing. + var flt = function(a) { return props.indexOf(a) == -1; }; + props = props.concat(Object.getOwnPropertyNames(obj).filter(flt)); + + // If we've got an Xray wrapper, include the expandos as well. + if ('wrappedJSObject' in obj) + props = props.concat(Object.getOwnPropertyNames(obj.wrappedJSObject) + .filter(flt)); + + return props; +} + +SpecialPowersHandler.prototype.getOwnPropertyNames = function() { + return doGetOwnPropertyNames(this.wrappedObject, []); +}; + +SpecialPowersHandler.prototype.getPropertyNames = function() { + + // Manually walk the prototype chain, making sure to add only property names + // that haven't been overridden. + // + // There's some trickiness here with Xray wrappers. Xray wrappers don't have + // a prototype, so we need to unwrap them if we want to get all of the names + // with Object.getOwnPropertyNames(). But we don't really want to unwrap the + // base object, because that will include expandos that are inaccessible via + // our implementation of get{,Own}PropertyDescriptor(). So we unwrap just + // before accessing the prototype. This ensures that we get Xray vision on + // the base object, and no Xray vision for the rest of the way up. + var obj = this.wrappedObject; + var props = []; + while (obj) { + props = doGetOwnPropertyNames(obj, props); + obj = Object.getPrototypeOf(XPCNativeWrapper.unwrap(obj)); + } + return props; +}; + +SpecialPowersHandler.prototype.defineProperty = function(name, desc) { + return Object.defineProperty(this.wrappedObject, name, desc); +}; + +SpecialPowersHandler.prototype.delete = function(name) { + return delete this.wrappedObject[name]; +}; + +SpecialPowersHandler.prototype.fix = function() { return undefined; /* Throws a TypeError. */ }; + +// Per the ES5 spec this is a derived trap, but it's fundamental in spidermonkey +// for some reason. See bug 665198. +SpecialPowersHandler.prototype.enumerate = function() { + var t = this; + var filt = function(name) { return t.getPropertyDescriptor(name).enumerable; }; + return this.getPropertyNames().filter(filt); +}; + +// 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); + } + + // expose all props of 'm' as read-only + let expose = {}; + for (let prop in m) + expose[prop] = 'r'; + m.__exposedProps__ = expose; + 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() { + args = Array.prototype.map.call(arguments, wrapIfUnwrapped); + return cb.apply(this, args); + } +} + +function wrapCallbackObject(obj) { + wrapper = { __exposedProps__: ExposedPropsWaiver }; + for (var i in obj) { + if (typeof obj[i] == 'function') + wrapper[i] = wrapCallback(obj[i]); + else + wrapper[i] = obj[i]; + } + return wrapper; +} + +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, + + /* + * Create blank privileged objects to use as out-params for privileged functions. + */ + createBlankObject: function () { + var obj = new Object; + obj.__exposedProps__ = ExposedPropsWaiver; + return obj; + }, + + /* + * 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 + }, + + loadChromeScript: function (url) { + // 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 + this._sendSyncMessage("SPLoadChromeScript", + { url: url, id: id }); + + // 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 }); + }, + + 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 }); + }, + + 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(this.wrap(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 {url, 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 = function (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, url, diagnostic].join(" | "); + if (parentRunner) { + if (err) { + parentRunner.addFailedTest(url); + 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); + }, + + 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() { + return typeof this.Components.classes == 'object' ? this.Components + : 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.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; + }, + + _delayCallbackTwice: function(callback) { + function delayedCallback() { + function delayAgain() { + content.window.setTimeout(callback, 0); + } + content.window.setTimeout(delayAgain, 0); + } + 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) { + var pendingPermissions = []; + var cleanupPermissions = []; + + for (var p in inPermissions) { + var permission = inPermissions[p]; + var originalValue = Ci.nsIPermissionManager.UNKNOWN_ACTION; + if (this.testPermission(permission.type, Ci.nsIPermissionManager.ALLOW_ACTION, permission.context)) { + originalValue = Ci.nsIPermissionManager.ALLOW_ACTION; + } else if (this.testPermission(permission.type, Ci.nsIPermissionManager.DENY_ACTION, permission.context)) { + originalValue = Ci.nsIPermissionManager.DENY_ACTION; + } else if (this.testPermission(permission.type, Ci.nsIPermissionManager.PROMPT_ACTION, permission.context)) { + originalValue = Ci.nsIPermissionManager.PROMPT_ACTION; + } else if (this.testPermission(permission.type, Ci.nsICookiePermission.ACCESS_SESSION, permission.context)) { + originalValue = Ci.nsICookiePermission.ACCESS_SESSION; + } else if (this.testPermission(permission.type, Ci.nsICookiePermission.ACCESS_ALLOW_FIRST_PARTY_ONLY, permission.context)) { + originalValue = Ci.nsICookiePermission.ACCESS_ALLOW_FIRST_PARTY_ONLY; + } else if (this.testPermission(permission.type, Ci.nsICookiePermission.ACCESS_LIMIT_THIRD_PARTY, permission.context)) { + originalValue = Ci.nsICookiePermission.ACCESS_LIMIT_THIRD_PARTY; + } + + let [url, appId, isInBrowserElement] = this._getInfoFromPermissionArg(permission.context); + + 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, 'url': url, 'appId': appId, 'isInBrowserElement': isInBrowserElement}; + if (permission.remove == true) + todo.op = 'remove'; + + pendingPermissions.push(todo); + + /* Push original permissions value or clear into cleanup array */ + var cleanupTodo = {'op': 'add', 'type': permission.type, 'permission': perm, 'value': perm, 'url': url, 'appId': appId, 'isInBrowserElement': isInBrowserElement}; + 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. + this._permissionsUndoStack.push(cleanupPermissions); + this._pendingPermissions.push([pendingPermissions, + this._delayCallbackTwice(callback)]); + this._applyPermissions(); + } else { + content.window.setTimeout(callback, 0); + } + }, + + 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/isInBrowserElement} or null */ + this._pendingPermissions.push([this._permissionsUndoStack.pop(), cb]); + this._applyPermissions(); + } else { + content.window.setTimeout(callback, 0); + } + }, + + flushPermissions: function(callback) { + while (this._permissionsUndoStack.length > 1) + this.popPermissions(null); + + this.popPermissions(callback); + }, + + + _permissionObserver: { + _lastPermission: {}, + _callBack: null, + _nextCallback: null, + + observe: function (aSubject, aTopic, aData) + { + if (aTopic == "perm-changed") { + var permission = aSubject.QueryInterface(Ci.nsIPermission); + if (permission.type == this._lastPermission.type) { + var os = Components.classes["@mozilla.org/observer-service;1"] + .getService(Components.interfaces.nsIObserverService); + os.removeObserver(this, "perm-changed"); + content.window.setTimeout(this._callback, 0); + content.window.setTimeout(this._nextCallback, 0); + } + } + } + }, + + /* + 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; + var os = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService); + 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(); + } + + os.addObserver(this._permissionObserver, "perm-changed", false); + + for (var idx in pendingActions) { + var perm = pendingActions[idx]; + this._sendSyncMessage('SPPermissionManager', perm)[0]; + } + }, + + /* + * Take in a list of pref changes to make, and invoke |callback| 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) { + var prefs = Components.classes["@mozilla.org/preferences-service;1"]. + getService(Components.interfaces.nsIPrefBranch); + + 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); + } + } + + 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(callback)]); + this._applyPrefs(); + } else { + content.window.setTimeout(callback, 0); + } + }, + + popPrefEnv: function(callback) { + if (this._prefEnvUndoStack.length > 0) { + // See pushPrefEnv comment regarding delay. + let cb = callback ? this._delayCallbackTwice(callback) : null; + /* Each pop will have a valid block of preferences */ + this._pendingPrefs.push([this._prefEnvUndoStack.pop(), cb]); + this._applyPrefs(); + } else { + content.window.setTimeout(callback, 0); + } + }, + + flushPrefEnv: function(callback) { + while (this._prefEnvUndoStack.length > 1) + this.popPrefEnv(null); + + this.popPrefEnv(callback); + }, + + /* + 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 = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch); + var self = this; + pb.addObserver(lastPref.name, function prefObs(subject, topic, data) { + pb.removeObserver(lastPref.name, prefObs); + + content.window.setTimeout(callback, 0); + content.window.setTimeout(function () { + self._applyingPrefs = false; + // Now apply any prefs that may have been queued while we were applying + self._applyPrefs(); + }, 0); + }, 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); + } + } + }, + + // Disables the app install prompt for the duration of this test. There is + // no need to re-enable the prompt at the end of the test. + // + // The provided callback is invoked once the prompt is disabled. + autoConfirmAppInstall: function(cb) { + this.pushPrefEnv({set: [['dom.mozApps.auto_confirm_install', true]]}, cb); + }, + + // Allow tests to disable the per platform app validity checks so we can + // test higher level WebApp functionality without full platform support. + setAllAppsLaunchable: function(launchable) { + var message = { + op: "set-launchable", + launchable: launchable + }; + return this._sendSyncMessage("SPWebAppService", message); + }, + + _proxiedObservers: { + "specialpowers-http-notify-request": function(aMessage) { + let uri = aMessage.json.uri; + Services.obs.notifyObservers(null, "specialpowers-http-notify-request", uri); + }, + }, + + _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); + if (typeof obs == 'object' && obs.observe.name != 'SpecialPowersCallbackWrapper') + obs.observe = wrapCallback(obs.observe); + var obsvc = Cc['@mozilla.org/observer-service;1'] + .getService(Ci.nsIObserverService); + obsvc.addObserver(obs, notification, weak); + }, + removeObserver: function(obs, notification) { + this._removeObserverProxy(notification); + var obsvc = Cc['@mozilla.org/observer-service;1'] + .getService(Ci.nsIObserverService); + obsvc.removeObserver(obs, notification); + }, + notifyObservers: function(subject, topic, data) { + var obsvc = Cc['@mozilla.org/observer-service;1'] + .getService(Ci.nsIObserverService); + obsvc.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 content/base/test/test_object.html because + // and 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"; + 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 + .QueryInterface(Ci.nsIMarkupDocumentViewer); + }, + //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(); + }, + + getMaxLineBoxWidth: function(window) { + return this._getMUDV(window).maxLineBoxWidth; + }, + + setMaxLineBoxWidth: function(window, width) { + this._getMUDV(window).changeMaxLineBoxWidth(width); + }, + + 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; + }, + + 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(); + }, + + // 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(win, callback) { + var self = this; + let count = 0; + + function doPreciseGCandCC() { + function scheduledGCCallback() { + self.getDOMWindowUtils(win).cycleCollect(); + + if (++count < 2) { + doPreciseGCandCC(); + } else { + callback(); + } + } + + Cu.schedulePreciseGC(scheduledGCCallback); + } + + doPreciseGCandCC(); + }, + + 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; + }, + + 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); + }, + + getDOMRequestService: function() { + var serv = Cc["@mozilla.org/dom/dom-request-service;1"]. + getService(Ci.nsIDOMRequestService); + var res = { __exposedProps__: {} }; + var props = ["createRequest", "createCursor", "fireError", "fireSuccess", + "fireDone", "fireDetailedError"]; + for (i in props) { + let prop = props[i]; + res[prop] = function() { return serv[prop].apply(serv, arguments) }; + res.__exposedProps__[prop] = "r"; + } + 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); + }, + + copyString: function(str, doc) { + Components.classes["@mozilla.org/widget/clipboardhelper;1"]. + getService(Components.interfaces.nsIClipboardHelper). + copyString(str, doc); + }, + + openDialog: function(win, args) { + return win.openDialog.apply(win, args); + }, + + // :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, childTargetWindow) { + return this.focusManager.getFocusedElementForWindow(targetWindow, aDeep, childTargetWindow); + }, + + 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(); + 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); + xferable.init(this._getDocShell(content.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(preExpectedVal, doc) { + var cbHelperSvc = Components.classes["@mozilla.org/widget/clipboardhelper;1"]. + getService(Components.interfaces.nsIClipboardHelper); + cbHelperSvc.copyString(preExpectedVal, doc); + }, + + 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) { + 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 this.isDebugBuild; + var debug = Cc["@mozilla.org/xpcom/debug;1"].getService(Ci.nsIDebug2); + return this.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