diff options
Diffstat (limited to 'testing/specialpowers/content')
-rw-r--r-- | testing/specialpowers/content/MockColorPicker.jsm | 117 | ||||
-rw-r--r-- | testing/specialpowers/content/MockFilePicker.jsm | 235 | ||||
-rw-r--r-- | testing/specialpowers/content/MockPermissionPrompt.jsm | 97 | ||||
-rw-r--r-- | testing/specialpowers/content/MozillaLogger.js | 134 | ||||
-rw-r--r-- | testing/specialpowers/content/SpecialPowersObserver.jsm | 313 | ||||
-rw-r--r-- | testing/specialpowers/content/SpecialPowersObserverAPI.js | 635 | ||||
-rw-r--r-- | testing/specialpowers/content/specialpowers.js | 278 | ||||
-rw-r--r-- | testing/specialpowers/content/specialpowersAPI.js | 2100 |
8 files changed, 3909 insertions, 0 deletions
diff --git a/testing/specialpowers/content/MockColorPicker.jsm b/testing/specialpowers/content/MockColorPicker.jsm new file mode 100644 index 000000000..3cce150c8 --- /dev/null +++ b/testing/specialpowers/content/MockColorPicker.jsm @@ -0,0 +1,117 @@ +/* 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"); + +// Allow stuff from this scope to be accessed from non-privileged scopes. This +// would crash if used outside of automation. +Cu.forcePermissiveCOWs(); + +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); + } +}; diff --git a/testing/specialpowers/content/MockFilePicker.jsm b/testing/specialpowers/content/MockFilePicker.jsm new file mode 100644 index 000000000..4c93cd0e2 --- /dev/null +++ b/testing/specialpowers/content/MockFilePicker.jsm @@ -0,0 +1,235 @@ +/* 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"); + +// Allow stuff from this scope to be accessed from non-privileged scopes. This +// would crash if used outside of automation. +Cu.forcePermissiveCOWs(); + +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, 0o644); + this.returnFiles = [file]; + }, + + useBlobFile: function() { + var blob = new this.window.Blob([]); + var file = new this.window.File([blob], 'helloworld.txt', { type: 'plain/text' }); + this.returnFiles = [file]; + }, + + useDirectory: function(aPath) { + var directory = new this.window.Directory(aPath); + this.returnFiles = [directory]; + }, + + 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; + }, + + // We don't support directories here. + get domFileOrDirectory() { + 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 domFileOrDirectoryEnumerator() { + 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); + } +}; diff --git a/testing/specialpowers/content/MockPermissionPrompt.jsm b/testing/specialpowers/content/MockPermissionPrompt.jsm new file mode 100644 index 000000000..e07f8e002 --- /dev/null +++ b/testing/specialpowers/content/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/testing/specialpowers/content/MozillaLogger.js b/testing/specialpowers/content/MozillaLogger.js new file mode 100644 index 000000000..52e16cabc --- /dev/null +++ b/testing/specialpowers/content/MozillaLogger.js @@ -0,0 +1,134 @@ +/** + * MozillaLogger, a base class logger that just logs to stdout. + */ + +"use strict"; + +function MozillaLogger(aPath) { +} + +function formatLogMessage(msg) { + return msg.info.join(' ') + "\n"; +} + +MozillaLogger.prototype = { + init : function(path) {}, + + getLogCallback : function() { + return function (msg) { + var data = formatLogMessage(msg); + 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 = formatLogMessage(msg); + 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, + 436 /* 0664 */, 0); + + this._converter = Components.classes["@mozilla.org/intl/converter-output-stream;1"]. + createInstance(Components.interfaces.nsIConverterOutputStream); + this._converter.init(this._foStream, "UTF-8", 0, 0); + }, + + getLogCallback : function() { + return function (msg) { + var data = formatLogMessage(msg); + if (MozillaFileLogger._converter) { + this._converter.writeString(data); + } + + if (data.indexOf("SimpleTest FINISH") >= 0) { + MozillaFileLogger.close(); + } + }; + }, + + log : function(msg) { + if (this._converter) { + this._converter.writeString(msg); + } + }, + close : function() { + if (this._converter) { + this._converter.flush(); + this._converter.close(); + } + + this._foStream = null; + this._converter = null; + this._file = null; + } +}; + +this.MozillaLogger = MozillaLogger; +this.SpecialPowersLogger = SpecialPowersLogger; +this.MozillaFileLogger = MozillaFileLogger; diff --git a/testing/specialpowers/content/SpecialPowersObserver.jsm b/testing/specialpowers/content/SpecialPowersObserver.jsm new file mode 100644 index 000000000..fc7584505 --- /dev/null +++ b/testing/specialpowers/content/SpecialPowersObserver.jsm @@ -0,0 +1,313 @@ +/* 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 +// https://bug549539.bugzilla.mozilla.org/attachment.cgi?id=429661 +// https://developer.mozilla.org/en/XPCOM/XPCOM_changes_in_Gecko_1.9.3 +// https://developer.mozilla.org/en/how_to_build_an_xpcom_component_in_javascript + +var EXPORTED_SYMBOLS = ["SpecialPowersObserver", "SpecialPowersObserverFactory"]; + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/Services.jsm"); +Components.utils.importGlobalProperties(['File']); + +if (typeof(Cc) == "undefined") { + 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" + + +// 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 */ +this.SpecialPowersObserver = function SpecialPowersObserver() { + this._isFrameScriptLoaded = false; + this._messageManager = Cc["@mozilla.org/globalmessagemanager;1"]. + getService(Ci.nsIMessageBroadcaster); +} + + +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.observe = function(aSubject, aTopic, aData) +{ + switch (aTopic) { + case "chrome-document-global-created": + this._loadFrameScript(); + 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; + + default: + this._observe(aSubject, aTopic, aData); + break; + } +}; + +SpecialPowersObserver.prototype._loadFrameScript = function() +{ + if (!this._isFrameScriptLoaded) { + // Register for any messages our API needs us to handle + 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("SpecialPowers.CreateFiles", this); + this._messageManager.addMessageListener("SpecialPowers.RemoveFiles", this); + this._messageManager.addMessageListener("SPPermissionManager", this); + this._messageManager.addMessageListener("SPObserverService", this); + this._messageManager.addMessageListener("SPLoadChromeScript", this); + this._messageManager.addMessageListener("SPImportInMainProcess", this); + this._messageManager.addMessageListener("SPChromeScriptMessage", this); + this._messageManager.addMessageListener("SPQuotaManager", this); + this._messageManager.addMessageListener("SPSetTestPluginEnabledState", this); + this._messageManager.addMessageListener("SPLoadExtension", this); + this._messageManager.addMessageListener("SPStartupExtension", this); + this._messageManager.addMessageListener("SPUnloadExtension", this); + this._messageManager.addMessageListener("SPExtensionMessage", this); + this._messageManager.addMessageListener("SPCleanUpSTSData", this); + this._messageManager.addMessageListener("SPClearAppPrivateData", this); + + this._messageManager.loadFrameScript(CHILD_LOGGER_SCRIPT, true); + this._messageManager.loadFrameScript(CHILD_SCRIPT_API, true); + this._messageManager.loadFrameScript(CHILD_SCRIPT, true); + this._isFrameScriptLoaded = true; + this._createdFiles = null; + } +}; + +SpecialPowersObserver.prototype._sendAsyncMessage = function(msgname, msg) +{ + this._messageManager.broadcastAsyncMessage(msgname, msg); +}; + +SpecialPowersObserver.prototype._receiveMessage = function(aMessage) { + return this._receiveMessageAPI(aMessage); +}; + +SpecialPowersObserver.prototype.init = function() +{ + var obs = Services.obs; + obs.addObserver(this, "chrome-document-global-created", false); + + // Register special testing modules. + var testsURI = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties). + get("ProfD", Ci.nsILocalFile); + testsURI.append("tests.manifest"); + var ioSvc = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + var manifestFile = ioSvc.newFileURI(testsURI). + QueryInterface(Ci.nsIFileURL).file; + + Components.manager.QueryInterface(Ci.nsIComponentRegistrar). + autoRegister(manifestFile); + + obs.addObserver(this, "http-on-modify-request", false); + + this._loadFrameScript(); +}; + +SpecialPowersObserver.prototype.uninit = function() +{ + var obs = Services.obs; + obs.removeObserver(this, "chrome-document-global-created"); + obs.removeObserver(this, "http-on-modify-request"); + this._registerObservers._topics.forEach(function(element) { + obs.removeObserver(this._registerObservers, element); + }); + this._removeProcessCrashObservers(); + + if (this._isFrameScriptLoaded) { + this._messageManager.removeMessageListener("SPPrefService", this); + this._messageManager.removeMessageListener("SPProcessCrashService", this); + this._messageManager.removeMessageListener("SPPingService", this); + this._messageManager.removeMessageListener("SpecialPowers.Quit", this); + this._messageManager.removeMessageListener("SpecialPowers.Focus", this); + this._messageManager.removeMessageListener("SpecialPowers.CreateFiles", this); + this._messageManager.removeMessageListener("SpecialPowers.RemoveFiles", this); + this._messageManager.removeMessageListener("SPPermissionManager", this); + this._messageManager.removeMessageListener("SPObserverService", this); + this._messageManager.removeMessageListener("SPLoadChromeScript", this); + this._messageManager.removeMessageListener("SPImportInMainProcess", this); + this._messageManager.removeMessageListener("SPChromeScriptMessage", this); + this._messageManager.removeMessageListener("SPQuotaManager", this); + this._messageManager.removeMessageListener("SPSetTestPluginEnabledState", this); + this._messageManager.removeMessageListener("SPLoadExtension", this); + this._messageManager.removeMessageListener("SPStartupExtension", this); + this._messageManager.removeMessageListener("SPUnloadExtension", this); + this._messageManager.removeMessageListener("SPExtensionMessage", this); + this._messageManager.removeMessageListener("SPCleanUpSTSData", this); + this._messageManager.removeMessageListener("SPClearAppPrivateData", this); + + this._messageManager.removeDelayedFrameScript(CHILD_LOGGER_SCRIPT); + this._messageManager.removeDelayedFrameScript(CHILD_SCRIPT_API); + this._messageManager.removeDelayedFrameScript(CHILD_SCRIPT); + this._isFrameScriptLoaded = false; + } +}; + +SpecialPowersObserver.prototype._addProcessCrashObservers = function() { + if (this._processCrashObserversRegistered) { + return; + } + + var obs = Components.classes["@mozilla.org/observer-service;1"] + .getService(Components.interfaces.nsIObserverService); + + obs.addObserver(this, "plugin-crashed", false); + obs.addObserver(this, "ipc:content-shutdown", false); + this._processCrashObserversRegistered = true; +}; + +SpecialPowersObserver.prototype._removeProcessCrashObservers = function() { + if (!this._processCrashObserversRegistered) { + return; + } + + 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; +}; + +SpecialPowersObserver.prototype._registerObservers = { + _self: null, + _topics: [], + _add: function(topic) { + if (this._topics.indexOf(topic) < 0) { + this._topics.push(topic); + Services.obs.addObserver(this, topic, false); + } + }, + observe: function (aSubject, aTopic, aData) { + var msg = { aData: aData }; + switch (aTopic) { + case "perm-changed": + var permission = aSubject.QueryInterface(Ci.nsIPermission); + + // specialPowersAPI will consume this value, and it is used as a + // fake permission, but only type and principal.appId will be used. + // + // We need to ensure that it looks the same as a real permission, + // so we fake these properties. + msg.permission = { + principal: { + originAttributes: {appId: permission.principal.appId} + }, + type: permission.type + }; + default: + this._self._sendAsyncMessage("specialpowers-" + aTopic, msg); + } + } +}; + +/** + * messageManager callback function + * This will get requests from our API in the window and process them in chrome for it + **/ +SpecialPowersObserver.prototype.receiveMessage = function(aMessage) { + switch(aMessage.name) { + case "SPPingService": + if (aMessage.json.op == "ping") { + aMessage.target + .QueryInterface(Ci.nsIFrameLoaderOwner) + .frameLoader + .messageManager + .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; + case "SpecialPowers.CreateFiles": + let filePaths = new Array; + if (!this._createdFiles) { + this._createdFiles = new Array; + } + let createdFiles = this._createdFiles; + try { + aMessage.data.forEach(function(request) { + const filePerms = 0666; + let testFile = Services.dirsvc.get("ProfD", Ci.nsIFile); + if (request.name) { + testFile.appendRelativePath(request.name); + } else { + testFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, filePerms); + } + let outStream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(Ci.nsIFileOutputStream); + outStream.init(testFile, 0x02 | 0x08 | 0x20, // PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE + filePerms, 0); + if (request.data) { + outStream.write(request.data, request.data.length); + } + outStream.close(); + filePaths.push(File.createFromFileName(testFile.path, request.options)); + createdFiles.push(testFile); + }); + aMessage.target + .QueryInterface(Ci.nsIFrameLoaderOwner) + .frameLoader + .messageManager + .sendAsyncMessage("SpecialPowers.FilesCreated", filePaths); + } catch (e) { + aMessage.target + .QueryInterface(Ci.nsIFrameLoaderOwner) + .frameLoader + .messageManager + .sendAsyncMessage("SpecialPowers.FilesError", e.toString()); + } + + break; + case "SpecialPowers.RemoveFiles": + if (this._createdFiles) { + this._createdFiles.forEach(function (testFile) { + try { + testFile.remove(false); + } catch (e) {} + }); + this._createdFiles = null; + } + break; + default: + return this._receiveMessage(aMessage); + } +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SpecialPowersObserver]); +this.SpecialPowersObserverFactory = Object.freeze({ + createInstance: function(outer, id) { + if (outer) { throw Components.results.NS_ERROR_NO_AGGREGATION }; + return new SpecialPowersObserver(); + }, + loadFactory: function(lock){}, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIFactory]) +}); diff --git a/testing/specialpowers/content/SpecialPowersObserverAPI.js b/testing/specialpowers/content/SpecialPowersObserverAPI.js new file mode 100644 index 000000000..f37f7bf0e --- /dev/null +++ b/testing/specialpowers/content/SpecialPowersObserverAPI.js @@ -0,0 +1,635 @@ +/* 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"); +Components.utils.import("resource://gre/modules/NetUtil.jsm"); + +if (typeof(Ci) == 'undefined') { + var Ci = Components.interfaces; +} + +if (typeof(Cc) == 'undefined') { + var Cc = Components.classes; +} + +this.SpecialPowersError = function(aMsg) { + Error.call(this); + let {stack} = new Error(); + this.message = aMsg; + this.name = "SpecialPowersError"; +} +SpecialPowersError.prototype = Object.create(Error.prototype); + +SpecialPowersError.prototype.toString = function() { + return `${this.name}: ${this.message}`; +}; + +this.SpecialPowersObserverAPI = function SpecialPowersObserverAPI() { + this._crashDumpDir = null; + this._processCrashObserversRegistered = false; + this._chromeScriptListeners = []; + this._extensions = new Map(); +} + +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); +} + +function getTestPlugin(pluginName) { + var ph = Cc["@mozilla.org/plugin/host;1"] + .getService(Ci.nsIPluginHost); + var tags = ph.getPluginTags(); + var name = pluginName || "Test Plug-in"; + for (var tag of tags) { + if (tag.name == name) { + return tag; + } + } + + return null; +} + +SpecialPowersObserverAPI.prototype = { + + _observe: function(aSubject, aTopic, aData) { + function addDumpIDToMessage(propertyName) { + try { + var id = aSubject.getPropertyAsAString(propertyName); + } catch(ex) { + var id = null; + } + 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 = NetUtil.newChannel({ + uri: aUrl, + loadUsingSystemPrincipal: true + }); + var input = channel.open2(); + 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 SpecialPowersError( + "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; + }, + + _sendReply: function(aMessage, aReplyName, aReplyMsg) { + let mm = aMessage.target + .QueryInterface(Ci.nsIFrameLoaderOwner) + .frameLoader + .messageManager; + mm.sendAsyncMessage(aReplyName, aReplyMsg); + }, + + _notifyCategoryAndObservers: function(subject, topic, data) { + const serviceMarker = "service,"; + + // First create observers from the category manager. + let cm = + Cc["@mozilla.org/categorymanager;1"].getService(Ci.nsICategoryManager); + let enumerator = cm.enumerateCategory(topic); + + let observers = []; + + while (enumerator.hasMoreElements()) { + let entry = + enumerator.getNext().QueryInterface(Ci.nsISupportsCString).data; + let contractID = cm.getCategoryEntry(topic, entry); + + let factoryFunction; + if (contractID.substring(0, serviceMarker.length) == serviceMarker) { + contractID = contractID.substring(serviceMarker.length); + factoryFunction = "getService"; + } + else { + factoryFunction = "createInstance"; + } + + try { + let handler = Cc[contractID][factoryFunction](); + if (handler) { + let observer = handler.QueryInterface(Ci.nsIObserver); + observers.push(observer); + } + } catch(e) { } + } + + // Next enumerate the registered observers. + enumerator = Services.obs.enumerateObservers(topic); + while (enumerator.hasMoreElements()) { + try { + let observer = enumerator.getNext().QueryInterface(Ci.nsIObserver); + if (observers.indexOf(observer) == -1) { + observers.push(observer); + } + } catch (e) { } + } + + observers.forEach(function (observer) { + try { + observer.observe(subject, topic, data); + } catch(e) { } + }); + }, + + /** + * 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": { + let prefs = Services.prefs; + let prefType = aMessage.json.prefType.toUpperCase(); + let prefName = aMessage.json.prefName; + let prefValue = "prefValue" in aMessage.json ? aMessage.json.prefValue : null; + + if (aMessage.json.op == "get") { + if (!prefName || !prefType) + throw new SpecialPowersError("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 SpecialPowersError("Invalid parameters for set in SPPrefService"); + } else if (aMessage.json.op == "clear") { + if (!prefName) + throw new SpecialPowersError("Invalid parameters for clear in SPPrefService"); + } else { + throw new SpecialPowersError("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 SpecialPowersError("Invalid operation for SPProcessCrashService"); + } + return undefined; // See comment at the beginning of this function. + } + + case "SPPermissionManager": { + let msg = aMessage.json; + let principal = msg.principal; + + switch (msg.op) { + case "add": + Services.perms.addFromPrincipal(principal, msg.type, msg.permission, msg.expireType, msg.expireTime); + break; + case "remove": + Services.perms.removeFromPrincipal(principal, msg.type); + break; + case "has": + let hasPerm = Services.perms.testPermissionFromPrincipal(principal, msg.type); + return hasPerm == Ci.nsIPermissionManager.ALLOW_ACTION; + case "test": + let testPerm = Services.perms.testPermissionFromPrincipal(principal, msg.type, msg.value); + return testPerm == msg.value; + default: + throw new SpecialPowersError( + "Invalid operation for SPPermissionManager"); + } + return undefined; // See comment at the beginning of this function. + } + + case "SPSetTestPluginEnabledState": { + var plugin = getTestPlugin(aMessage.data.pluginName); + if (!plugin) { + return undefined; + } + var oldEnabledState = plugin.enabledState; + plugin.enabledState = aMessage.data.newEnabledState; + return oldEnabledState; + } + + case "SPObserverService": { + let topic = aMessage.json.observerTopic; + switch (aMessage.json.op) { + case "notify": + let data = aMessage.json.observerData + Services.obs.notifyObservers(null, topic, data); + break; + case "add": + this._registerObservers._self = this; + this._registerObservers._add(topic); + break; + default: + throw new SpecialPowersError("Invalid operation for SPObserverervice"); + } + return undefined; // See comment at the beginning of this function. + } + + case "SPLoadChromeScript": { + let id = aMessage.json.id; + let jsScript; + let scriptName; + + if (aMessage.json.url) { + jsScript = this._readUrlAsString(aMessage.json.url); + scriptName = aMessage.json.url; + } else if (aMessage.json.function) { + jsScript = aMessage.json.function.body; + scriptName = aMessage.json.function.name + || "<loadChromeScript anonymous function>"; + } else { + throw new SpecialPowersError("SPLoadChromeScript: Invalid script"); + } + + // Setup a chrome sandbox that has access to sendAsyncMessage + // and addMessageListener in order to communicate with + // the mochitest. + let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal(); + let sb = Components.utils.Sandbox(systemPrincipal); + let 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 }); + }; + sb.browserElement = aMessage.target; + + // Also expose assertion functions + let reporter = function (err, message, stack) { + // Pipe assertions back to parent process + mm.sendAsyncMessage("SPChromeScriptAssert", + { id, name: scriptName, err, message, + stack }); + }; + Object.defineProperty(sb, "assert", { + get: function () { + let scope = Components.utils.createObjectIn(sb); + Services.scriptloader.loadSubScript("chrome://specialpowers/content/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", scriptName, 1); + } catch(e) { + throw new SpecialPowersError( + "Error while executing chrome script '" + scriptName + "':\n" + + e + "\n" + + e.fileName + ":" + e.lineNumber); + } + return undefined; // See comment at the beginning of this function. + } + + case "SPChromeScriptMessage": { + let id = aMessage.json.id; + let name = aMessage.json.name; + let message = aMessage.json.message; + return this._chromeScriptListeners + .filter(o => (o.name == name && o.id == id)) + .map(o => o.listener(message)); + } + + case "SPImportInMainProcess": { + var message = { hadError: false, errorMessage: null }; + try { + Components.utils.import(aMessage.data); + } catch (e) { + message.hadError = true; + message.errorMessage = e.toString(); + } + return message; + } + + case "SPCleanUpSTSData": { + let origin = aMessage.data.origin; + let flags = aMessage.data.flags; + let uri = Services.io.newURI(origin, null, null); + let sss = Cc["@mozilla.org/ssservice;1"]. + getService(Ci.nsISiteSecurityService); + sss.removeState(Ci.nsISiteSecurityService.HEADER_HSTS, uri, flags); + return undefined; + } + + case "SPLoadExtension": { + let {Extension} = Components.utils.import("resource://gre/modules/Extension.jsm", {}); + + let id = aMessage.data.id; + let ext = aMessage.data.ext; + let extension = Extension.generate(ext); + + let resultListener = (...args) => { + this._sendReply(aMessage, "SPExtensionMessage", {id, type: "testResult", args}); + }; + + let messageListener = (...args) => { + args.shift(); + this._sendReply(aMessage, "SPExtensionMessage", {id, type: "testMessage", args}); + }; + + // Register pass/fail handlers. + extension.on("test-result", resultListener); + extension.on("test-eq", resultListener); + extension.on("test-log", resultListener); + extension.on("test-done", resultListener); + + extension.on("test-message", messageListener); + + this._extensions.set(id, extension); + return undefined; + } + + case "SPStartupExtension": { + let {ExtensionData, Management} = Components.utils.import("resource://gre/modules/Extension.jsm", {}); + + let id = aMessage.data.id; + let extension = this._extensions.get(id); + let startupListener = (msg, ext) => { + if (ext == extension) { + this._sendReply(aMessage, "SPExtensionMessage", {id, type: "extensionSetId", args: [extension.id]}); + Management.off("startup", startupListener); + } + }; + Management.on("startup", startupListener); + + // Make sure the extension passes the packaging checks when + // they're run on a bare archive rather than a running instance, + // as the add-on manager runs them. + let extensionData = new ExtensionData(extension.rootURI); + extensionData.readManifest().then( + () => { + return extensionData.initAllLocales().then(() => { + if (extensionData.errors.length) { + return Promise.reject("Extension contains packaging errors"); + } + }); + }, + () => { + // readManifest() will throw if we're loading an embedded + // extension, so don't worry about locale errors in that + // case. + } + ).then(() => { + return extension.startup(); + }).then(() => { + this._sendReply(aMessage, "SPExtensionMessage", {id, type: "extensionStarted", args: []}); + }).catch(e => { + dump(`Extension startup failed: ${e}\n${e.stack}`); + Management.off("startup", startupListener); + this._sendReply(aMessage, "SPExtensionMessage", {id, type: "extensionFailed", args: []}); + }); + return undefined; + } + + case "SPExtensionMessage": { + let id = aMessage.data.id; + let extension = this._extensions.get(id); + extension.testMessage(...aMessage.data.args); + return undefined; + } + + case "SPUnloadExtension": { + let id = aMessage.data.id; + let extension = this._extensions.get(id); + this._extensions.delete(id); + extension.shutdown(); + this._sendReply(aMessage, "SPExtensionMessage", {id, type: "extensionUnloaded", args: []}); + return undefined; + } + + case "SPClearAppPrivateData": { + let appId = aMessage.data.appId; + let browserOnly = aMessage.data.browserOnly; + + let attributes = { appId: appId }; + if (browserOnly) { + attributes.inIsolatedMozBrowser = true; + } + this._notifyCategoryAndObservers(null, + "clear-origin-attributes-data", + JSON.stringify(attributes)); + + let subject = { + appId: appId, + browserOnly: browserOnly, + QueryInterface: XPCOMUtils.generateQI([Ci.mozIApplicationClearPrivateDataParams]) + }; + this._notifyCategoryAndObservers(subject, "webapps-clear-data", null); + + return undefined; + } + + default: + throw new SpecialPowersError("Unrecognized Special Powers API"); + } + + // We throw an exception before reaching this explicit return because + // we should never be arriving here anyway. + throw new SpecialPowersError("Unreached code"); + return undefined; + } +}; diff --git a/testing/specialpowers/content/specialpowers.js b/testing/specialpowers/content/specialpowers.js new file mode 100644 index 000000000..09dbb5209 --- /dev/null +++ b/testing/specialpowers/content/specialpowers.js @@ -0,0 +1,278 @@ +/* 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 + */ + +function SpecialPowers(window) { + this.window = Components.utils.getWeakReference(window); + this._windowID = window.QueryInterface(Components.interfaces.nsIInterfaceRequestor) + .getInterface(Components.interfaces.nsIDOMWindowUtils) + .currentInnerWindowID; + 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); + this._grandChildFrameMM = null; + this._createFilesOnError = null; + this._createFilesOnSuccess = null; + this.SP_SYNC_MESSAGES = ["SPChromeScriptMessage", + "SPLoadChromeScript", + "SPImportInMainProcess", + "SPObserverService", + "SPPermissionManager", + "SPPrefService", + "SPProcessCrashService", + "SPSetTestPluginEnabledState", + "SPCleanUpSTSData"]; + + this.SP_ASYNC_MESSAGES = ["SpecialPowers.Focus", + "SpecialPowers.Quit", + "SpecialPowers.CreateFiles", + "SpecialPowers.RemoveFiles", + "SPPingService", + "SPLoadExtension", + "SPStartupExtension", + "SPUnloadExtension", + "SPExtensionMessage", + "SPClearAppPrivateData"]; + addMessageListener("SPPingService", this._messageListener); + addMessageListener("SpecialPowers.FilesCreated", this._messageListener); + addMessageListener("SpecialPowers.FilesError", this._messageListener); + let self = this; + Services.obs.addObserver(function onInnerWindowDestroyed(subject, topic, data) { + var id = subject.QueryInterface(Components.interfaces.nsISupportsPRUint64).data; + if (self._windowID === id) { + Services.obs.removeObserver(onInnerWindowDestroyed, "inner-window-destroyed"); + try { + removeMessageListener("SPPingService", self._messageListener); + removeMessageListener("SpecialPowers.FilesCreated", self._messageListener); + removeMessageListener("SpecialPowers.FilesError", self._messageListener); + } catch (e if e.result == Components.results.NS_ERROR_ILLEGAL_VALUE) { + // Ignore the exception which the message manager has been destroyed. + ; + } + } + }, "inner-window-destroyed", false); +} + +SpecialPowers.prototype = new SpecialPowersAPI(); + +SpecialPowers.prototype.toString = function() { return "[SpecialPowers]"; }; +SpecialPowers.prototype.sanityCheck = function() { return "foo"; }; + +// This gets filled in in the constructor. +SpecialPowers.prototype.DOMWindowUtils = undefined; +SpecialPowers.prototype.Components = undefined; +SpecialPowers.prototype.IsInNestedFrame = false; + +SpecialPowers.prototype._sendSyncMessage = function(msgname, msg) { + if (this.SP_SYNC_MESSAGES.indexOf(msgname) == -1) { + dump("TEST-INFO | specialpowers.js | Unexpected SP message: " + msgname + "\n"); + } + return sendSyncMessage(msgname, msg); +}; + +SpecialPowers.prototype._sendAsyncMessage = function(msgname, msg) { + if (this.SP_ASYNC_MESSAGES.indexOf(msgname) == -1) { + dump("TEST-INFO | specialpowers.js | Unexpected SP message: " + msgname + "\n"); + } + sendAsyncMessage(msgname, msg); +}; + +SpecialPowers.prototype._addMessageListener = function(msgname, listener) { + addMessageListener(msgname, listener); + sendAsyncMessage("SPPAddNestedMessageListener", { name: msgname }); +}; + +SpecialPowers.prototype._removeMessageListener = function(msgname, listener) { + removeMessageListener(msgname, listener); +}; + +SpecialPowers.prototype.registerProcessCrashObservers = function() { + addMessageListener("SPProcessCrashService", this._messageListener); + sendSyncMessage("SPProcessCrashService", { op: "register-observer" }); +}; + +SpecialPowers.prototype.unregisterProcessCrashObservers = function() { + removeMessageListener("SPProcessCrashService", this._messageListener); + sendSyncMessage("SPProcessCrashService", { op: "unregister-observer" }); +}; + +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; + + case "SPPingService": + if (aMessage.json.op == "pong") { + var handler = this._pongHandlers.shift(); + if (handler) { + handler(); + } + if (this._grandChildFrameMM) { + this._grandChildFrameMM.sendAsyncMessage("SPPingService", { op: "pong" }); + } + } + break; + + case "SpecialPowers.FilesCreated": + var handler = this._createFilesOnSuccess; + this._createFilesOnSuccess = null; + this._createFilesOnError = null; + if (handler) { + handler(aMessage.data); + } + break; + + case "SpecialPowers.FilesError": + var handler = this._createFilesOnError; + this._createFilesOnSuccess = null; + this._createFilesOnError = null; + if (handler) { + handler(aMessage.data); + } + break; + } + + return true; +}; + +SpecialPowers.prototype.quit = function() { + sendAsyncMessage("SpecialPowers.Quit", {}); +}; + +// fileRequests is an array of file requests. Each file request is an object. +// A request must have a field |name|, which gives the base of the name of the +// file to be created in the profile directory. If the request has a |data| field +// then that data will be written to the file. +SpecialPowers.prototype.createFiles = function(fileRequests, onCreation, onError) { + if (this._createFilesOnSuccess || this._createFilesOnError) { + onError("Already waiting for SpecialPowers.createFiles() to finish."); + return; + } + + this._createFilesOnSuccess = onCreation; + this._createFilesOnError = onError; + sendAsyncMessage("SpecialPowers.CreateFiles", fileRequests); +}; + +// Remove the files that were created using |SpecialPowers.createFiles()|. +// This will be automatically called by |SimpleTest.finish()|. +SpecialPowers.prototype.removeFiles = function() { + sendAsyncMessage("SpecialPowers.RemoveFiles", {}); +}; + +SpecialPowers.prototype.executeAfterFlushingMessageQueue = function(aCallback) { + this._pongHandlers.push(aCallback); + sendAsyncMessage("SPPingService", { op: "ping" }); +}; + +SpecialPowers.prototype.nestedFrameSetup = function() { + let self = this; + Services.obs.addObserver(function onRemoteBrowserShown(subject, topic, data) { + let frameLoader = subject; + // get a ref to the app <iframe> + frameLoader.QueryInterface(Components.interfaces.nsIFrameLoader); + let frame = frameLoader.ownerElement; + let frameId = frame.getAttribute('id'); + if (frameId === "nested-parent-frame") { + Services.obs.removeObserver(onRemoteBrowserShown, "remote-browser-shown"); + + let mm = frame.QueryInterface(Components.interfaces.nsIFrameLoaderOwner).frameLoader.messageManager; + self._grandChildFrameMM = mm; + + self.SP_SYNC_MESSAGES.forEach(function (msgname) { + mm.addMessageListener(msgname, function (msg) { + return self._sendSyncMessage(msgname, msg.data)[0]; + }); + }); + self.SP_ASYNC_MESSAGES.forEach(function (msgname) { + mm.addMessageListener(msgname, function (msg) { + self._sendAsyncMessage(msgname, msg.data); + }); + }); + mm.addMessageListener("SPPAddNestedMessageListener", function(msg) { + self._addMessageListener(msg.json.name, function(aMsg) { + mm.sendAsyncMessage(aMsg.name, aMsg.data); + }); + }); + + mm.loadFrameScript("chrome://specialpowers/content/MozillaLogger.js", false); + mm.loadFrameScript("chrome://specialpowers/content/specialpowersAPI.js", false); + mm.loadFrameScript("chrome://specialpowers/content/specialpowers.js", false); + + let frameScript = "SpecialPowers.prototype.IsInNestedFrame=true;"; + mm.loadFrameScript("data:," + frameScript, false); + } + }, "remote-browser-shown", false); +}; + +SpecialPowers.prototype.isServiceWorkerRegistered = function() { + var swm = Components.classes["@mozilla.org/serviceworkers/manager;1"] + .getService(Components.interfaces.nsIServiceWorkerManager); + return swm.getAllRegistrations().length != 0; +}; + +// Attach our API to the window. +function attachSpecialPowersToWindow(aWindow) { + try { + if ((aWindow !== null) && + (aWindow !== undefined) && + (aWindow.wrappedJSObject) && + !(aWindow.wrappedJSObject.SpecialPowers)) { + let sp = new SpecialPowers(aWindow); + aWindow.wrappedJSObject.SpecialPowers = sp; + if (sp.IsInNestedFrame) { + sp.addPermission("allowXULXBL", true, aWindow.document); + } + } + } catch(ex) { + dump("TEST-INFO | specialpowers.js | Failed to attach specialpowers to window exception: " + ex + "\n"); + } +} + +// This is a frame script, so it may be running in a content process. +// In any event, it is targeted at a specific "tab", so we listen for +// the DOMWindowCreated event to be notified about content windows +// being created in this context. + +function SpecialPowersManager() { + addEventListener("DOMWindowCreated", this, false); +} + +SpecialPowersManager.prototype = { + handleEvent: function handleEvent(aEvent) { + var window = aEvent.target.defaultView; + attachSpecialPowersToWindow(window); + } +}; + + +var specialpowersmanager = new SpecialPowersManager(); + +this.SpecialPowers = SpecialPowers; +this.attachSpecialPowersToWindow = attachSpecialPowersToWindow; + +// In the case of Chrome mochitests that inject specialpowers.js as +// a regular content script +if (typeof window != 'undefined') { + window.addMessageListener = function() {} + window.removeMessageListener = function() {} + window.wrappedJSObject.SpecialPowers = new SpecialPowers(window); +} diff --git a/testing/specialpowers/content/specialpowersAPI.js b/testing/specialpowers/content/specialpowersAPI.js new file mode 100644 index 000000000..ee94e84a3 --- /dev/null +++ b/testing/specialpowers/content/specialpowersAPI.js @@ -0,0 +1,2100 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +/* This code is loaded in every child process that is started by mochitest in + * order to be used as a replacement for UniversalXPConnect + */ + +"use strict"; + +var global = this; + +var Ci = Components.interfaces; +var Cc = Components.classes; +var Cu = Components.utils; + +Cu.import("chrome://specialpowers/content/MockFilePicker.jsm"); +Cu.import("chrome://specialpowers/content/MockColorPicker.jsm"); +Cu.import("chrome://specialpowers/content/MockPermissionPrompt.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +// We're loaded with "this" not set to the global in some cases, so we +// have to play some games to get at the global object here. Normally +// we'd try "this" from a function called with undefined this value, +// but this whole file is in strict mode. So instead fall back on +// returning "this" from indirect eval, which returns the global. +if (!(function() { var e = eval; return e("this"); })().File) { + Cu.importGlobalProperties(["File"]); +} + +// Allow stuff from this scope to be accessed from non-privileged scopes. This +// would crash if used outside of automation. +Cu.forcePermissiveCOWs(); + +function SpecialPowersAPI() { + this._consoleListeners = []; + this._encounteredCrashDumpFiles = []; + this._unexpectedCrashDumpFiles = { }; + this._crashDumpDir = null; + this._mfl = null; + this._prefEnvUndoStack = []; + this._pendingPrefs = []; + this._applyingPrefs = false; + this._permissionsUndoStack = []; + this._pendingPermissions = []; + this._applyingPermissions = false; + this._observingPermissions = false; + this._fm = null; + this._cb = null; +} + +function bindDOMWindowUtils(aWindow) { + if (!aWindow) + return + + var util = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + return wrapPrivileged(util); +} + +function getRawComponents(aWindow) { + // If we're running in automation that supports enablePrivilege, then we also + // provided access to the privileged Components. + try { + let win = Cu.waiveXrays(aWindow); + if (typeof win.netscape.security.PrivilegeManager == 'object') + Cu.forcePrivilegedComponentsForScope(aWindow); + } catch (e) {} + return Cu.getComponentsForScope(aWindow); +} + +function isWrappable(x) { + if (typeof x === "object") + return x !== null; + return typeof x === "function"; +}; + +function isWrapper(x) { + return isWrappable(x) && (typeof x.SpecialPowers_wrappedObject !== "undefined"); +}; + +function unwrapIfWrapped(x) { + return isWrapper(x) ? unwrapPrivileged(x) : x; +}; + +function wrapIfUnwrapped(x) { + return isWrapper(x) ? x : wrapPrivileged(x); +} + +function isObjectOrArray(obj) { + if (Object(obj) !== obj) + return false; + let arrayClasses = ['Object', 'Array', 'Int8Array', 'Uint8Array', + 'Int16Array', 'Uint16Array', 'Int32Array', + 'Uint32Array', 'Float32Array', 'Float64Array', + 'Uint8ClampedArray']; + let className = Cu.getClassName(obj, true); + return arrayClasses.indexOf(className) != -1; +} + +// In general, we want Xray wrappers for content DOM objects, because waiving +// Xray gives us Xray waiver wrappers that clamp the principal when we cross +// compartment boundaries. However, there are some exceptions where we want +// to use a waiver: +// +// * Xray adds some gunk to toString(), which has the potential to confuse +// consumers that aren't expecting Xray wrappers. Since toString() is a +// non-privileged method that returns only strings, we can just waive Xray +// for that case. +// +// * We implement Xrays to pure JS [[Object]] and [[Array]] instances that +// filter out tricky things like callables. This is the right thing for +// security in general, but tends to break tests that try to pass object +// literals into SpecialPowers. So we waive [[Object]] and [[Array]] +// instances before inspecting properties. +// +// * When we don't have meaningful Xray semantics, we create an Opaque +// XrayWrapper for security reasons. For test code, we generally want to see +// through that sort of thing. +function waiveXraysIfAppropriate(obj, propName) { + if (propName == 'toString' || isObjectOrArray(obj) || + /Opaque/.test(Object.prototype.toString.call(obj))) +{ + return XPCNativeWrapper.unwrap(obj); +} + return obj; +} + +// We can't call apply() directy on Xray-wrapped functions, so we have to be +// clever. +function doApply(fun, invocant, args) { + // We implement Xrays to pure JS [[Object]] instances that filter out tricky + // things like callables. This is the right thing for security in general, + // but tends to break tests that try to pass object literals into + // SpecialPowers. So we waive [[Object]] instances when they're passed to a + // SpecialPowers-wrapped callable. + // + // Note that the transitive nature of Xray waivers means that any property + // pulled off such an object will also be waived, and so we'll get principal + // clamping for Xrayed DOM objects reached from literals, so passing things + // like {l : xoWin.location} won't work. Hopefully the rabbit hole doesn't + // go that deep. + args = args.map(x => isObjectOrArray(x) ? Cu.waiveXrays(x) : x); + return Reflect.apply(fun, invocant, args); +} + +function wrapPrivileged(obj) { + + // Primitives pass straight through. + if (!isWrappable(obj)) + return obj; + + // No double wrapping. + if (isWrapper(obj)) + throw "Trying to double-wrap object!"; + + let dummy; + if (typeof obj === "function") + dummy = function() {}; + else + dummy = Object.create(null); + + return new Proxy(dummy, new SpecialPowersHandler(obj)); +}; + +function unwrapPrivileged(x) { + + // We don't wrap primitives, so sometimes we have a primitive where we'd + // expect to have a wrapper. The proxy pretends to be the type that it's + // emulating, so we can just as easily check isWrappable() on a proxy as + // we can on an unwrapped object. + if (!isWrappable(x)) + return x; + + // If we have a wrappable type, make sure it's wrapped. + if (!isWrapper(x)) + throw "Trying to unwrap a non-wrapped object!"; + + var obj = x.SpecialPowers_wrappedObject; + // unwrapped. + return obj; +}; + +function SpecialPowersHandler(wrappedObject) { + this.wrappedObject = wrappedObject; +} + +SpecialPowersHandler.prototype = { + construct(target, args) { + // The arguments may or may not be wrappers. Unwrap them if necessary. + var unwrappedArgs = Array.prototype.slice.call(args).map(unwrapIfWrapped); + + // We want to invoke "obj" as a constructor, but using unwrappedArgs as + // the arguments. Make sure to wrap and re-throw exceptions! + try { + return wrapIfUnwrapped(Reflect.construct(this.wrappedObject, unwrappedArgs)); + } catch (e) { + throw wrapIfUnwrapped(e); + } + }, + + apply(target, thisValue, args) { + // The invocant and arguments may or may not be wrappers. Unwrap + // them if necessary. + var invocant = unwrapIfWrapped(thisValue); + var unwrappedArgs = Array.prototype.slice.call(args).map(unwrapIfWrapped); + + try { + return wrapIfUnwrapped(doApply(this.wrappedObject, invocant, unwrappedArgs)); + } catch (e) { + // Wrap exceptions and re-throw them. + throw wrapIfUnwrapped(e); + } + }, + + has(target, prop) { + if (prop === "SpecialPowers_wrappedObject") + return true; + + return Reflect.has(this.wrappedObject, prop); + }, + + get(target, prop, receiver) { + if (prop === "SpecialPowers_wrappedObject") + return this.wrappedObject; + + let obj = waiveXraysIfAppropriate(this.wrappedObject, prop); + return wrapIfUnwrapped(Reflect.get(obj, prop)); + }, + + set(target, prop, val, receiver) { + if (prop === "SpecialPowers_wrappedObject") + return false; + + let obj = waiveXraysIfAppropriate(this.wrappedObject, prop); + return Reflect.set(obj, prop, unwrapIfWrapped(val)); + }, + + delete(target, prop) { + if (prop === "SpecialPowers_wrappedObject") + return false; + + return Reflect.deleteProperty(this.wrappedObject, prop); + }, + + defineProperty(target, prop, descriptor) { + throw "Can't call defineProperty on SpecialPowers wrapped object"; + }, + + getOwnPropertyDescriptor(target, prop) { + // Handle our special API. + if (prop === "SpecialPowers_wrappedObject") { + return { value: this.wrappedObject, writeable: true, + configurable: true, enumerable: false }; + } + + let obj = waiveXraysIfAppropriate(this.wrappedObject, prop); + let desc = Reflect.getOwnPropertyDescriptor(obj, prop); + + if (desc === undefined) + return undefined; + + // Transitively maintain the wrapper membrane. + function wrapIfExists(key) { + if (key in desc) + desc[key] = wrapIfUnwrapped(desc[key]); + }; + + wrapIfExists('value'); + wrapIfExists('get'); + wrapIfExists('set'); + + // A trapping proxy's properties must always be configurable, but sometimes + // we come across non-configurable properties. Tell a white lie. + desc.configurable = true; + + return desc; + }, + + ownKeys(target) { + // Insert our special API. It's not enumerable, but ownKeys() + // includes non-enumerable properties. + let props = ['SpecialPowers_wrappedObject']; + + // Do the normal thing. + let flt = (a) => !props.includes(a); + props = props.concat(Reflect.ownKeys(this.wrappedObject).filter(flt)); + + // If we've got an Xray wrapper, include the expandos as well. + if ('wrappedJSObject' in this.wrappedObject) { + props = props.concat(Reflect.ownKeys(this.wrappedObject.wrappedJSObject) + .filter(flt)); + } + + return props; + }, + + preventExtensions(target) { + throw "Can't call preventExtensions on SpecialPowers wrapped object"; + } +}; + +// SPConsoleListener reflects nsIConsoleMessage objects into JS in a +// tidy, XPCOM-hiding way. Messages that are nsIScriptError objects +// have their properties exposed in detail. It also auto-unregisters +// itself when it receives a "sentinel" message. +function SPConsoleListener(callback) { + this.callback = callback; +} + +SPConsoleListener.prototype = { + observe: function(msg) { + let m = { message: msg.message, + errorMessage: null, + sourceName: null, + sourceLine: null, + lineNumber: null, + columnNumber: null, + category: null, + windowID: null, + isScriptError: false, + isWarning: false, + isException: false, + isStrict: false }; + if (msg instanceof Ci.nsIScriptError) { + m.errorMessage = msg.errorMessage; + m.sourceName = msg.sourceName; + m.sourceLine = msg.sourceLine; + m.lineNumber = msg.lineNumber; + m.columnNumber = msg.columnNumber; + m.category = msg.category; + m.windowID = msg.outerWindowID; + m.isScriptError = true; + m.isWarning = ((msg.flags & Ci.nsIScriptError.warningFlag) === 1); + m.isException = ((msg.flags & Ci.nsIScriptError.exceptionFlag) === 1); + m.isStrict = ((msg.flags & Ci.nsIScriptError.strictFlag) === 1); + } + + Object.freeze(m); + + this.callback.call(undefined, m); + + if (!m.isScriptError && m.message === "SENTINEL") + Services.console.unregisterListener(this); + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIConsoleListener]) +}; + +function wrapCallback(cb) { + return function SpecialPowersCallbackWrapper() { + var args = Array.prototype.map.call(arguments, wrapIfUnwrapped); + return cb.apply(this, args); + } +} + +function wrapCallbackObject(obj) { + obj = Cu.waiveXrays(obj); + var wrapper = {}; + for (var i in obj) { + if (typeof obj[i] == 'function') + wrapper[i] = wrapCallback(obj[i]); + else + wrapper[i] = obj[i]; + } + return wrapper; +} + +function setWrapped(obj, prop, val) { + if (!isWrapper(obj)) + throw "You only need to use this for SpecialPowers wrapped objects"; + + obj = unwrapPrivileged(obj); + return Reflect.set(obj, prop, val); +} + +SpecialPowersAPI.prototype = { + + /* + * Privileged object wrapping API + * + * Usage: + * var wrapper = SpecialPowers.wrap(obj); + * wrapper.privilegedMethod(); wrapper.privilegedProperty; + * obj === SpecialPowers.unwrap(wrapper); + * + * These functions provide transparent access to privileged objects using + * various pieces of deep SpiderMagic. Conceptually, a wrapper is just an + * object containing a reference to the underlying object, where all method + * calls and property accesses are transparently performed with the System + * Principal. Moreover, objects obtained from the wrapper (including properties + * and method return values) are wrapped automatically. Thus, after a single + * call to SpecialPowers.wrap(), the wrapper layer is transitively maintained. + * + * Known Issues: + * + * - The wrapping function does not preserve identity, so + * SpecialPowers.wrap(foo) !== SpecialPowers.wrap(foo). See bug 718543. + * + * - The wrapper cannot see expando properties on unprivileged DOM objects. + * That is to say, the wrapper uses Xray delegation. + * + * - The wrapper sometimes guesses certain ES5 attributes for returned + * properties. This is explained in a comment in the wrapper code above, + * and shouldn't be a problem. + */ + wrap: wrapIfUnwrapped, + unwrap: unwrapIfWrapped, + isWrapper: isWrapper, + + /* + * When content needs to pass a callback or a callback object to an API + * accessed over SpecialPowers, that API may sometimes receive arguments for + * whom it is forbidden to create a wrapper in content scopes. As such, we + * need a layer to wrap the values in SpecialPowers wrappers before they ever + * reach content. + */ + wrapCallback: wrapCallback, + wrapCallbackObject: wrapCallbackObject, + + /* + * Used for assigning a property to a SpecialPowers wrapper, without unwrapping + * the value that is assigned. + */ + setWrapped: setWrapped, + + /* + * Create blank privileged objects to use as out-params for privileged functions. + */ + createBlankObject: function () { + return new Object; + }, + + /* + * Because SpecialPowers wrappers don't preserve identity, comparing with == + * can be hazardous. Sometimes we can just unwrap to compare, but sometimes + * wrapping the underlying object into a content scope is forbidden. This + * function strips any wrappers if they exist and compare the underlying + * values. + */ + compare: function(a, b) { + return unwrapIfWrapped(a) === unwrapIfWrapped(b); + }, + + get MockFilePicker() { + return MockFilePicker; + }, + + get MockColorPicker() { + return MockColorPicker; + }, + + get MockPermissionPrompt() { + return MockPermissionPrompt; + }, + + /* + * Load a privileged script that runs same-process. This is different from + * |loadChromeScript|, which will run in the parent process in e10s mode. + */ + loadPrivilegedScript: function (aFunction) { + var str = "(" + aFunction.toString() + ")();"; + var systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal(); + var sb = Cu.Sandbox(systemPrincipal); + var window = this.window.get(); + var mc = new window.MessageChannel(); + sb.port = mc.port1; + try { + sb.eval(str); + } catch (e) { + throw wrapIfUnwrapped(e); + } + + return mc.port2; + }, + + loadChromeScript: function (urlOrFunction) { + // Create a unique id for this chrome script + let uuidGenerator = Cc["@mozilla.org/uuid-generator;1"] + .getService(Ci.nsIUUIDGenerator); + let id = uuidGenerator.generateUUID().toString(); + + // Tells chrome code to evaluate this chrome script + let scriptArgs = { id }; + if (typeof(urlOrFunction) == "function") { + scriptArgs.function = { + body: "(" + urlOrFunction.toString() + ")();", + name: urlOrFunction.name, + }; + } else { + scriptArgs.url = urlOrFunction; + } + this._sendSyncMessage("SPLoadChromeScript", + scriptArgs); + + // Returns a MessageManager like API in order to be + // able to communicate with this chrome script + let listeners = []; + let chromeScript = { + addMessageListener: (name, listener) => { + listeners.push({ name: name, listener: listener }); + }, + + promiseOneMessage: name => new Promise(resolve => { + chromeScript.addMessageListener(name, function listener(message) { + chromeScript.removeMessageListener(name, listener); + resolve(message); + }); + }), + + removeMessageListener: (name, listener) => { + listeners = listeners.filter( + o => (o.name != name || o.listener != listener) + ); + }, + + sendAsyncMessage: (name, message) => { + this._sendSyncMessage("SPChromeScriptMessage", + { id: id, name: name, message: message }); + }, + + sendSyncMessage: (name, message) => { + return this._sendSyncMessage("SPChromeScriptMessage", + { id, name, message }); + }, + + destroy: () => { + listeners = []; + this._removeMessageListener("SPChromeScriptMessage", chromeScript); + this._removeMessageListener("SPChromeScriptAssert", chromeScript); + }, + + receiveMessage: (aMessage) => { + let messageId = aMessage.json.id; + let name = aMessage.json.name; + let message = aMessage.json.message; + // Ignore message from other chrome script + if (messageId != id) + return; + + if (aMessage.name == "SPChromeScriptMessage") { + listeners.filter(o => (o.name == name)) + .forEach(o => o.listener(message)); + } else if (aMessage.name == "SPChromeScriptAssert") { + assert(aMessage.json); + } + } + }; + this._addMessageListener("SPChromeScriptMessage", chromeScript); + this._addMessageListener("SPChromeScriptAssert", chromeScript); + + let assert = json => { + // An assertion has been done in a mochitest chrome script + let {name, err, message, stack} = json; + + // Try to fetch a test runner from the mochitest + // in order to properly log these assertions and notify + // all usefull log observers + let window = this.window.get(); + let parentRunner, repr = o => o; + if (window) { + window = window.wrappedJSObject; + parentRunner = window.TestRunner; + if (window.repr) { + repr = window.repr; + } + } + + // Craft a mochitest-like report string + var resultString = err ? "TEST-UNEXPECTED-FAIL" : "TEST-PASS"; + var diagnostic = + message ? message : + ("assertion @ " + stack.filename + ":" + stack.lineNumber); + if (err) { + diagnostic += + " - got " + repr(err.actual) + + ", expected " + repr(err.expected) + + " (operator " + err.operator + ")"; + } + var msg = [resultString, name, diagnostic].join(" | "); + if (parentRunner) { + if (err) { + parentRunner.addFailedTest(name); + parentRunner.error(msg); + } else { + parentRunner.log(msg); + } + } else { + // When we are running only a single mochitest, there is no test runner + dump(msg + "\n"); + } + }; + + return this.wrap(chromeScript); + }, + + importInMainProcess: function (importString) { + var message = this._sendSyncMessage("SPImportInMainProcess", importString)[0]; + if (message.hadError) { + throw "SpecialPowers.importInMainProcess failed with error " + message.errorMessage; + } + return; + }, + + get Services() { + return wrapPrivileged(Services); + }, + + /* + * In general, any Components object created for unprivileged scopes is + * neutered (it implements nsIXPCComponentsBase, but not nsIXPCComponents). + * We override this in certain legacy automation configurations (see the + * implementation of getRawComponents() above), but don't want to support + * it in cases where it isn't already required. + * + * In scopes with neutered Components, we don't have a natural referent for + * things like SpecialPowers.Cc. So in those cases, we fall back to the + * Components object from the SpecialPowers scope. This doesn't quite behave + * the same way (in particular, SpecialPowers.Cc[foo].createInstance() will + * create an instance in the SpecialPowers scope), but SpecialPowers wrapping + * is already a YMMV / Whatever-It-Takes-To-Get-TBPL-Green sort of thing. + * + * It probably wouldn't be too much work to just make SpecialPowers.Components + * unconditionally point to the Components object in the SpecialPowers scope. + * Try will tell what needs to be fixed up. + */ + getFullComponents: function() { + if (this.Components && typeof this.Components.classes == 'object') { + return this.Components; + } + return Components; + }, + + /* + * Convenient shortcuts to the standard Components abbreviations. Note that + * we don't SpecialPowers-wrap Components.interfaces, because it's available + * to untrusted content, and wrapping it confuses QI and identity checks. + */ + get Cc() { return wrapPrivileged(this.getFullComponents().classes); }, + get Ci() { return this.Components ? this.Components.interfaces + : Components.interfaces; }, + get Cu() { return wrapPrivileged(this.getFullComponents().utils); }, + get Cr() { return wrapPrivileged(this.Components.results); }, + + /* + * SpecialPowers.getRawComponents() allows content to get a reference to a + * naked (and, in certain automation configurations, privileged) Components + * object for its scope. + * + * SpecialPowers.getRawComponents(window) is defined as the global property + * window.SpecialPowers.Components for convenience. + */ + getRawComponents: getRawComponents, + + getDOMWindowUtils: function(aWindow) { + if (aWindow == this.window.get() && this.DOMWindowUtils != null) + return this.DOMWindowUtils; + + return bindDOMWindowUtils(aWindow); + }, + + removeExpectedCrashDumpFiles: function(aExpectingProcessCrash) { + var success = true; + if (aExpectingProcessCrash) { + var message = { + op: "delete-crash-dump-files", + filenames: this._encounteredCrashDumpFiles + }; + if (!this._sendSyncMessage("SPProcessCrashService", message)[0]) { + success = false; + } + } + this._encounteredCrashDumpFiles.length = 0; + return success; + }, + + findUnexpectedCrashDumpFiles: function() { + var self = this; + var message = { + op: "find-crash-dump-files", + crashDumpFilesToIgnore: this._unexpectedCrashDumpFiles + }; + var crashDumpFiles = this._sendSyncMessage("SPProcessCrashService", message)[0]; + crashDumpFiles.forEach(function(aFilename) { + self._unexpectedCrashDumpFiles[aFilename] = true; + }); + return crashDumpFiles; + }, + + _setTimeout: function(callback) { + // for mochitest-browser + if (typeof window != 'undefined') + setTimeout(callback, 0); + // for mochitest-plain + else + content.window.setTimeout(callback, 0); + }, + + _delayCallbackTwice: function(callback) { + function delayedCallback() { + function delayAgain(aCallback) { + // Using this._setTimeout doesn't work here + // It causes failures in mochtests that use + // multiple pushPrefEnv calls + // For chrome/browser-chrome mochitests + if (typeof window != 'undefined') + setTimeout(aCallback, 0); + // For mochitest-plain + else + content.window.setTimeout(aCallback, 0); + } + delayAgain(delayAgain(callback)); + } + return delayedCallback; + }, + + /* apply permissions to the system and when the test case is finished (SimpleTest.finish()) + we will revert the permission back to the original. + + inPermissions is an array of objects where each object has a type, action, context, ex: + [{'type': 'SystemXHR', 'allow': 1, 'context': document}, + {'type': 'SystemXHR', 'allow': Ci.nsIPermissionManager.PROMPT_ACTION, 'context': document}] + + Allow can be a boolean value of true/false or ALLOW_ACTION/DENY_ACTION/PROMPT_ACTION/UNKNOWN_ACTION + */ + pushPermissions: function(inPermissions, callback) { + inPermissions = Cu.waiveXrays(inPermissions); + var pendingPermissions = []; + var cleanupPermissions = []; + + for (var p in inPermissions) { + var permission = inPermissions[p]; + var originalValue = Ci.nsIPermissionManager.UNKNOWN_ACTION; + var context = Cu.unwaiveXrays(permission.context); // Sometimes |context| is a DOM object on which we expect + // to be able to access .nodePrincipal, so we need to unwaive. + if (this.testPermission(permission.type, Ci.nsIPermissionManager.ALLOW_ACTION, context)) { + originalValue = Ci.nsIPermissionManager.ALLOW_ACTION; + } else if (this.testPermission(permission.type, Ci.nsIPermissionManager.DENY_ACTION, context)) { + originalValue = Ci.nsIPermissionManager.DENY_ACTION; + } else if (this.testPermission(permission.type, Ci.nsIPermissionManager.PROMPT_ACTION, context)) { + originalValue = Ci.nsIPermissionManager.PROMPT_ACTION; + } else if (this.testPermission(permission.type, Ci.nsICookiePermission.ACCESS_SESSION, context)) { + originalValue = Ci.nsICookiePermission.ACCESS_SESSION; + } else if (this.testPermission(permission.type, Ci.nsICookiePermission.ACCESS_ALLOW_FIRST_PARTY_ONLY, context)) { + originalValue = Ci.nsICookiePermission.ACCESS_ALLOW_FIRST_PARTY_ONLY; + } else if (this.testPermission(permission.type, Ci.nsICookiePermission.ACCESS_LIMIT_THIRD_PARTY, context)) { + originalValue = Ci.nsICookiePermission.ACCESS_LIMIT_THIRD_PARTY; + } + + let principal = this._getPrincipalFromArg(context); + if (principal.isSystemPrincipal) { + continue; + } + + let perm; + if (typeof permission.allow !== 'boolean') { + perm = permission.allow; + } else { + perm = permission.allow ? Ci.nsIPermissionManager.ALLOW_ACTION + : Ci.nsIPermissionManager.DENY_ACTION; + } + + if (permission.remove == true) + perm = Ci.nsIPermissionManager.UNKNOWN_ACTION; + + if (originalValue == perm) { + continue; + } + + var todo = {'op': 'add', + 'type': permission.type, + 'permission': perm, + 'value': perm, + 'principal': principal, + 'expireType': (typeof permission.expireType === "number") ? + permission.expireType : 0, // default: EXPIRE_NEVER + 'expireTime': (typeof permission.expireTime === "number") ? + permission.expireTime : 0}; + + var cleanupTodo = Object.assign({}, todo); + + if (permission.remove == true) + todo.op = 'remove'; + + pendingPermissions.push(todo); + + /* Push original permissions value or clear into cleanup array */ + if (originalValue == Ci.nsIPermissionManager.UNKNOWN_ACTION) { + cleanupTodo.op = 'remove'; + } else { + cleanupTodo.value = originalValue; + cleanupTodo.permission = originalValue; + } + cleanupPermissions.push(cleanupTodo); + } + + if (pendingPermissions.length > 0) { + // The callback needs to be delayed twice. One delay is because the pref + // service doesn't guarantee the order it calls its observers in, so it + // may notify the observer holding the callback before the other + // observers have been notified and given a chance to make the changes + // that the callback checks for. The second delay is because pref + // observers often defer making their changes by posting an event to the + // event loop. + if (!this._observingPermissions) { + this._observingPermissions = true; + // If specialpowers is in main-process, then we can add a observer + // to get all 'perm-changed' signals. Otherwise, it can't receive + // all signals, so we register a observer in specialpowersobserver(in + // main-process) and get signals from it. + if (this.isMainProcess()) { + this.permissionObserverProxy._specialPowersAPI = this; + Services.obs.addObserver(this.permissionObserverProxy, "perm-changed", false); + } else { + this.registerObservers("perm-changed"); + // bind() is used to set 'this' to SpecialPowersAPI itself. + this._addMessageListener("specialpowers-perm-changed", this.permChangedProxy.bind(this)); + } + } + this._permissionsUndoStack.push(cleanupPermissions); + this._pendingPermissions.push([pendingPermissions, + this._delayCallbackTwice(callback)]); + this._applyPermissions(); + } else { + this._setTimeout(callback); + } + }, + + /* + * This function should be used when specialpowers is in content process but + * it want to get the notification from chrome space. + * + * This function will call Services.obs.addObserver in SpecialPowersObserver + * (that is in chrome process) and forward the data received to SpecialPowers + * via messageManager. + * You can use this._addMessageListener("specialpowers-YOUR_TOPIC") to fire + * the callback. + * + * To get the expected data, you should modify + * SpecialPowersObserver.prototype._registerObservers.observe. Or the message + * you received from messageManager will only contain 'aData' from Service.obs. + * + * NOTICE: there is no implementation of _addMessageListener in + * ChromePowers.js + */ + registerObservers: function(topic) { + var msg = { + 'op': 'add', + 'observerTopic': topic, + }; + this._sendSyncMessage("SPObserverService", msg); + }, + + permChangedProxy: function(aMessage) { + let permission = aMessage.json.permission; + let aData = aMessage.json.aData; + this._permissionObserver.observe(permission, aData); + }, + + permissionObserverProxy: { + // 'this' in permChangedObserverProxy is the permChangedObserverProxy + // object itself. The '_specialPowersAPI' will be set to the 'SpecialPowersAPI' + // object to call the member function in SpecialPowersAPI. + _specialPowersAPI: null, + observe: function (aSubject, aTopic, aData) + { + if (aTopic == "perm-changed") { + var permission = aSubject.QueryInterface(Ci.nsIPermission); + this._specialPowersAPI._permissionObserver.observe(permission, aData); + } + } + }, + + popPermissions: function(callback) { + if (this._permissionsUndoStack.length > 0) { + // See pushPermissions comment regarding delay. + let cb = callback ? this._delayCallbackTwice(callback) : null; + /* Each pop from the stack will yield an object {op/type/permission/value/url/appid/isInIsolatedMozBrowserElement} or null */ + this._pendingPermissions.push([this._permissionsUndoStack.pop(), cb]); + this._applyPermissions(); + } else { + if (this._observingPermissions) { + this._observingPermissions = false; + this._removeMessageListener("specialpowers-perm-changed", this.permChangedProxy.bind(this)); + } + this._setTimeout(callback); + } + }, + + flushPermissions: function(callback) { + while (this._permissionsUndoStack.length > 1) + this.popPermissions(null); + + this.popPermissions(callback); + }, + + + setTestPluginEnabledState: function(newEnabledState, pluginName) { + return this._sendSyncMessage("SPSetTestPluginEnabledState", + { newEnabledState: newEnabledState, pluginName: pluginName })[0]; + }, + + + _permissionObserver: { + _self: null, + _lastPermission: {}, + _callBack: null, + _nextCallback: null, + _obsDataMap: { + 'deleted':'remove', + 'added':'add' + }, + observe: function (permission, aData) + { + if (this._self._applyingPermissions) { + if (permission.type == this._lastPermission.type) { + this._self._setTimeout(this._callback); + this._self._setTimeout(this._nextCallback); + this._callback = null; + this._nextCallback = null; + } + } else { + var found = false; + for (var i = 0; !found && i < this._self._permissionsUndoStack.length; i++) { + var undos = this._self._permissionsUndoStack[i]; + for (var j = 0; j < undos.length; j++) { + var undo = undos[j]; + if (undo.op == this._obsDataMap[aData] && + undo.principal.originAttributes.appId == permission.principal.originAttributes.appId && + undo.type == permission.type) { + // Remove this undo item if it has been done by others(not + // specialpowers itself.) + undos.splice(j,1); + found = true; + break; + } + } + if (!undos.length) { + // Remove the empty row in permissionsUndoStack + this._self._permissionsUndoStack.splice(i, 1); + } + } + } + } + }, + + /* + Iterate through one atomic set of permissions actions and perform allow/deny as appropriate. + All actions performed must modify the relevant permission. + */ + _applyPermissions: function() { + if (this._applyingPermissions || this._pendingPermissions.length <= 0) { + return; + } + + /* Set lock and get prefs from the _pendingPrefs queue */ + this._applyingPermissions = true; + var transaction = this._pendingPermissions.shift(); + var pendingActions = transaction[0]; + var callback = transaction[1]; + var lastPermission = pendingActions[pendingActions.length-1]; + + var self = this; + this._permissionObserver._self = self; + this._permissionObserver._lastPermission = lastPermission; + this._permissionObserver._callback = callback; + this._permissionObserver._nextCallback = function () { + self._applyingPermissions = false; + // Now apply any permissions that may have been queued while we were applying + self._applyPermissions(); + } + + for (var idx in pendingActions) { + var perm = pendingActions[idx]; + this._sendSyncMessage('SPPermissionManager', perm)[0]; + } + }, + + /** + * Helper to resolve a promise by calling the resolve function and call an + * optional callback. + */ + _resolveAndCallOptionalCallback(resolveFn, callback = null) { + resolveFn(); + + if (callback) { + callback(); + } + }, + + /** + * Take in a list of pref changes to make, then invokes |callback| and resolves + * the returned Promise once those changes have taken effect. When the test + * finishes, these changes are reverted. + * + * |inPrefs| must be an object with up to two properties: "set" and "clear". + * pushPrefEnv will set prefs as indicated in |inPrefs.set| and will unset + * the prefs indicated in |inPrefs.clear|. + * + * For example, you might pass |inPrefs| as: + * + * inPrefs = {'set': [['foo.bar', 2], ['magic.pref', 'baz']], + * 'clear': [['clear.this'], ['also.this']] }; + * + * Notice that |set| and |clear| are both an array of arrays. In |set|, each + * of the inner arrays must have the form [pref_name, value] or [pref_name, + * value, iid]. (The latter form is used for prefs with "complex" values.) + * + * In |clear|, each inner array should have the form [pref_name]. + * + * If you set the same pref more than once (or both set and clear a pref), + * the behavior of this method is undefined. + * + * (Implementation note: _prefEnvUndoStack is a stack of values to revert to, + * not values which have been set!) + * + * TODO: complex values for original cleanup? + * + */ + pushPrefEnv: function(inPrefs, callback = null) { + var prefs = Services.prefs; + + var pref_string = []; + pref_string[prefs.PREF_INT] = "INT"; + pref_string[prefs.PREF_BOOL] = "BOOL"; + pref_string[prefs.PREF_STRING] = "CHAR"; + + var pendingActions = []; + var cleanupActions = []; + + for (var action in inPrefs) { /* set|clear */ + for (var idx in inPrefs[action]) { + var aPref = inPrefs[action][idx]; + var prefName = aPref[0]; + var prefValue = null; + var prefIid = null; + var prefType = prefs.PREF_INVALID; + var originalValue = null; + + if (aPref.length == 3) { + prefValue = aPref[1]; + prefIid = aPref[2]; + } else if (aPref.length == 2) { + prefValue = aPref[1]; + } + + /* If pref is not found or invalid it doesn't exist. */ + if (prefs.getPrefType(prefName) != prefs.PREF_INVALID) { + prefType = pref_string[prefs.getPrefType(prefName)]; + if ((prefs.prefHasUserValue(prefName) && action == 'clear') || + (action == 'set')) + originalValue = this._getPref(prefName, prefType); + } else if (action == 'set') { + /* prefName doesn't exist, so 'clear' is pointless */ + if (aPref.length == 3) { + prefType = "COMPLEX"; + } else if (aPref.length == 2) { + if (typeof(prefValue) == "boolean") + prefType = "BOOL"; + else if (typeof(prefValue) == "number") + prefType = "INT"; + else if (typeof(prefValue) == "string") + prefType = "CHAR"; + } + } + + /* PREF_INVALID: A non existing pref which we are clearing or invalid values for a set */ + if (prefType == prefs.PREF_INVALID) + continue; + + /* We are not going to set a pref if the value is the same */ + if (originalValue == prefValue) + continue; + + pendingActions.push({'action': action, 'type': prefType, 'name': prefName, 'value': prefValue, 'Iid': prefIid}); + + /* Push original preference value or clear into cleanup array */ + var cleanupTodo = {'action': action, 'type': prefType, 'name': prefName, 'value': originalValue, 'Iid': prefIid}; + if (originalValue == null) { + cleanupTodo.action = 'clear'; + } else { + cleanupTodo.action = 'set'; + } + cleanupActions.push(cleanupTodo); + } + } + + return new Promise(resolve => { + let done = this._resolveAndCallOptionalCallback.bind(this, resolve, callback); + if (pendingActions.length > 0) { + // The callback needs to be delayed twice. One delay is because the pref + // service doesn't guarantee the order it calls its observers in, so it + // may notify the observer holding the callback before the other + // observers have been notified and given a chance to make the changes + // that the callback checks for. The second delay is because pref + // observers often defer making their changes by posting an event to the + // event loop. + this._prefEnvUndoStack.push(cleanupActions); + this._pendingPrefs.push([pendingActions, + this._delayCallbackTwice(done)]); + this._applyPrefs(); + } else { + this._setTimeout(done); + } + }); + }, + + popPrefEnv: function(callback = null) { + return new Promise(resolve => { + let done = this._resolveAndCallOptionalCallback.bind(this, resolve, callback); + if (this._prefEnvUndoStack.length > 0) { + // See pushPrefEnv comment regarding delay. + let cb = this._delayCallbackTwice(done); + /* Each pop will have a valid block of preferences */ + this._pendingPrefs.push([this._prefEnvUndoStack.pop(), cb]); + this._applyPrefs(); + } else { + this._setTimeout(done); + } + }); + }, + + flushPrefEnv: function(callback = null) { + while (this._prefEnvUndoStack.length > 1) + this.popPrefEnv(null); + + return new Promise(resolve => { + let done = this._resolveAndCallOptionalCallback.bind(this, resolve, callback); + this.popPrefEnv(done); + }); + }, + + /* + Iterate through one atomic set of pref actions and perform sets/clears as appropriate. + All actions performed must modify the relevant pref. + */ + _applyPrefs: function() { + if (this._applyingPrefs || this._pendingPrefs.length <= 0) { + return; + } + + /* Set lock and get prefs from the _pendingPrefs queue */ + this._applyingPrefs = true; + var transaction = this._pendingPrefs.shift(); + var pendingActions = transaction[0]; + var callback = transaction[1]; + + var lastPref = pendingActions[pendingActions.length-1]; + + var pb = Services.prefs; + var self = this; + pb.addObserver(lastPref.name, function prefObs(subject, topic, data) { + pb.removeObserver(lastPref.name, prefObs); + + self._setTimeout(callback); + self._setTimeout(function () { + self._applyingPrefs = false; + // Now apply any prefs that may have been queued while we were applying + self._applyPrefs(); + }); + }, false); + + for (var idx in pendingActions) { + var pref = pendingActions[idx]; + if (pref.action == 'set') { + this._setPref(pref.name, pref.type, pref.value, pref.Iid); + } else if (pref.action == 'clear') { + this.clearUserPref(pref.name); + } + } + }, + + _proxiedObservers: { + "specialpowers-http-notify-request": function(aMessage) { + let uri = aMessage.json.uri; + Services.obs.notifyObservers(null, "specialpowers-http-notify-request", uri); + }, + "specialpowers-browser-fullZoom:zoomReset": function() { + Services.obs.notifyObservers(null, "specialpowers-browser-fullZoom:zoomReset", null); + }, + }, + + _addObserverProxy: function(notification) { + if (notification in this._proxiedObservers) { + this._addMessageListener(notification, this._proxiedObservers[notification]); + } + }, + _removeObserverProxy: function(notification) { + if (notification in this._proxiedObservers) { + this._removeMessageListener(notification, this._proxiedObservers[notification]); + } + }, + + addObserver: function(obs, notification, weak) { + this._addObserverProxy(notification); + obs = Cu.waiveXrays(obs); + if (typeof obs == 'object' && obs.observe.name != 'SpecialPowersCallbackWrapper') + obs.observe = wrapCallback(obs.observe); + Services.obs.addObserver(obs, notification, weak); + }, + removeObserver: function(obs, notification) { + this._removeObserverProxy(notification); + Services.obs.removeObserver(Cu.waiveXrays(obs), notification); + }, + notifyObservers: function(subject, topic, data) { + Services.obs.notifyObservers(subject, topic, data); + }, + + can_QI: function(obj) { + return obj.QueryInterface !== undefined; + }, + do_QueryInterface: function(obj, iface) { + return obj.QueryInterface(Ci[iface]); + }, + + call_Instanceof: function (obj1, obj2) { + obj1=unwrapIfWrapped(obj1); + obj2=unwrapIfWrapped(obj2); + return obj1 instanceof obj2; + }, + + // Returns a privileged getter from an object. GetOwnPropertyDescriptor does + // not work here because xray wrappers don't properly implement it. + // + // This terribleness is used by dom/base/test/test_object.html because + // <object> and <embed> tags will spawn plugins if their prototype is touched, + // so we need to get and cache the getter of |hasRunningPlugin| if we want to + // call it without paradoxically spawning the plugin. + do_lookupGetter: function(obj, name) { + return Object.prototype.__lookupGetter__.call(obj, name); + }, + + // Mimic the get*Pref API + getBoolPref: function(aPrefName) { + return (this._getPref(aPrefName, 'BOOL')); + }, + getIntPref: function(aPrefName) { + return (this._getPref(aPrefName, 'INT')); + }, + getCharPref: function(aPrefName) { + return (this._getPref(aPrefName, 'CHAR')); + }, + getComplexValue: function(aPrefName, aIid) { + return (this._getPref(aPrefName, 'COMPLEX', aIid)); + }, + + // Mimic the set*Pref API + setBoolPref: function(aPrefName, aValue) { + return (this._setPref(aPrefName, 'BOOL', aValue)); + }, + setIntPref: function(aPrefName, aValue) { + return (this._setPref(aPrefName, 'INT', aValue)); + }, + setCharPref: function(aPrefName, aValue) { + return (this._setPref(aPrefName, 'CHAR', aValue)); + }, + setComplexValue: function(aPrefName, aIid, aValue) { + return (this._setPref(aPrefName, 'COMPLEX', aValue, aIid)); + }, + + // Mimic the clearUserPref API + clearUserPref: function(aPrefName) { + var msg = {'op':'clear', 'prefName': aPrefName, 'prefType': ""}; + this._sendSyncMessage('SPPrefService', msg); + }, + + // Private pref functions to communicate to chrome + _getPref: function(aPrefName, aPrefType, aIid) { + var msg = {}; + if (aIid) { + // Overloading prefValue to handle complex prefs + msg = {'op':'get', 'prefName': aPrefName, 'prefType':aPrefType, 'prefValue':[aIid]}; + } else { + msg = {'op':'get', 'prefName': aPrefName,'prefType': aPrefType}; + } + var val = this._sendSyncMessage('SPPrefService', msg); + + if (val == null || val[0] == null) + throw "Error getting pref '" + aPrefName + "'"; + return val[0]; + }, + _setPref: function(aPrefName, aPrefType, aValue, aIid) { + var msg = {}; + if (aIid) { + msg = {'op':'set','prefName':aPrefName, 'prefType': aPrefType, 'prefValue': [aIid,aValue]}; + } else { + msg = {'op':'set', 'prefName': aPrefName, 'prefType': aPrefType, 'prefValue': aValue}; + } + return(this._sendSyncMessage('SPPrefService', msg)[0]); + }, + + _getDocShell: function(window) { + return window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell); + }, + _getMUDV: function(window) { + return this._getDocShell(window).contentViewer; + }, + //XXX: these APIs really ought to be removed, they're not e10s-safe. + // (also they're pretty Firefox-specific) + _getTopChromeWindow: function(window) { + return window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShellTreeItem) + .rootTreeItem + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow) + .QueryInterface(Ci.nsIDOMChromeWindow); + }, + _getAutoCompletePopup: function(window) { + return this._getTopChromeWindow(window).document + .getElementById("PopupAutoComplete"); + }, + addAutoCompletePopupEventListener: function(window, eventname, listener) { + this._getAutoCompletePopup(window).addEventListener(eventname, + listener, + false); + }, + removeAutoCompletePopupEventListener: function(window, eventname, listener) { + this._getAutoCompletePopup(window).removeEventListener(eventname, + listener, + false); + }, + get formHistory() { + let tmp = {}; + Cu.import("resource://gre/modules/FormHistory.jsm", tmp); + return wrapPrivileged(tmp.FormHistory); + }, + getFormFillController: function(window) { + return Components.classes["@mozilla.org/satchel/form-fill-controller;1"] + .getService(Components.interfaces.nsIFormFillController); + }, + attachFormFillControllerTo: function(window) { + this.getFormFillController() + .attachToBrowser(this._getDocShell(window), + this._getAutoCompletePopup(window)); + }, + detachFormFillControllerFrom: function(window) { + this.getFormFillController().detachFromBrowser(this._getDocShell(window)); + }, + isBackButtonEnabled: function(window) { + return !this._getTopChromeWindow(window).document + .getElementById("Browser:Back") + .hasAttribute("disabled"); + }, + //XXX end of problematic APIs + + addChromeEventListener: function(type, listener, capture, allowUntrusted) { + addEventListener(type, listener, capture, allowUntrusted); + }, + removeChromeEventListener: function(type, listener, capture) { + removeEventListener(type, listener, capture); + }, + + // Note: each call to registerConsoleListener MUST be paired with a + // call to postConsoleSentinel; when the callback receives the + // sentinel it will unregister itself (_after_ calling the + // callback). SimpleTest.expectConsoleMessages does this for you. + // If you register more than one console listener, a call to + // postConsoleSentinel will zap all of them. + registerConsoleListener: function(callback) { + let listener = new SPConsoleListener(callback); + Services.console.registerListener(listener); + }, + postConsoleSentinel: function() { + Services.console.logStringMessage("SENTINEL"); + }, + resetConsole: function() { + Services.console.reset(); + }, + + getFullZoom: function(window) { + return this._getMUDV(window).fullZoom; + }, + setFullZoom: function(window, zoom) { + this._getMUDV(window).fullZoom = zoom; + }, + getTextZoom: function(window) { + return this._getMUDV(window).textZoom; + }, + setTextZoom: function(window, zoom) { + this._getMUDV(window).textZoom = zoom; + }, + + getOverrideDPPX: function(window) { + return this._getMUDV(window).overrideDPPX; + }, + setOverrideDPPX: function(window, dppx) { + this._getMUDV(window).overrideDPPX = dppx; + }, + + emulateMedium: function(window, mediaType) { + this._getMUDV(window).emulateMedium(mediaType); + }, + stopEmulatingMedium: function(window) { + this._getMUDV(window).stopEmulatingMedium(); + }, + + snapshotWindowWithOptions: function (win, rect, bgcolor, options) { + var el = this.window.get().document.createElementNS("http://www.w3.org/1999/xhtml", "canvas"); + if (rect === undefined) { + rect = { top: win.scrollY, left: win.scrollX, + width: win.innerWidth, height: win.innerHeight }; + } + if (bgcolor === undefined) { + bgcolor = "rgb(255,255,255)"; + } + if (options === undefined) { + options = { }; + } + + el.width = rect.width; + el.height = rect.height; + var ctx = el.getContext("2d"); + var flags = 0; + + for (var option in options) { + flags |= options[option] && ctx[option]; + } + + ctx.drawWindow(win, + rect.left, rect.top, rect.width, rect.height, + bgcolor, + flags); + return el; + }, + + snapshotWindow: function (win, withCaret, rect, bgcolor) { + return this.snapshotWindowWithOptions(win, rect, bgcolor, + { DRAWWINDOW_DRAW_CARET: withCaret }); + }, + + snapshotRect: function (win, rect, bgcolor) { + return this.snapshotWindowWithOptions(win, rect, bgcolor); + }, + + gc: function() { + this.DOMWindowUtils.garbageCollect(); + }, + + forceGC: function() { + Cu.forceGC(); + }, + + forceCC: function() { + Cu.forceCC(); + }, + + finishCC: function() { + Cu.finishCC(); + }, + + ccSlice: function(budget) { + Cu.ccSlice(budget); + }, + + // Due to various dependencies between JS objects and C++ objects, an ordinary + // forceGC doesn't necessarily clear all unused objects, thus the GC and CC + // needs to run several times and when no other JS is running. + // The current number of iterations has been determined according to massive + // cross platform testing. + exactGC: function(callback) { + let count = 0; + + function genGCCallback(cb) { + return function() { + Cu.forceCC(); + if (++count < 2) { + Cu.schedulePreciseGC(genGCCallback(cb)); + } else if (cb) { + cb(); + } + } + } + + Cu.schedulePreciseGC(genGCCallback(callback)); + }, + + setGCZeal: function(zeal) { + Cu.setGCZeal(zeal); + }, + + isMainProcess: function() { + try { + return Cc["@mozilla.org/xre/app-info;1"]. + getService(Ci.nsIXULRuntime). + processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; + } catch (e) { } + return true; + }, + + _xpcomabi: null, + + get XPCOMABI() { + if (this._xpcomabi != null) + return this._xpcomabi; + + var xulRuntime = Cc["@mozilla.org/xre/app-info;1"] + .getService(Components.interfaces.nsIXULAppInfo) + .QueryInterface(Components.interfaces.nsIXULRuntime); + + this._xpcomabi = xulRuntime.XPCOMABI; + return this._xpcomabi; + }, + + // The optional aWin parameter allows the caller to specify a given window in + // whose scope the runnable should be dispatched. If aFun throws, the + // exception will be reported to aWin. + executeSoon: function(aFun, aWin) { + // Create the runnable in the scope of aWin to avoid running into COWs. + var runnable = {}; + if (aWin) + runnable = Cu.createObjectIn(aWin); + runnable.run = aFun; + Cu.dispatch(runnable, aWin); + }, + + _os: null, + + get OS() { + if (this._os != null) + return this._os; + + var xulRuntime = Cc["@mozilla.org/xre/app-info;1"] + .getService(Components.interfaces.nsIXULAppInfo) + .QueryInterface(Components.interfaces.nsIXULRuntime); + + this._os = xulRuntime.OS; + return this._os; + }, + + get isB2G() { +#ifdef MOZ_B2G + return true; +#else + return false; +#endif + }, + + addSystemEventListener: function(target, type, listener, useCapture) { + Cc["@mozilla.org/eventlistenerservice;1"]. + getService(Ci.nsIEventListenerService). + addSystemEventListener(target, type, listener, useCapture); + }, + removeSystemEventListener: function(target, type, listener, useCapture) { + Cc["@mozilla.org/eventlistenerservice;1"]. + getService(Ci.nsIEventListenerService). + removeSystemEventListener(target, type, listener, useCapture); + }, + + // helper method to check if the event is consumed by either default group's + // event listener or system group's event listener. + defaultPreventedInAnyGroup: function(event) { + // FYI: Event.defaultPrevented returns false in content context if the + // event is consumed only by system group's event listeners. + return event.defaultPrevented; + }, + + getDOMRequestService: function() { + var serv = Services.DOMRequest; + var res = {}; + var props = ["createRequest", "createCursor", "fireError", "fireSuccess", + "fireDone", "fireDetailedError"]; + for (var i in props) { + let prop = props[i]; + res[prop] = function() { return serv[prop].apply(serv, arguments) }; + } + return res; + }, + + setLogFile: function(path) { + this._mfl = new MozillaFileLogger(path); + }, + + log: function(data) { + this._mfl.log(data); + }, + + closeLogFile: function() { + this._mfl.close(); + }, + + addCategoryEntry: function(category, entry, value, persists, replace) { + Components.classes["@mozilla.org/categorymanager;1"]. + getService(Components.interfaces.nsICategoryManager). + addCategoryEntry(category, entry, value, persists, replace); + }, + + deleteCategoryEntry: function(category, entry, persists) { + Components.classes["@mozilla.org/categorymanager;1"]. + getService(Components.interfaces.nsICategoryManager). + deleteCategoryEntry(category, entry, persists); + }, + openDialog: function(win, args) { + return win.openDialog.apply(win, args); + }, + // This is a blocking call which creates and spins a native event loop + spinEventLoop: function(win) { + // simply do a sync XHR back to our windows location. + var syncXHR = new win.XMLHttpRequest(); + syncXHR.open('GET', win.location, false); + syncXHR.send(); + }, + + // :jdm gets credit for this. ex: getPrivilegedProps(window, 'location.href'); + getPrivilegedProps: function(obj, props) { + var parts = props.split('.'); + for (var i = 0; i < parts.length; i++) { + var p = parts[i]; + if (obj[p]) { + obj = obj[p]; + } else { + return null; + } + } + return obj; + }, + + get focusManager() { + if (this._fm != null) + return this._fm; + + this._fm = Components.classes["@mozilla.org/focus-manager;1"]. + getService(Components.interfaces.nsIFocusManager); + + return this._fm; + }, + + getFocusedElementForWindow: function(targetWindow, aDeep) { + var outParam = {}; + this.focusManager.getFocusedElementForWindow(targetWindow, aDeep, outParam); + return outParam.value; + }, + + activeWindow: function() { + return this.focusManager.activeWindow; + }, + + focusedWindow: function() { + return this.focusManager.focusedWindow; + }, + + focus: function(aWindow) { + // This is called inside TestRunner._makeIframe without aWindow, because of assertions in oop mochitests + // With aWindow, it is called in SimpleTest.waitForFocus to allow popup window opener focus switching + if (aWindow) + aWindow.focus(); + var mm = global; + if (aWindow) { + try { + mm = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDocShell) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIContentFrameMessageManager); + } catch (ex) { + /* Ignore exceptions for e.g. XUL chrome windows from mochitest-chrome + * which won't have a message manager */ + } + } + mm.sendAsyncMessage("SpecialPowers.Focus", {}); + }, + + getClipboardData: function(flavor, whichClipboard) { + if (this._cb == null) + this._cb = Components.classes["@mozilla.org/widget/clipboard;1"]. + getService(Components.interfaces.nsIClipboard); + if (whichClipboard === undefined) + whichClipboard = this._cb.kGlobalClipboard; + + var xferable = Components.classes["@mozilla.org/widget/transferable;1"]. + createInstance(Components.interfaces.nsITransferable); + // in e10s b-c tests |content.window| is a CPOW whereas |window| works fine. + // for some non-e10s mochi tests, |window| is null whereas |content.window| + // works fine. So we take whatever is non-null! + xferable.init(this._getDocShell(typeof(window) == "undefined" ? content.window : window) + .QueryInterface(Components.interfaces.nsILoadContext)); + xferable.addDataFlavor(flavor); + this._cb.getData(xferable, whichClipboard); + var data = {}; + try { + xferable.getTransferData(flavor, data, {}); + } catch (e) {} + data = data.value || null; + if (data == null) + return ""; + + return data.QueryInterface(Components.interfaces.nsISupportsString).data; + }, + + clipboardCopyString: function(str) { + Cc["@mozilla.org/widget/clipboardhelper;1"]. + getService(Ci.nsIClipboardHelper). + copyString(str); + }, + + supportsSelectionClipboard: function() { + if (this._cb == null) { + this._cb = Components.classes["@mozilla.org/widget/clipboard;1"]. + getService(Components.interfaces.nsIClipboard); + } + return this._cb.supportsSelectionClipboard(); + }, + + swapFactoryRegistration: function(cid, contractID, newFactory, oldFactory) { + newFactory = Cu.waiveXrays(newFactory); + oldFactory = Cu.waiveXrays(oldFactory); + + var componentRegistrar = Components.manager.QueryInterface(Components.interfaces.nsIComponentRegistrar); + + var unregisterFactory = newFactory; + var registerFactory = oldFactory; + + if (cid == null) { + if (contractID != null) { + cid = componentRegistrar.contractIDToCID(contractID); + oldFactory = Components.manager.getClassObject(Components.classes[contractID], + Components.interfaces.nsIFactory); + } else { + return {'error': "trying to register a new contract ID: Missing contractID"}; + } + + unregisterFactory = oldFactory; + registerFactory = newFactory; + } + componentRegistrar.unregisterFactory(cid, + unregisterFactory); + + // Restore the original factory. + componentRegistrar.registerFactory(cid, + "", + contractID, + registerFactory); + return {'cid':cid, 'originalFactory':oldFactory}; + }, + + _getElement: function(aWindow, id) { + return ((typeof(id) == "string") ? + aWindow.document.getElementById(id) : id); + }, + + dispatchEvent: function(aWindow, target, event) { + var el = this._getElement(aWindow, target); + return el.dispatchEvent(event); + }, + + get isDebugBuild() { + delete SpecialPowersAPI.prototype.isDebugBuild; + + var debug = Cc["@mozilla.org/xpcom/debug;1"].getService(Ci.nsIDebug2); + return SpecialPowersAPI.prototype.isDebugBuild = debug.isDebugBuild; + }, + assertionCount: function() { + var debugsvc = Cc['@mozilla.org/xpcom/debug;1'].getService(Ci.nsIDebug2); + return debugsvc.assertionCount; + }, + + /** + * Get the message manager associated with an <iframe mozbrowser>. + */ + getBrowserFrameMessageManager: function(aFrameElement) { + return this.wrap(aFrameElement.QueryInterface(Ci.nsIFrameLoaderOwner) + .frameLoader + .messageManager); + }, + + _getPrincipalFromArg: function(arg) { + let principal; + let secMan = Services.scriptSecurityManager; + + if (typeof(arg) == "string") { + // It's an URL. + let uri = Services.io.newURI(arg, null, null); + principal = secMan.createCodebasePrincipal(uri, {}); + } else if (arg.manifestURL) { + // It's a thing representing an app. + let appsSvc = Cc["@mozilla.org/AppsService;1"] + .getService(Ci.nsIAppsService) + let app = appsSvc.getAppByManifestURL(arg.manifestURL); + if (!app) { + throw "No app for this manifest!"; + } + + principal = app.principal; + } else if (arg.nodePrincipal) { + // It's a document. + // In some tests the arg is a wrapped DOM element, so we unwrap it first. + principal = unwrapIfWrapped(arg).nodePrincipal; + } else { + let uri = Services.io.newURI(arg.url, null, null); + let attrs = arg.originAttributes || {}; + principal = secMan.createCodebasePrincipal(uri, attrs); + } + + return principal; + }, + + addPermission: function(type, allow, arg, expireType, expireTime) { + let principal = this._getPrincipalFromArg(arg); + if (principal.isSystemPrincipal) { + return; // nothing to do + } + + let permission; + if (typeof allow !== 'boolean') { + permission = allow; + } else { + permission = allow ? Ci.nsIPermissionManager.ALLOW_ACTION + : Ci.nsIPermissionManager.DENY_ACTION; + } + + var msg = { + 'op': 'add', + 'type': type, + 'permission': permission, + 'principal': principal, + 'expireType': (typeof expireType === "number") ? expireType : 0, + 'expireTime': (typeof expireTime === "number") ? expireTime : 0 + }; + + this._sendSyncMessage('SPPermissionManager', msg); + }, + + removePermission: function(type, arg) { + let principal = this._getPrincipalFromArg(arg); + if (principal.isSystemPrincipal) { + return; // nothing to do + } + + var msg = { + 'op': 'remove', + 'type': type, + 'principal': principal + }; + + this._sendSyncMessage('SPPermissionManager', msg); + }, + + hasPermission: function (type, arg) { + let principal = this._getPrincipalFromArg(arg); + if (principal.isSystemPrincipal) { + return true; // system principals have all permissions + } + + var msg = { + 'op': 'has', + 'type': type, + 'principal': principal + }; + + return this._sendSyncMessage('SPPermissionManager', msg)[0]; + }, + + testPermission: function (type, value, arg) { + let principal = this._getPrincipalFromArg(arg); + if (principal.isSystemPrincipal) { + return true; // system principals have all permissions + } + + var msg = { + 'op': 'test', + 'type': type, + 'value': value, + 'principal': principal + }; + return this._sendSyncMessage('SPPermissionManager', msg)[0]; + }, + + isContentWindowPrivate: function(win) { + return PrivateBrowsingUtils.isContentWindowPrivate(win); + }, + + notifyObserversInParentProcess: function(subject, topic, data) { + if (subject) { + throw new Error("Can't send subject to another process!"); + } + if (this.isMainProcess()) { + this.notifyObservers(subject, topic, data); + return; + } + var msg = { + 'op': 'notify', + 'observerTopic': topic, + 'observerData': data + }; + this._sendSyncMessage('SPObserverService', msg); + }, + + removeAllServiceWorkerData: function() { + this.notifyObserversInParentProcess(null, "browser:purge-session-history", ""); + }, + + removeServiceWorkerDataForExampleDomain: function() { + this.notifyObserversInParentProcess(null, "browser:purge-domain-data", "example.com"); + }, + + cleanUpSTSData: function(origin, flags) { + return this._sendSyncMessage('SPCleanUpSTSData', {origin: origin, flags: flags || 0}); + }, + + _nextExtensionID: 0, + _extensionListeners: null, + + loadExtension: function(ext, handler) { + if (this._extensionListeners == null) { + this._extensionListeners = new Set(); + + this._addMessageListener("SPExtensionMessage", msg => { + for (let listener of this._extensionListeners) { + try { + listener(msg); + } catch (e) { + Cu.reportError(e); + } + } + }); + } + + // Note, this is not the addon is as used by the AddonManager etc, + // this is just an identifier used for specialpowers messaging + // between this content process and the chrome process. + let id = this._nextExtensionID++; + + let resolveStartup, resolveUnload, rejectStartup; + let startupPromise = new Promise((resolve, reject) => { + resolveStartup = resolve; + rejectStartup = reject; + }); + let unloadPromise = new Promise(resolve => { resolveUnload = resolve; }); + + startupPromise.catch(() => { + this._extensionListeners.delete(listener); + }); + + handler = Cu.waiveXrays(handler); + ext = Cu.waiveXrays(ext); + + let sp = this; + let state = "uninitialized"; + let extension = { + get state() { return state; }, + + startup() { + state = "pending"; + sp._sendAsyncMessage("SPStartupExtension", {id}); + return startupPromise; + }, + + unload() { + state = "unloading"; + sp._sendAsyncMessage("SPUnloadExtension", {id}); + return unloadPromise; + }, + + sendMessage(...args) { + sp._sendAsyncMessage("SPExtensionMessage", {id, args}); + }, + }; + + this._sendAsyncMessage("SPLoadExtension", {ext, id}); + + let listener = (msg) => { + if (msg.data.id == id) { + if (msg.data.type == "extensionStarted") { + state = "running"; + resolveStartup(); + } else if (msg.data.type == "extensionSetId") { + extension.id = msg.data.args[0]; + } else if (msg.data.type == "extensionFailed") { + state = "failed"; + rejectStartup("startup failed"); + } else if (msg.data.type == "extensionUnloaded") { + this._extensionListeners.delete(listener); + state = "unloaded"; + resolveUnload(); + } else if (msg.data.type in handler) { + handler[msg.data.type](...msg.data.args); + } else { + dump(`Unexpected: ${msg.data.type}\n`); + } + } + }; + + this._extensionListeners.add(listener); + return extension; + }, + + invalidateExtensionStorageCache: function() { + this.notifyObserversInParentProcess(null, "extension-invalidate-storage-cache", ""); + }, + + allowMedia: function(window, enable) { + this._getDocShell(window).allowMedia = enable; + }, + + createChromeCache: function(name, url) { + let principal = this._getPrincipalFromArg(url); + return wrapIfUnwrapped(new content.window.CacheStorage(name, principal)); + }, + + loadChannelAndReturnStatus: function(url, loadUsingSystemPrincipal) { + const BinaryInputStream = + Components.Constructor("@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream"); + + return new Promise(function(resolve) { + let listener = { + httpStatus : 0, + + onStartRequest: function(request, context) { + request.QueryInterface(Ci.nsIHttpChannel); + this.httpStatus = request.responseStatus; + }, + + onDataAvailable: function(request, context, stream, offset, count) { + new BinaryInputStream(stream).readByteArray(count); + }, + + onStopRequest: function(request, context, status) { + /* testing here that the redirect was not followed. If it was followed + we would see a http status of 200 and status of NS_OK */ + + let httpStatus = this.httpStatus; + resolve({status, httpStatus}); + } + }; + let uri = NetUtil.newURI(url); + let channel = NetUtil.newChannel({uri, loadUsingSystemPrincipal}); + + channel.loadFlags |= Ci.nsIChannel.LOAD_DOCUMENT_URI; + channel.QueryInterface(Ci.nsIHttpChannelInternal); + channel.documentURI = uri; + channel.asyncOpen2(listener); + }); + }, + + _pu: null, + + get ParserUtils() { + if (this._pu != null) + return this._pu; + + let pu = Cc["@mozilla.org/parserutils;1"].getService(Ci.nsIParserUtils); + // We need to create and return our own wrapper. + this._pu = { + sanitize: function(src, flags) { + return pu.sanitize(src, flags); + }, + convertToPlainText: function(src, flags, wrapCol) { + return pu.convertToPlainText(src, flags, wrapCol); + }, + parseFragment: function(fragment, flags, isXML, baseURL, element) { + let baseURI = baseURL ? NetUtil.newURI(baseURL) : null; + return pu.parseFragment(unwrapIfWrapped(fragment), + flags, isXML, baseURI, + unwrapIfWrapped(element)); + }, + }; + return this._pu; + }, + + createDOMWalker: function(node, showAnonymousContent) { + node = unwrapIfWrapped(node); + let walker = Cc["@mozilla.org/inspector/deep-tree-walker;1"]. + createInstance(Ci.inIDeepTreeWalker); + walker.showAnonymousContent = showAnonymousContent; + walker.init(node.ownerDocument, Ci.nsIDOMNodeFilter.SHOW_ALL); + walker.currentNode = node; + return { + get firstChild() { + return wrapIfUnwrapped(walker.firstChild()); + }, + get lastChild() { + return wrapIfUnwrapped(walker.lastChild()); + }, + }; + }, + + observeMutationEvents: function(mo, node, nativeAnonymousChildList, subtree) { + unwrapIfWrapped(mo).observe(unwrapIfWrapped(node), + {nativeAnonymousChildList, subtree}); + }, + + doCommand(window, cmd) { + return this._getDocShell(window).doCommand(cmd); + }, + + setCommandNode(window, node) { + return this._getDocShell(window).contentViewer + .QueryInterface(Ci.nsIContentViewerEdit) + .setCommandNode(node); + }, +}; + +this.SpecialPowersAPI = SpecialPowersAPI; +this.bindDOMWindowUtils = bindDOMWindowUtils; +this.getRawComponents = getRawComponents; |