From da522d42d4f09e5914fa16f1c0cbb079bcd5f4cf Mon Sep 17 00:00:00 2001 From: Brendan Dahl <brendan.dahl@gmail.com> Date: Fri, 11 Apr 2014 10:40:15 -0700 Subject: [PATCH] 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<Base>::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 + // <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"; + 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 <iframe mozbrowser>. + */ + getBrowserFrameMessageManager: function(aFrameElement) { + return this.wrap(aFrameElement.QueryInterface(Ci.nsIFrameLoaderOwner) + .frameLoader + .messageManager); + }, + + setFullscreenAllowed: function(document) { + var pm = Cc["@mozilla.org/permissionmanager;1"].getService(Ci.nsIPermissionManager); + pm.addFromPrincipal(document.nodePrincipal, "fullscreen", Ci.nsIPermissionManager.ALLOW_ACTION); + var obsvc = Cc['@mozilla.org/observer-service;1'] + .getService(Ci.nsIObserverService); + obsvc.notifyObservers(document, "fullscreen-approved", null); + }, + + removeFullscreenAllowed: function(document) { + var pm = Cc["@mozilla.org/permissionmanager;1"].getService(Ci.nsIPermissionManager); + pm.removeFromPrincipal(document.nodePrincipal, "fullscreen"); + }, + + _getInfoFromPermissionArg: function(arg) { + let url = ""; + let appId = Ci.nsIScriptSecurityManager.NO_APP_ID; + let isInBrowserElement = false; + + if (typeof(arg) == "string") { + // It's an URL. + url = Cc["@mozilla.org/network/io-service;1"] + .getService(Ci.nsIIOService) + .newURI(arg, null, null) + .spec; + } 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!"; + } + + appId = appsSvc.getAppLocalIdByManifestURL(arg.manifestURL); + url = app.origin; + isInBrowserElement = arg.isInBrowserElement || false; + } else if (arg.nodePrincipal) { + // It's a document. + url = arg.nodePrincipal.URI.spec; + appId = arg.nodePrincipal.appId; + isInBrowserElement = arg.nodePrincipal.isInBrowserElement; + } else { + url = arg.url; + appId = arg.appId; + isInBrowserElement = arg.isInBrowserElement; + } + + return [ url, appId, isInBrowserElement ]; + }, + + addPermission: function(type, allow, arg) { + let [url, appId, isInBrowserElement] = this._getInfoFromPermissionArg(arg); + + 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, + 'url': url, + 'appId': appId, + 'isInBrowserElement': isInBrowserElement + }; + + this._sendSyncMessage('SPPermissionManager', msg); + }, + + removePermission: function(type, arg) { + let [url, appId, isInBrowserElement] = this._getInfoFromPermissionArg(arg); + + var msg = { + 'op': 'remove', + 'type': type, + 'url': url, + 'appId': appId, + 'isInBrowserElement': isInBrowserElement + }; + + this._sendSyncMessage('SPPermissionManager', msg); + }, + + hasPermission: function (type, arg) { + let [url, appId, isInBrowserElement] = this._getInfoFromPermissionArg(arg); + + var msg = { + 'op': 'has', + 'type': type, + 'url': url, + 'appId': appId, + 'isInBrowserElement': isInBrowserElement + }; + + return this._sendSyncMessage('SPPermissionManager', msg)[0]; + }, + testPermission: function (type, value, arg) { + let [url, appId, isInBrowserElement] = this._getInfoFromPermissionArg(arg); + + var msg = { + 'op': 'test', + 'type': type, + 'value': value, + 'url': url, + 'appId': appId, + 'isInBrowserElement': isInBrowserElement + }; + return this._sendSyncMessage('SPPermissionManager', msg)[0]; + }, + + getMozFullPath: function(file) { + return file.mozFullPath; + }, + + isWindowPrivate: function(win) { + return PrivateBrowsingUtils.isWindowPrivate(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); + }, +}; + +this.SpecialPowersAPI = SpecialPowersAPI; +this.bindDOMWindowUtils = bindDOMWindowUtils; +this.getRawComponents = getRawComponents; diff --git a/test/resources/firefox/extensions/special-powers@mozilla.org/chrome/specialpowers/modules/Assert.jsm b/test/resources/firefox/extensions/special-powers@mozilla.org/chrome/specialpowers/modules/Assert.jsm new file mode 100644 index 000000000..a80c407b7 --- /dev/null +++ b/test/resources/firefox/extensions/special-powers@mozilla.org/chrome/specialpowers/modules/Assert.jsm @@ -0,0 +1,442 @@ +/* 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/. */ + +// http://wiki.commonjs.org/wiki/Unit_Testing/1.0 +// When you see a javadoc comment that contains a number, it's a reference to a +// specific section of the CommonJS spec. +// +// Originally from narwhal.js (http://narwhaljs.org) +// Copyright (c) 2009 Thomas Robinson <280north.com> +// MIT license: http://opensource.org/licenses/MIT + +"use strict"; + +this.EXPORTED_SYMBOLS = [ + "Assert" +]; + +/** + * 1. The assert module provides functions that throw AssertionError's when + * particular conditions are not met. + * + * To use the module you'll need to instantiate it first, which allows consumers + * to override certain behavior on the newly obtained instance. For examples, + * see the javadoc comments for the `report` member function. + */ +let Assert = this.Assert = function(reporterFunc) { + if (reporterFunc) + this.setReporter(reporterFunc); +}; + +function instanceOf(object, type) { + return Object.prototype.toString.call(object) == "[object " + type + "]"; +} + +function replacer(key, value) { + if (value === undefined) { + return "" + value; + } + if (typeof value === "number" && (isNaN(value) || !isFinite(value))) { + return value.toString(); + } + if (typeof value === "function" || instanceOf(value, "RegExp")) { + return value.toString(); + } + return value; +} + +const kTruncateLength = 128; + +function truncate(text, newLength = kTruncateLength) { + if (typeof text == "string") { + return text.length < newLength ? text : text.slice(0, newLength); + } else { + return text; + } +} + +function getMessage(error, prefix = "") { + let actual, expected; + // Wrap calls to JSON.stringify in try...catch blocks, as they may throw. If + // so, fall back to toString(). + try { + actual = JSON.stringify(error.actual, replacer); + } catch (ex) { + actual = Object.prototype.toString.call(error.actual); + } + try { + expected = JSON.stringify(error.expected, replacer); + } catch (ex) { + expected = Object.prototype.toString.call(error.expected); + } + let message = prefix; + if (error.operator) { + message += (prefix ? " - " : "") + truncate(actual) + " " + error.operator + + " " + truncate(expected); + } + return message; +} + +/** + * 2. The AssertionError is defined in assert. + * + * Example: + * new assert.AssertionError({ + * message: message, + * actual: actual, + * expected: expected, + * operator: operator + * }); + * + * At present only the four keys mentioned above are used and + * understood by the spec. Implementations or sub modules can pass + * other keys to the AssertionError's constructor - they will be + * ignored. + */ +Assert.AssertionError = function(options) { + this.name = "AssertionError"; + this.actual = options.actual; + this.expected = options.expected; + this.operator = options.operator; + this.message = getMessage(this, options.message); + // The part of the stack that comes from this module is not interesting. + let stack = Components.stack; + do { + stack = stack.caller; + } while(stack.filename && stack.filename.contains("Assert.jsm")) + this.stack = stack; +}; + +// assert.AssertionError instanceof Error +Assert.AssertionError.prototype = Object.create(Error.prototype, { + constructor: { + value: Assert.AssertionError, + enumerable: false, + writable: true, + configurable: true + } +}); + +let proto = Assert.prototype; + +proto._reporter = null; +/** + * Set a custom assertion report handler function. Arguments passed in to this + * function are: + * err (AssertionError|null) An error object when the assertion failed or null + * when it passed + * message (string) Message describing the assertion + * stack (stack) Stack trace of the assertion function + * + * Example: + * ```js + * Assert.setReporter(function customReporter(err, message, stack) { + * if (err) { + * do_report_result(false, err.message, err.stack); + * } else { + * do_report_result(true, message, stack); + * } + * }); + * ``` + * + * @param reporterFunc + * (function) Report handler function + */ +proto.setReporter = function(reporterFunc) { + this._reporter = reporterFunc; +}; + +/** + * 3. All of the following functions must throw an AssertionError when a + * corresponding condition is not met, with a message that may be undefined if + * not provided. All assertion methods provide both the actual and expected + * values to the assertion error for display purposes. + * + * This report method only throws errors on assertion failures, as per spec, + * but consumers of this module (think: xpcshell-test, mochitest) may want to + * override this default implementation. + * + * Example: + * ```js + * // The following will report an assertion failure. + * this.report(1 != 2, 1, 2, "testing JS number math!", "=="); + * ``` + * + * @param failed + * (boolean) Indicates if the assertion failed or not + * @param actual + * (mixed) The result of evaluating the assertion + * @param expected (optional) + * (mixed) Expected result from the test author + * @param message (optional) + * (string) Short explanation of the expected result + * @param operator (optional) + * (string) Operation qualifier used by the assertion method (ex: '==') + */ +proto.report = function(failed, actual, expected, message, operator) { + let err = new Assert.AssertionError({ + message: message, + actual: actual, + expected: expected, + operator: operator + }); + if (!this._reporter) { + // If no custom reporter is set, throw the error. + if (failed) { + throw err; + } + } else { + this._reporter(failed ? err : null, message, err.stack); + } +}; + +/** + * 4. Pure assertion tests whether a value is truthy, as determined by !!guard. + * assert.ok(guard, message_opt); + * This statement is equivalent to assert.equal(true, !!guard, message_opt);. + * To test strictly for the value true, use assert.strictEqual(true, guard, + * message_opt);. + * + * @param value + * (mixed) Test subject to be evaluated as truthy + * @param message (optional) + * (string) Short explanation of the expected result + */ +proto.ok = function(value, message) { + this.report(!value, value, true, message, "=="); +}; + +/** + * 5. The equality assertion tests shallow, coercive equality with ==. + * assert.equal(actual, expected, message_opt); + * + * @param actual + * (mixed) Test subject to be evaluated as equivalent to `expected` + * @param expected + * (mixed) Test reference to evaluate against `actual` + * @param message (optional) + * (string) Short explanation of the expected result + */ +proto.equal = function equal(actual, expected, message) { + this.report(actual != expected, actual, expected, message, "=="); +}; + +/** + * 6. The non-equality assertion tests for whether two objects are not equal + * with != assert.notEqual(actual, expected, message_opt); + * + * @param actual + * (mixed) Test subject to be evaluated as NOT equivalent to `expected` + * @param expected + * (mixed) Test reference to evaluate against `actual` + * @param message (optional) + * (string) Short explanation of the expected result + */ +proto.notEqual = function notEqual(actual, expected, message) { + this.report(actual == expected, actual, expected, message, "!="); +}; + +/** + * 7. The equivalence assertion tests a deep equality relation. + * assert.deepEqual(actual, expected, message_opt); + * + * We check using the most exact approximation of equality between two objects + * to keep the chance of false positives to a minimum. + * `JSON.stringify` is not designed to be used for this purpose; objects may + * have ambiguous `toJSON()` implementations that would influence the test. + * + * @param actual + * (mixed) Test subject to be evaluated as equivalent to `expected`, including nested properties + * @param expected + * (mixed) Test reference to evaluate against `actual` + * @param message (optional) + * (string) Short explanation of the expected result + */ +proto.deepEqual = function deepEqual(actual, expected, message) { + this.report(!_deepEqual(actual, expected), actual, expected, message, "deepEqual"); +}; + +function _deepEqual(actual, expected) { + // 7.1. All identical values are equivalent, as determined by ===. + if (actual === expected) { + return true; + // 7.2. If the expected value is a Date object, the actual value is + // equivalent if it is also a Date object that refers to the same time. + } else if (instanceOf(actual, "Date") && instanceOf(expected, "Date")) { + return actual.getTime() === expected.getTime(); + // 7.3 If the expected value is a RegExp object, the actual value is + // equivalent if it is also a RegExp object with the same source and + // properties (`global`, `multiline`, `lastIndex`, `ignoreCase`). + } else if (instanceOf(actual, "RegExp") && instanceOf(expected, "RegExp")) { + return actual.source === expected.source && + actual.global === expected.global && + actual.multiline === expected.multiline && + actual.lastIndex === expected.lastIndex && + actual.ignoreCase === expected.ignoreCase; + // 7.4. Other pairs that do not both pass typeof value == "object", + // equivalence is determined by ==. + } else if (typeof actual != "object" && typeof expected != "object") { + return actual == expected; + // 7.5 For all other Object pairs, including Array objects, equivalence is + // determined by having the same number of owned properties (as verified + // with Object.prototype.hasOwnProperty.call), the same set of keys + // (although not necessarily the same order), equivalent values for every + // corresponding key, and an identical 'prototype' property. Note: this + // accounts for both named and indexed properties on Arrays. + } else { + return objEquiv(actual, expected); + } +} + +function isUndefinedOrNull(value) { + return value === null || value === undefined; +} + +function isArguments(object) { + return instanceOf(object, "Arguments"); +} + +function objEquiv(a, b) { + if (isUndefinedOrNull(a) || isUndefinedOrNull(b)) { + return false; + } + // An identical 'prototype' property. + if (a.prototype !== b.prototype) { + return false; + } + // Object.keys may be broken through screwy arguments passing. Converting to + // an array solves the problem. + if (isArguments(a)) { + if (!isArguments(b)) { + return false; + } + a = pSlice.call(a); + b = pSlice.call(b); + return _deepEqual(a, b); + } + let ka, kb, key, i; + try { + ka = Object.keys(a); + kb = Object.keys(b); + } catch (e) { + // Happens when one is a string literal and the other isn't + return false; + } + // Having the same number of owned properties (keys incorporates + // hasOwnProperty) + if (ka.length != kb.length) + return false; + // The same set of keys (although not necessarily the same order), + ka.sort(); + kb.sort(); + // Equivalent values for every corresponding key, and possibly expensive deep + // test + for (i = ka.length - 1; i >= 0; i--) { + key = ka[i]; + if (!_deepEqual(a[key], b[key])) { + return false; + } + } + return true; +} + +/** + * 8. The non-equivalence assertion tests for any deep inequality. + * assert.notDeepEqual(actual, expected, message_opt); + * + * @param actual + * (mixed) Test subject to be evaluated as NOT equivalent to `expected`, including nested properties + * @param expected + * (mixed) Test reference to evaluate against `actual` + * @param message (optional) + * (string) Short explanation of the expected result + */ +proto.notDeepEqual = function notDeepEqual(actual, expected, message) { + this.report(_deepEqual(actual, expected), actual, expected, message, "notDeepEqual"); +}; + +/** + * 9. The strict equality assertion tests strict equality, as determined by ===. + * assert.strictEqual(actual, expected, message_opt); + * + * @param actual + * (mixed) Test subject to be evaluated as strictly equivalent to `expected` + * @param expected + * (mixed) Test reference to evaluate against `actual` + * @param message (optional) + * (string) Short explanation of the expected result + */ +proto.strictEqual = function strictEqual(actual, expected, message) { + this.report(actual !== expected, actual, expected, message, "==="); +}; + +/** + * 10. The strict non-equality assertion tests for strict inequality, as + * determined by !==. assert.notStrictEqual(actual, expected, message_opt); + * + * @param actual + * (mixed) Test subject to be evaluated as NOT strictly equivalent to `expected` + * @param expected + * (mixed) Test reference to evaluate against `actual` + * @param message (optional) + * (string) Short explanation of the expected result + */ +proto.notStrictEqual = function notStrictEqual(actual, expected, message) { + this.report(actual === expected, actual, expected, message, "!=="); +}; + +function expectedException(actual, expected) { + if (!actual || !expected) { + return false; + } + + if (instanceOf(expected, "RegExp")) { + return expected.test(actual); + } else if (actual instanceof expected) { + return true; + } else if (expected.call({}, actual) === true) { + return true; + } + + return false; +} + +/** + * 11. Expected to throw an error: + * assert.throws(block, Error_opt, message_opt); + * + * @param block + * (function) Function block to evaluate and catch eventual thrown errors + * @param expected (optional) + * (mixed) Test reference to evaluate against the thrown result from `block` + * @param message (optional) + * (string) Short explanation of the expected result + */ +proto.throws = function(block, expected, message) { + let actual; + + if (typeof expected === "string") { + message = expected; + expected = null; + } + + try { + block(); + } catch (e) { + actual = e; + } + + message = (expected && expected.name ? " (" + expected.name + ")." : ".") + + (message ? " " + message : "."); + + if (!actual) { + this.report(true, actual, expected, "Missing expected exception" + message); + } + + if ((actual && expected && !expectedException(actual, expected))) { + throw actual; + } + + this.report(false, expected, expected, message); +}; diff --git a/test/resources/firefox/extensions/special-powers@mozilla.org/chrome/specialpowers/modules/MockColorPicker.jsm b/test/resources/firefox/extensions/special-powers@mozilla.org/chrome/specialpowers/modules/MockColorPicker.jsm new file mode 100644 index 000000000..ab9f00b0c --- /dev/null +++ b/test/resources/firefox/extensions/special-powers@mozilla.org/chrome/specialpowers/modules/MockColorPicker.jsm @@ -0,0 +1,125 @@ +/* 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.EXPORTED_SYMBOLS = ["MockColorPicker"]; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cm = Components.manager; +const Cu = Components.utils; + +const CONTRACT_ID = "@mozilla.org/colorpicker;1"; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +var registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar); +var oldClassID = "", oldFactory = null; +var newClassID = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator).generateUUID(); +var newFactory = function (window) { + return { + createInstance: function(aOuter, aIID) { + if (aOuter) + throw Components.results.NS_ERROR_NO_AGGREGATION; + return new MockColorPickerInstance(window).QueryInterface(aIID); + }, + lockFactory: function(aLock) { + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIFactory]) + }; +} + +this.MockColorPicker = { + init: function(window) { + this.reset(); + this.factory = newFactory(window); + if (!registrar.isCIDRegistered(newClassID)) { + try { + oldClassID = registrar.contractIDToCID(CONTRACT_ID); + oldFactory = Cm.getClassObject(Cc[CONTRACT_ID], Ci.nsIFactory); + } catch(ex) { + oldClassID = ""; + oldFactory = null; + dump("TEST-INFO | can't get colorpicker registered component, " + + "assuming there is none"); + } + if (oldClassID != "" && oldFactory != null) { + registrar.unregisterFactory(oldClassID, oldFactory); + } + registrar.registerFactory(newClassID, "", CONTRACT_ID, this.factory); + } + }, + + reset: function() { + this.returnColor = ""; + this.showCallback = null; + this.shown = false; + this.showing = false; + }, + + cleanup: function() { + var previousFactory = this.factory; + this.reset(); + this.factory = null; + + registrar.unregisterFactory(newClassID, previousFactory); + if (oldClassID != "" && oldFactory != null) { + registrar.registerFactory(oldClassID, "", CONTRACT_ID, oldFactory); + } + } +}; + +function MockColorPickerInstance(window) { + this.window = window; +}; +MockColorPickerInstance.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIColorPicker]), + init: function(aParent, aTitle, aInitialColor) { + this.parent = aParent; + this.initialColor = aInitialColor; + }, + initialColor: "", + parent: null, + open: function(aColorPickerShownCallback) { + MockColorPicker.showing = true; + MockColorPicker.shown = true; + + this.window.setTimeout(function() { + let result = ""; + try { + if (typeof MockColorPicker.showCallback == "function") { + var updateCb = function(color) { + result = color; + aColorPickerShownCallback.update(color); + }; + let returnColor = MockColorPicker.showCallback(this, updateCb); + if (typeof returnColor === "string") { + result = returnColor; + } + } else if (typeof MockColorPicker.returnColor === "string") { + result = MockColorPicker.returnColor; + } + } catch(ex) { + dump("TEST-UNEXPECTED-FAIL | Exception in MockColorPicker.jsm open() " + + "method: " + ex + "\n"); + } + if (aColorPickerShownCallback) { + aColorPickerShownCallback.done(result); + } + }.bind(this), 0); + } +}; + +// Expose everything to content. We call reset() here so that all of the +// relevant lazy expandos get added. +MockColorPicker.reset(); +function exposeAll(obj) { + var props = {}; + for (var prop in obj) + props[prop] = 'rw'; + obj.__exposedProps__ = props; +} +exposeAll(MockColorPicker); +exposeAll(MockColorPickerInstance.prototype); diff --git a/test/resources/firefox/extensions/special-powers@mozilla.org/chrome/specialpowers/modules/MockFilePicker.jsm b/test/resources/firefox/extensions/special-powers@mozilla.org/chrome/specialpowers/modules/MockFilePicker.jsm new file mode 100644 index 000000000..f7b130e08 --- /dev/null +++ b/test/resources/firefox/extensions/special-powers@mozilla.org/chrome/specialpowers/modules/MockFilePicker.jsm @@ -0,0 +1,236 @@ +/* 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.EXPORTED_SYMBOLS = ["MockFilePicker"]; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cm = Components.manager; +const Cu = Components.utils; + +const CONTRACT_ID = "@mozilla.org/filepicker;1"; + +Cu.import("resource://gre/modules/FileUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +var registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar); +var oldClassID, oldFactory; +var newClassID = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator).generateUUID(); +var newFactory = function (window) { + return { + createInstance: function(aOuter, aIID) { + if (aOuter) + throw Components.results.NS_ERROR_NO_AGGREGATION; + return new MockFilePickerInstance(window).QueryInterface(aIID); + }, + lockFactory: function(aLock) { + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIFactory]) + }; +} + +this.MockFilePicker = { + returnOK: Ci.nsIFilePicker.returnOK, + returnCancel: Ci.nsIFilePicker.returnCancel, + returnReplace: Ci.nsIFilePicker.returnReplace, + + filterAll: Ci.nsIFilePicker.filterAll, + filterHTML: Ci.nsIFilePicker.filterHTML, + filterText: Ci.nsIFilePicker.filterText, + filterImages: Ci.nsIFilePicker.filterImages, + filterXML: Ci.nsIFilePicker.filterXML, + filterXUL: Ci.nsIFilePicker.filterXUL, + filterApps: Ci.nsIFilePicker.filterApps, + filterAllowURLs: Ci.nsIFilePicker.filterAllowURLs, + filterAudio: Ci.nsIFilePicker.filterAudio, + filterVideo: Ci.nsIFilePicker.filterVideo, + + window: null, + + init: function(window) { + this.window = window; + + this.reset(); + this.factory = newFactory(window); + if (!registrar.isCIDRegistered(newClassID)) { + oldClassID = registrar.contractIDToCID(CONTRACT_ID); + oldFactory = Cm.getClassObject(Cc[CONTRACT_ID], Ci.nsIFactory); + registrar.unregisterFactory(oldClassID, oldFactory); + registrar.registerFactory(newClassID, "", CONTRACT_ID, this.factory); + } + }, + + reset: function() { + this.appendFilterCallback = null; + this.appendFiltersCallback = null; + this.displayDirectory = null; + this.filterIndex = 0; + this.mode = null; + this.returnFiles = []; + this.returnValue = null; + this.showCallback = null; + this.shown = false; + this.showing = false; + }, + + cleanup: function() { + var previousFactory = this.factory; + this.reset(); + this.factory = null; + if (oldFactory) { + registrar.unregisterFactory(newClassID, previousFactory); + registrar.registerFactory(oldClassID, "", CONTRACT_ID, oldFactory); + } + }, + + useAnyFile: function() { + var file = FileUtils.getDir("TmpD", [], false); + file.append("testfile"); + file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0644); + this.returnFiles = [file]; + }, + + useBlobFile: function() { + var blob = new this.window.Blob([]); + var file = new this.window.File(blob, { name: 'helloworld.txt', type: 'plain/text' }); + this.returnFiles = [file]; + }, + + isNsIFile: function(aFile) { + let ret = false; + try { + if (aFile.QueryInterface(Ci.nsIFile)) + ret = true; + } catch(e) {} + + return ret; + } +}; + +function MockFilePickerInstance(window) { + this.window = window; +}; +MockFilePickerInstance.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIFilePicker]), + init: function(aParent, aTitle, aMode) { + MockFilePicker.mode = aMode; + this.filterIndex = MockFilePicker.filterIndex; + this.parent = aParent; + }, + appendFilter: function(aTitle, aFilter) { + if (typeof MockFilePicker.appendFilterCallback == "function") + MockFilePicker.appendFilterCallback(this, aTitle, aFilter); + }, + appendFilters: function(aFilterMask) { + if (typeof MockFilePicker.appendFiltersCallback == "function") + MockFilePicker.appendFiltersCallback(this, aFilterMask); + }, + defaultString: "", + defaultExtension: "", + parent: null, + filterIndex: 0, + displayDirectory: null, + get file() { + if (MockFilePicker.returnFiles.length >= 1 && + // window.File does not implement nsIFile + MockFilePicker.isNsIFile(MockFilePicker.returnFiles[0])) { + return MockFilePicker.returnFiles[0]; + } + + return null; + }, + get domfile() { + if (MockFilePicker.returnFiles.length >= 1) { + // window.File does not implement nsIFile + if (!MockFilePicker.isNsIFile(MockFilePicker.returnFiles[0])) { + return MockFilePicker.returnFiles[0]; + } + + let utils = this.parent.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + return utils.wrapDOMFile(MockFilePicker.returnFiles[0]); + } + return null; + }, + get fileURL() { + if (MockFilePicker.returnFiles.length >= 1 && + // window.File does not implement nsIFile + MockFilePicker.isNsIFile(MockFilePicker.returnFiles[0])) { + return Services.io.newFileURI(MockFilePicker.returnFiles[0]); + } + + return null; + }, + get files() { + return { + index: 0, + QueryInterface: XPCOMUtils.generateQI([Ci.nsISimpleEnumerator]), + hasMoreElements: function() { + return this.index < MockFilePicker.returnFiles.length; + }, + getNext: function() { + // window.File does not implement nsIFile + if (!MockFilePicker.isNsIFile(MockFilePicker.returnFiles[this.index])) { + return null; + } + return MockFilePicker.returnFiles[this.index++]; + } + }; + }, + get domfiles() { + let utils = this.parent.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + return { + index: 0, + QueryInterface: XPCOMUtils.generateQI([Ci.nsISimpleEnumerator]), + hasMoreElements: function() { + return this.index < MockFilePicker.returnFiles.length; + }, + getNext: function() { + // window.File does not implement nsIFile + if (!MockFilePicker.isNsIFile(MockFilePicker.returnFiles[this.index])) { + return MockFilePicker.returnFiles[this.index++]; + } + return utils.wrapDOMFile(MockFilePicker.returnFiles[this.index++]); + } + }; + }, + show: function() { + MockFilePicker.displayDirectory = this.displayDirectory; + MockFilePicker.shown = true; + if (typeof MockFilePicker.showCallback == "function") { + var returnValue = MockFilePicker.showCallback(this); + if (typeof returnValue != "undefined") + return returnValue; + } + return MockFilePicker.returnValue; + }, + open: function(aFilePickerShownCallback) { + MockFilePicker.showing = true; + this.window.setTimeout(function() { + let result = Components.interfaces.nsIFilePicker.returnCancel; + try { + result = this.show(); + } catch(ex) { + } + if (aFilePickerShownCallback) { + aFilePickerShownCallback.done(result); + } + }.bind(this), 0); + } +}; + +// Expose everything to content. We call reset() here so that all of the relevant +// lazy expandos get added. +MockFilePicker.reset(); +function exposeAll(obj) { + var props = {}; + for (var prop in obj) + props[prop] = 'rw'; + obj.__exposedProps__ = props; +} +exposeAll(MockFilePicker); +exposeAll(MockFilePickerInstance.prototype); diff --git a/test/resources/firefox/extensions/special-powers@mozilla.org/chrome/specialpowers/modules/MockPermissionPrompt.jsm b/test/resources/firefox/extensions/special-powers@mozilla.org/chrome/specialpowers/modules/MockPermissionPrompt.jsm new file mode 100644 index 000000000..e07f8e002 --- /dev/null +++ b/test/resources/firefox/extensions/special-powers@mozilla.org/chrome/specialpowers/modules/MockPermissionPrompt.jsm @@ -0,0 +1,97 @@ +/* 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.EXPORTED_SYMBOLS = ["MockPermissionPrompt"]; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cm = Components.manager; +const Cu = Components.utils; + +const CONTRACT_ID = "@mozilla.org/content-permission/prompt;1"; + +Cu.import("resource://gre/modules/FileUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +var registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar); +var oldClassID, oldFactory; +var newClassID = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator).generateUUID(); +var newFactory = { + createInstance: function(aOuter, aIID) { + if (aOuter) + throw Components.results.NS_ERROR_NO_AGGREGATION; + return new MockPermissionPromptInstance().QueryInterface(aIID); + }, + lockFactory: function(aLock) { + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIFactory]) +}; + +this.MockPermissionPrompt = { + init: function() { + this.reset(); + if (!registrar.isCIDRegistered(newClassID)) { + try { + oldClassID = registrar.contractIDToCID(CONTRACT_ID); + oldFactory = Cm.getClassObject(Cc[CONTRACT_ID], Ci.nsIFactory); + } catch (ex) { + oldClassID = ""; + oldFactory = null; + dump("TEST-INFO | can't get permission prompt registered component, " + + "assuming there is none"); + } + if (oldFactory) { + registrar.unregisterFactory(oldClassID, oldFactory); + } + registrar.registerFactory(newClassID, "", CONTRACT_ID, newFactory); + } + }, + + reset: function() { + }, + + cleanup: function() { + this.reset(); + if (oldFactory) { + registrar.unregisterFactory(newClassID, newFactory); + registrar.registerFactory(oldClassID, "", CONTRACT_ID, oldFactory); + } + }, +}; + +function MockPermissionPromptInstance() { }; +MockPermissionPromptInstance.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPermissionPrompt]), + + promptResult: Ci.nsIPermissionManager.UNKNOWN_ACTION, + + prompt: function(request) { + + let perms = request.types.QueryInterface(Ci.nsIArray); + for (let idx = 0; idx < perms.length; idx++) { + let perm = perms.queryElementAt(idx, Ci.nsIContentPermissionType); + if (Services.perms.testExactPermissionFromPrincipal( + request.principal, perm.type) != Ci.nsIPermissionManager.ALLOW_ACTION) { + request.cancel(); + return; + } + } + + request.allow(); + } +}; + +// Expose everything to content. We call reset() here so that all of the relevant +// lazy expandos get added. +MockPermissionPrompt.reset(); +function exposeAll(obj) { + var props = {}; + for (var prop in obj) + props[prop] = 'rw'; + obj.__exposedProps__ = props; +} +exposeAll(MockPermissionPrompt); +exposeAll(MockPermissionPromptInstance.prototype); diff --git a/test/resources/firefox/extensions/special-powers@mozilla.org/components/SpecialPowersObserver.js b/test/resources/firefox/extensions/special-powers@mozilla.org/components/SpecialPowersObserver.js old mode 100755 new mode 100644 index 90655e2e7..7adc63758 --- a/test/resources/firefox/extensions/special-powers@mozilla.org/components/SpecialPowersObserver.js +++ b/test/resources/firefox/extensions/special-powers@mozilla.org/components/SpecialPowersObserver.js @@ -1,40 +1,6 @@ -/* ***** 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): - * Jesse Ruderman <jruderman@mozilla.com> - * Robert Sayre <sayrer@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/. */ // Based on: // https://bugzilla.mozilla.org/show_bug.cgi?id=549539 @@ -50,34 +16,33 @@ const Cc = Components.classes; const Ci = Components.interfaces; const CHILD_SCRIPT = "chrome://specialpowers/content/specialpowers.js" +const CHILD_SCRIPT_API = "chrome://specialpowers/content/specialpowersAPI.js" +const CHILD_LOGGER_SCRIPT = "chrome://specialpowers/content/MozillaLogger.js" -/** - * 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 + '"'; -}; +// Glue to add in the observer API to this object. This allows us to share code with chrome tests +var loader = Components.classes["@mozilla.org/moz/jssubscript-loader;1"] + .getService(Components.interfaces.mozIJSSubScriptLoader); +loader.loadSubScript("chrome://specialpowers/content/SpecialPowersObserverAPI.js"); /* XPCOM gunk */ -function SpecialPowersObserver() { +this.SpecialPowersObserver = function SpecialPowersObserver() { this._isFrameScriptLoaded = false; + this._mmIsGlobal = true; this._messageManager = Cc["@mozilla.org/globalmessagemanager;1"]. - getService(Ci.nsIChromeFrameMessageManager); + getService(Ci.nsIMessageBroadcaster); } -SpecialPowersObserver.prototype = { - classDescription: "Special powers Observer for use in testing.", - classID: Components.ID("{59a52458-13e0-4d93-9d85-a637344f29a1}"), - contractID: "@mozilla.org/special-powers-observer;1", - QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsIObserver]), - _xpcom_categories: [{category: "profile-after-change", service: true }], - observe: function(aSubject, aTopic, aData) +SpecialPowersObserver.prototype = new SpecialPowersObserverAPI(); + + SpecialPowersObserver.prototype.classDescription = "Special powers Observer for use in testing."; + SpecialPowersObserver.prototype.classID = Components.ID("{59a52458-13e0-4d93-9d85-a637344f29a1}"); + SpecialPowersObserver.prototype.contractID = "@mozilla.org/special-powers-observer;1"; + SpecialPowersObserver.prototype.QueryInterface = XPCOMUtils.generateQI([Components.interfaces.nsIObserver]); + SpecialPowersObserver.prototype._xpcom_categories = [{category: "profile-after-change", service: true }]; + + SpecialPowersObserver.prototype.observe = function(aSubject, aTopic, aData) { switch (aTopic) { case "profile-after-change": @@ -90,190 +55,105 @@ SpecialPowersObserver.prototype = { this._messageManager.addMessageListener("SPPrefService", this); this._messageManager.addMessageListener("SPProcessCrashService", this); this._messageManager.addMessageListener("SPPingService", this); + this._messageManager.addMessageListener("SpecialPowers.Quit", this); + this._messageManager.addMessageListener("SpecialPowers.Focus", this); + this._messageManager.addMessageListener("SPPermissionManager", this); + this._messageManager.addMessageListener("SPWebAppService", this); + this._messageManager.addMessageListener("SPObserverService", this); + this._messageManager.addMessageListener("SPLoadChromeScript", this); + this._messageManager.addMessageListener("SPChromeScriptMessage", this); + this._messageManager.loadFrameScript(CHILD_LOGGER_SCRIPT, true); + this._messageManager.loadFrameScript(CHILD_SCRIPT_API, true); this._messageManager.loadFrameScript(CHILD_SCRIPT, true); this._isFrameScriptLoaded = true; } break; + case "http-on-modify-request": + if (aSubject instanceof Ci.nsIChannel) { + let uri = aSubject.URI.spec; + this._sendAsyncMessage("specialpowers-http-notify-request", { uri: uri }); + } + break; + case "xpcom-shutdown": this.uninit(); break; - case "plugin-crashed": - case "ipc:content-shutdown": - function addDumpIDToMessage(propertyName) { - var id = aSubject.getPropertyAsAString(propertyName); - if (id) { - message.dumpIDs.push(id); - } - } - - var message = { type: "crash-observed", dumpIDs: [] }; - aSubject = aSubject.QueryInterface(Ci.nsIPropertyBag2); - if (aTopic == "plugin-crashed") { - addDumpIDToMessage("pluginDumpID"); - addDumpIDToMessage("browserDumpID"); - } else { // ipc:content-shutdown - addDumpIDToMessage("dumpID"); - } - this._messageManager.sendAsyncMessage("SPProcessCrashService", message); + default: + this._observe(aSubject, aTopic, aData); break; } - }, + }; - init: function() + SpecialPowersObserver.prototype._sendAsyncMessage = function(msgname, msg) + { + if (this._mmIsGlobal) { + this._messageManager.broadcastAsyncMessage(msgname, msg); + } + else { + this._messageManager.sendAsyncMessage(msgname, msg); + } + }; + + SpecialPowersObserver.prototype._receiveMessage = function(aMessage) { + return this._receiveMessageAPI(aMessage); + }; + + SpecialPowersObserver.prototype.init = function(messageManager) { var obs = Services.obs; obs.addObserver(this, "xpcom-shutdown", false); obs.addObserver(this, "chrome-document-global-created", false); - }, + obs.addObserver(this, "http-on-modify-request", false); - uninit: function() + if (messageManager) { + this._messageManager = messageManager; + this._mmIsGlobal = false; + } + }; + + SpecialPowersObserver.prototype.uninit = function() { var obs = Services.obs; - obs.removeObserver(this, "chrome-document-global-created", false); - this.removeProcessCrashObservers(); - }, - - addProcessCrashObservers: function() { + obs.removeObserver(this, "chrome-document-global-created"); + obs.removeObserver(this, "http-on-modify-request"); + this._removeProcessCrashObservers(); + }; + + SpecialPowersObserver.prototype._addProcessCrashObservers = function() { if (this._processCrashObserversRegistered) { return; } - Services.obs.addObserver(this, "plugin-crashed", false); - Services.obs.addObserver(this, "ipc:content-shutdown", false); - this._processCrashObserversRegistered = true; - }, + var obs = Components.classes["@mozilla.org/observer-service;1"] + .getService(Components.interfaces.nsIObserverService); - removeProcessCrashObservers: function() { + obs.addObserver(this, "plugin-crashed", false); + obs.addObserver(this, "ipc:content-shutdown", false); + this._processCrashObserversRegistered = true; + }; + + SpecialPowersObserver.prototype._removeProcessCrashObservers = function() { if (!this._processCrashObserversRegistered) { return; } - Services.obs.removeObserver(this, "plugin-crashed"); - Services.obs.removeObserver(this, "ipc:content-shutdown"); + var obs = Components.classes["@mozilla.org/observer-service;1"] + .getService(Components.interfaces.nsIObserverService); + + obs.removeObserver(this, "plugin-crashed"); + obs.removeObserver(this, "ipc:content-shutdown"); this._processCrashObserversRegistered = false; - }, - - getCrashDumpDir: function() { - if (!this._crashDumpDir) { - var directoryService = Cc["@mozilla.org/file/directory_service;1"] - .getService(Ci.nsIProperties); - this._crashDumpDir = directoryService.get("ProfD", Ci.nsIFile); - this._crashDumpDir.append("minidumps"); - } - return this._crashDumpDir; - }, - - 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(); - }, + }; /** * messageManager callback function * This will get requests from our API in the window and process them in chrome for it **/ - receiveMessage: function(aMessage) { + SpecialPowersObserver.prototype.receiveMessage = function(aMessage) { 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"); - } 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; - } - } - break; - - 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"); - } - break; - case "SPPingService": if (aMessage.json.op == "ping") { aMessage.target @@ -283,11 +163,16 @@ SpecialPowersObserver.prototype = { .sendAsyncMessage("SPPingService", { op: "pong" }); } break; - + case "SpecialPowers.Quit": + let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"].getService(Ci.nsIAppStartup); + appStartup.quit(Ci.nsIAppStartup.eForceQuit); + break; + case "SpecialPowers.Focus": + aMessage.target.focus(); + break; default: - throw new SpecialPowersException("Unrecognized Special Powers API"); + return this._receiveMessage(aMessage); } - } -}; + }; -const NSGetFactory = XPCOMUtils.generateNSGetFactory([SpecialPowersObserver]); +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SpecialPowersObserver]); diff --git a/test/resources/firefox/prefs.js b/test/resources/firefox/prefs.js index 8d59d66bd..343384338 100644 --- a/test/resources/firefox/prefs.js +++ b/test/resources/firefox/prefs.js @@ -1 +1,2 @@ user_pref("browser.shell.checkDefaultBrowser", false); +user_pref('extensions.autoDisableScopes', 14);