From 5f8de423f190bbb79a62f804151bc24824fa32d8 Mon Sep 17 00:00:00 2001 From: "Matt A. Tobin" Date: Fri, 2 Feb 2018 04:16:08 -0500 Subject: Add m-esr52 at 52.6.0 --- testing/specialpowers/content/MockColorPicker.jsm | 117 ++ testing/specialpowers/content/MockFilePicker.jsm | 235 +++ .../specialpowers/content/MockPermissionPrompt.jsm | 97 + testing/specialpowers/content/MozillaLogger.js | 134 ++ .../content/SpecialPowersObserver.jsm | 313 +++ .../content/SpecialPowersObserverAPI.js | 635 ++++++ testing/specialpowers/content/specialpowers.js | 278 +++ testing/specialpowers/content/specialpowersAPI.js | 2100 ++++++++++++++++++++ 8 files changed, 3909 insertions(+) create mode 100644 testing/specialpowers/content/MockColorPicker.jsm create mode 100644 testing/specialpowers/content/MockFilePicker.jsm create mode 100644 testing/specialpowers/content/MockPermissionPrompt.jsm create mode 100644 testing/specialpowers/content/MozillaLogger.js create mode 100644 testing/specialpowers/content/SpecialPowersObserver.jsm create mode 100644 testing/specialpowers/content/SpecialPowersObserverAPI.js create mode 100644 testing/specialpowers/content/specialpowers.js create mode 100644 testing/specialpowers/content/specialpowersAPI.js (limited to 'testing/specialpowers/content') 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 + || ""; + } 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