summaryrefslogtreecommitdiffstats
path: root/testing/specialpowers
diff options
context:
space:
mode:
Diffstat (limited to 'testing/specialpowers')
-rw-r--r--testing/specialpowers/Makefile.in20
-rw-r--r--testing/specialpowers/bootstrap.js39
-rw-r--r--testing/specialpowers/content/MockColorPicker.jsm117
-rw-r--r--testing/specialpowers/content/MockFilePicker.jsm235
-rw-r--r--testing/specialpowers/content/MockPermissionPrompt.jsm97
-rw-r--r--testing/specialpowers/content/MozillaLogger.js134
-rw-r--r--testing/specialpowers/content/SpecialPowersObserver.jsm313
-rw-r--r--testing/specialpowers/content/SpecialPowersObserverAPI.js635
-rw-r--r--testing/specialpowers/content/specialpowers.js278
-rw-r--r--testing/specialpowers/content/specialpowersAPI.js2100
-rw-r--r--testing/specialpowers/install.rdf28
-rw-r--r--testing/specialpowers/jar.mn11
-rw-r--r--testing/specialpowers/moz.build20
13 files changed, 4027 insertions, 0 deletions
diff --git a/testing/specialpowers/Makefile.in b/testing/specialpowers/Makefile.in
new file mode 100644
index 000000000..c75be3c2d
--- /dev/null
+++ b/testing/specialpowers/Makefile.in
@@ -0,0 +1,20 @@
+#
+# 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/.
+
+TEST_EXTENSIONS_DIR = $(DEPTH)/testing/specialpowers
+XPI_PKGNAME = specialpowers@mozilla.org
+
+include $(topsrcdir)/config/rules.mk
+
+libs-preqs = \
+ $(call mkdir_deps,$(TEST_EXTENSIONS_DIR)) \
+ $(NULL)
+
+libs:: $(libs-preqs)
+ (cd $(DIST)/xpi-stage && tar $(TAR_CREATE_FLAGS) - $(XPI_NAME)) | (cd $(TEST_EXTENSIONS_DIR) && tar -xf -)
+ $(NSINSTALL) -D $(DEPTH)/_tests/testing/mochitest/extensions/specialpowers
+ cp -RL $(DEPTH)/testing/specialpowers/specialpowers $(DEPTH)/_tests/testing/mochitest/extensions
+ $(NSINSTALL) -D $(DEPTH)/_tests/reftest/specialpowers
+ cp -RL $(DEPTH)/testing/specialpowers/specialpowers $(DEPTH)/_tests/reftest
diff --git a/testing/specialpowers/bootstrap.js b/testing/specialpowers/bootstrap.js
new file mode 100644
index 000000000..346fd6ab5
--- /dev/null
+++ b/testing/specialpowers/bootstrap.js
@@ -0,0 +1,39 @@
+/* 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/. */
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+var spObserver;
+
+function startup(data, reason) {
+ let observer = {};
+ Components.utils.import("chrome://specialpowers/content/SpecialPowersObserver.jsm", observer);
+
+ let registrar = Components.manager.QueryInterface(Components.interfaces.nsIComponentRegistrar);
+ registrar.registerFactory(
+ observer.SpecialPowersObserver.prototype.classID,
+ "SpecialPowersObserver",
+ observer.SpecialPowersObserver.prototype.contractID,
+ observer.SpecialPowersObserverFactory
+ );
+
+ spObserver = new observer.SpecialPowersObserver();
+ spObserver.init();
+}
+
+function shutdown(data, reason) {
+ let observer = {};
+ Components.utils.import("chrome://specialpowers/content/SpecialPowersObserver.jsm", observer);
+
+ let registrar = Components.manager.QueryInterface(Components.interfaces.nsIComponentRegistrar);
+ registrar.unregisterFactory(
+ observer.SpecialPowersObserver.prototype.classID,
+ observer.SpecialPowersObserverFactory
+ );
+
+ spObserver.uninit();
+}
+
+function install(data, reason) {}
+function uninstall(data, reason) {}
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;
diff --git a/testing/specialpowers/install.rdf b/testing/specialpowers/install.rdf
new file mode 100644
index 000000000..6bf0c914d
--- /dev/null
+++ b/testing/specialpowers/install.rdf
@@ -0,0 +1,28 @@
+<?xml version="1.0"?>
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+ <Description about="urn:mozilla:install-manifest">
+ <em:id>special-powers@mozilla.org</em:id>
+ <em:version>2015.11.16</em:version>
+ <em:type>2</em:type>
+ <em:bootstrap>true</em:bootstrap>
+
+ <!-- Target Application this extension can install into,
+ with minimum and maximum supported versions. -->
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+#expand <em:minVersion>__MOZILLA_VERSION_U__</em:minVersion>
+ <!-- Set to * so toolkit/mozapps/update/chrome tests pass. -->
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- Front End MetaData -->
+ <em:name>Special Powers</em:name>
+ <em:description>Special powers for use in testing.</em:description>
+ <em:creator>Mozilla</em:creator>
+ </Description>
+</RDF>
diff --git a/testing/specialpowers/jar.mn b/testing/specialpowers/jar.mn
new file mode 100644
index 000000000..5e7ad8716
--- /dev/null
+++ b/testing/specialpowers/jar.mn
@@ -0,0 +1,11 @@
+specialpowers.jar:
+% content specialpowers %content/ contentaccessible=true
+ content/specialpowers.js (content/specialpowers.js)
+* content/specialpowersAPI.js (content/specialpowersAPI.js)
+ content/SpecialPowersObserverAPI.js (content/SpecialPowersObserverAPI.js)
+ content/SpecialPowersObserver.jsm (content/SpecialPowersObserver.jsm)
+ content/MozillaLogger.js (content/MozillaLogger.js)
+ content/MockFilePicker.jsm (content/MockFilePicker.jsm)
+ content/MockColorPicker.jsm (content/MockColorPicker.jsm)
+ content/MockPermissionPrompt.jsm (content/MockPermissionPrompt.jsm)
+ content/Assert.jsm (../modules/Assert.jsm)
diff --git a/testing/specialpowers/moz.build b/testing/specialpowers/moz.build
new file mode 100644
index 000000000..029615ee7
--- /dev/null
+++ b/testing/specialpowers/moz.build
@@ -0,0 +1,20 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+XPI_NAME = 'specialpowers'
+
+JAR_MANIFESTS += ['jar.mn']
+
+USE_EXTENSION_MANIFEST = True
+NO_JS_MANIFEST = True
+
+FINAL_TARGET_PP_FILES += [
+ 'install.rdf',
+]
+
+FINAL_TARGET_FILES += [
+ 'bootstrap.js',
+]