michael@0: /* Any copyright is dedicated to the Public Domain.
michael@0: * http://creativecommons.org/publicdomain/zero/1.0/
michael@0: */
michael@0:
michael@0: const AM_Cc = Components.classes;
michael@0: const AM_Ci = Components.interfaces;
michael@0:
michael@0: const XULAPPINFO_CONTRACTID = "@mozilla.org/xre/app-info;1";
michael@0: const XULAPPINFO_CID = Components.ID("{c763b610-9d49-455a-bbd2-ede71682a1ac}");
michael@0:
michael@0: const PREF_EM_CHECK_UPDATE_SECURITY = "extensions.checkUpdateSecurity";
michael@0: const PREF_EM_STRICT_COMPATIBILITY = "extensions.strictCompatibility";
michael@0: const PREF_EM_MIN_COMPAT_APP_VERSION = "extensions.minCompatibleAppVersion";
michael@0: const PREF_EM_MIN_COMPAT_PLATFORM_VERSION = "extensions.minCompatiblePlatformVersion";
michael@0: const PREF_GETADDONS_BYIDS = "extensions.getAddons.get.url";
michael@0: const PREF_GETADDONS_BYIDS_PERFORMANCE = "extensions.getAddons.getWithPerformance.url";
michael@0:
michael@0: // Forcibly end the test if it runs longer than 15 minutes
michael@0: const TIMEOUT_MS = 900000;
michael@0:
michael@0: Components.utils.import("resource://gre/modules/addons/AddonRepository.jsm");
michael@0: Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0: Components.utils.import("resource://gre/modules/FileUtils.jsm");
michael@0: Components.utils.import("resource://gre/modules/Services.jsm");
michael@0: Components.utils.import("resource://gre/modules/NetUtil.jsm");
michael@0: Components.utils.import("resource://gre/modules/Promise.jsm");
michael@0: Components.utils.import("resource://gre/modules/Task.jsm");
michael@0: Components.utils.import("resource://gre/modules/osfile.jsm");
michael@0:
michael@0: Services.prefs.setBoolPref("toolkit.osfile.log", true);
michael@0:
michael@0: // We need some internal bits of AddonManager
michael@0: let AMscope = Components.utils.import("resource://gre/modules/AddonManager.jsm");
michael@0: let AddonManager = AMscope.AddonManager;
michael@0: let AddonManagerInternal = AMscope.AddonManagerInternal;
michael@0: // Mock out AddonManager's reference to the AsyncShutdown module so we can shut
michael@0: // down AddonManager from the test
michael@0: let MockAsyncShutdown = {
michael@0: hook: null,
michael@0: profileBeforeChange: {
michael@0: addBlocker: function(aName, aBlocker) {
michael@0: do_print("Mock profileBeforeChange blocker for '" + aName + "'");
michael@0: MockAsyncShutdown.hook = aBlocker;
michael@0: }
michael@0: }
michael@0: };
michael@0: AMscope.AsyncShutdown = MockAsyncShutdown;
michael@0:
michael@0: var gInternalManager = null;
michael@0: var gAppInfo = null;
michael@0: var gAddonsList;
michael@0:
michael@0: var gPort = null;
michael@0: var gUrlToFileMap = {};
michael@0:
michael@0: var TEST_UNPACKED = false;
michael@0:
michael@0: function isNightlyChannel() {
michael@0: var channel = "default";
michael@0: try {
michael@0: channel = Services.prefs.getCharPref("app.update.channel");
michael@0: }
michael@0: catch (e) { }
michael@0:
michael@0: return channel != "aurora" && channel != "beta" && channel != "release" && channel != "esr";
michael@0: }
michael@0:
michael@0: function createAppInfo(id, name, version, platformVersion) {
michael@0: gAppInfo = {
michael@0: // nsIXULAppInfo
michael@0: vendor: "Mozilla",
michael@0: name: name,
michael@0: ID: id,
michael@0: version: version,
michael@0: appBuildID: "2007010101",
michael@0: platformVersion: platformVersion ? platformVersion : "1.0",
michael@0: platformBuildID: "2007010101",
michael@0:
michael@0: // nsIXULRuntime
michael@0: inSafeMode: false,
michael@0: logConsoleErrors: true,
michael@0: OS: "XPCShell",
michael@0: XPCOMABI: "noarch-spidermonkey",
michael@0: invalidateCachesOnRestart: function invalidateCachesOnRestart() {
michael@0: // Do nothing
michael@0: },
michael@0:
michael@0: // nsICrashReporter
michael@0: annotations: {},
michael@0:
michael@0: annotateCrashReport: function(key, data) {
michael@0: this.annotations[key] = data;
michael@0: },
michael@0:
michael@0: QueryInterface: XPCOMUtils.generateQI([AM_Ci.nsIXULAppInfo,
michael@0: AM_Ci.nsIXULRuntime,
michael@0: AM_Ci.nsICrashReporter,
michael@0: AM_Ci.nsISupports])
michael@0: };
michael@0:
michael@0: var XULAppInfoFactory = {
michael@0: createInstance: function (outer, iid) {
michael@0: if (outer != null)
michael@0: throw Components.results.NS_ERROR_NO_AGGREGATION;
michael@0: return gAppInfo.QueryInterface(iid);
michael@0: }
michael@0: };
michael@0: var registrar = Components.manager.QueryInterface(AM_Ci.nsIComponentRegistrar);
michael@0: registrar.registerFactory(XULAPPINFO_CID, "XULAppInfo",
michael@0: XULAPPINFO_CONTRACTID, XULAppInfoFactory);
michael@0: }
michael@0:
michael@0: /**
michael@0: * Tests that an add-on does appear in the crash report annotations, if
michael@0: * crash reporting is enabled. The test will fail if the add-on is not in the
michael@0: * annotation.
michael@0: * @param aId
michael@0: * The ID of the add-on
michael@0: * @param aVersion
michael@0: * The version of the add-on
michael@0: */
michael@0: function do_check_in_crash_annotation(aId, aVersion) {
michael@0: if (!("nsICrashReporter" in AM_Ci))
michael@0: return;
michael@0:
michael@0: if (!("Add-ons" in gAppInfo.annotations)) {
michael@0: do_check_false(true);
michael@0: return;
michael@0: }
michael@0:
michael@0: let addons = gAppInfo.annotations["Add-ons"].split(",");
michael@0: do_check_false(addons.indexOf(encodeURIComponent(aId) + ":" +
michael@0: encodeURIComponent(aVersion)) < 0);
michael@0: }
michael@0:
michael@0: /**
michael@0: * Tests that an add-on does not appear in the crash report annotations, if
michael@0: * crash reporting is enabled. The test will fail if the add-on is in the
michael@0: * annotation.
michael@0: * @param aId
michael@0: * The ID of the add-on
michael@0: * @param aVersion
michael@0: * The version of the add-on
michael@0: */
michael@0: function do_check_not_in_crash_annotation(aId, aVersion) {
michael@0: if (!("nsICrashReporter" in AM_Ci))
michael@0: return;
michael@0:
michael@0: if (!("Add-ons" in gAppInfo.annotations)) {
michael@0: do_check_true(true);
michael@0: return;
michael@0: }
michael@0:
michael@0: let addons = gAppInfo.annotations["Add-ons"].split(",");
michael@0: do_check_true(addons.indexOf(encodeURIComponent(aId) + ":" +
michael@0: encodeURIComponent(aVersion)) < 0);
michael@0: }
michael@0:
michael@0: /**
michael@0: * Returns a testcase xpi
michael@0: *
michael@0: * @param aName
michael@0: * The name of the testcase (without extension)
michael@0: * @return an nsIFile pointing to the testcase xpi
michael@0: */
michael@0: function do_get_addon(aName) {
michael@0: return do_get_file("addons/" + aName + ".xpi");
michael@0: }
michael@0:
michael@0: function do_get_addon_hash(aName, aAlgorithm) {
michael@0: if (!aAlgorithm)
michael@0: aAlgorithm = "sha1";
michael@0:
michael@0: let file = do_get_addon(aName);
michael@0:
michael@0: let crypto = AM_Cc["@mozilla.org/security/hash;1"].
michael@0: createInstance(AM_Ci.nsICryptoHash);
michael@0: crypto.initWithString(aAlgorithm);
michael@0: let fis = AM_Cc["@mozilla.org/network/file-input-stream;1"].
michael@0: createInstance(AM_Ci.nsIFileInputStream);
michael@0: fis.init(file, -1, -1, false);
michael@0: crypto.updateFromStream(fis, file.fileSize);
michael@0:
michael@0: // return the two-digit hexadecimal code for a byte
michael@0: function toHexString(charCode)
michael@0: ("0" + charCode.toString(16)).slice(-2);
michael@0:
michael@0: let binary = crypto.finish(false);
michael@0: return aAlgorithm + ":" + [toHexString(binary.charCodeAt(i)) for (i in binary)].join("")
michael@0: }
michael@0:
michael@0: /**
michael@0: * Returns an extension uri spec
michael@0: *
michael@0: * @param aProfileDir
michael@0: * The extension install directory
michael@0: * @return a uri spec pointing to the root of the extension
michael@0: */
michael@0: function do_get_addon_root_uri(aProfileDir, aId) {
michael@0: let path = aProfileDir.clone();
michael@0: path.append(aId);
michael@0: if (!path.exists()) {
michael@0: path.leafName += ".xpi";
michael@0: return "jar:" + Services.io.newFileURI(path).spec + "!/";
michael@0: }
michael@0: else {
michael@0: return Services.io.newFileURI(path).spec;
michael@0: }
michael@0: }
michael@0:
michael@0: function do_get_expected_addon_name(aId) {
michael@0: if (TEST_UNPACKED)
michael@0: return aId;
michael@0: return aId + ".xpi";
michael@0: }
michael@0:
michael@0: /**
michael@0: * Check that an array of actual add-ons is the same as an array of
michael@0: * expected add-ons.
michael@0: *
michael@0: * @param aActualAddons
michael@0: * The array of actual add-ons to check.
michael@0: * @param aExpectedAddons
michael@0: * The array of expected add-ons to check against.
michael@0: * @param aProperties
michael@0: * An array of properties to check.
michael@0: */
michael@0: function do_check_addons(aActualAddons, aExpectedAddons, aProperties) {
michael@0: do_check_neq(aActualAddons, null);
michael@0: do_check_eq(aActualAddons.length, aExpectedAddons.length);
michael@0: for (let i = 0; i < aActualAddons.length; i++)
michael@0: do_check_addon(aActualAddons[i], aExpectedAddons[i], aProperties);
michael@0: }
michael@0:
michael@0: /**
michael@0: * Check that the actual add-on is the same as the expected add-on.
michael@0: *
michael@0: * @param aActualAddon
michael@0: * The actual add-on to check.
michael@0: * @param aExpectedAddon
michael@0: * The expected add-on to check against.
michael@0: * @param aProperties
michael@0: * An array of properties to check.
michael@0: */
michael@0: function do_check_addon(aActualAddon, aExpectedAddon, aProperties) {
michael@0: do_check_neq(aActualAddon, null);
michael@0:
michael@0: aProperties.forEach(function(aProperty) {
michael@0: let actualValue = aActualAddon[aProperty];
michael@0: let expectedValue = aExpectedAddon[aProperty];
michael@0:
michael@0: // Check that all undefined expected properties are null on actual add-on
michael@0: if (!(aProperty in aExpectedAddon)) {
michael@0: if (actualValue !== undefined && actualValue !== null) {
michael@0: do_throw("Unexpected defined/non-null property for add-on " +
michael@0: aExpectedAddon.id + " (addon[" + aProperty + "] = " +
michael@0: actualValue.toSource() + ")");
michael@0: }
michael@0:
michael@0: return;
michael@0: }
michael@0: else if (expectedValue && !actualValue) {
michael@0: do_throw("Missing property for add-on " + aExpectedAddon.id +
michael@0: ": expected addon[" + aProperty + "] = " + expectedValue);
michael@0: return;
michael@0: }
michael@0:
michael@0: switch (aProperty) {
michael@0: case "creator":
michael@0: do_check_author(actualValue, expectedValue);
michael@0: break;
michael@0:
michael@0: case "developers":
michael@0: case "translators":
michael@0: case "contributors":
michael@0: do_check_eq(actualValue.length, expectedValue.length);
michael@0: for (let i = 0; i < actualValue.length; i++)
michael@0: do_check_author(actualValue[i], expectedValue[i]);
michael@0: break;
michael@0:
michael@0: case "screenshots":
michael@0: do_check_eq(actualValue.length, expectedValue.length);
michael@0: for (let i = 0; i < actualValue.length; i++)
michael@0: do_check_screenshot(actualValue[i], expectedValue[i]);
michael@0: break;
michael@0:
michael@0: case "sourceURI":
michael@0: do_check_eq(actualValue.spec, expectedValue);
michael@0: break;
michael@0:
michael@0: case "updateDate":
michael@0: do_check_eq(actualValue.getTime(), expectedValue.getTime());
michael@0: break;
michael@0:
michael@0: case "compatibilityOverrides":
michael@0: do_check_eq(actualValue.length, expectedValue.length);
michael@0: for (let i = 0; i < actualValue.length; i++)
michael@0: do_check_compatibilityoverride(actualValue[i], expectedValue[i]);
michael@0: break;
michael@0:
michael@0: case "icons":
michael@0: do_check_icons(actualValue, expectedValue);
michael@0: break;
michael@0:
michael@0: default:
michael@0: if (remove_port(actualValue) !== remove_port(expectedValue))
michael@0: do_throw("Failed for " + aProperty + " for add-on " + aExpectedAddon.id +
michael@0: " (" + actualValue + " === " + expectedValue + ")");
michael@0: }
michael@0: });
michael@0: }
michael@0:
michael@0: /**
michael@0: * Check that the actual author is the same as the expected author.
michael@0: *
michael@0: * @param aActual
michael@0: * The actual author to check.
michael@0: * @param aExpected
michael@0: * The expected author to check against.
michael@0: */
michael@0: function do_check_author(aActual, aExpected) {
michael@0: do_check_eq(aActual.toString(), aExpected.name);
michael@0: do_check_eq(aActual.name, aExpected.name);
michael@0: do_check_eq(aActual.url, aExpected.url);
michael@0: }
michael@0:
michael@0: /**
michael@0: * Check that the actual screenshot is the same as the expected screenshot.
michael@0: *
michael@0: * @param aActual
michael@0: * The actual screenshot to check.
michael@0: * @param aExpected
michael@0: * The expected screenshot to check against.
michael@0: */
michael@0: function do_check_screenshot(aActual, aExpected) {
michael@0: do_check_eq(aActual.toString(), aExpected.url);
michael@0: do_check_eq(aActual.url, aExpected.url);
michael@0: do_check_eq(aActual.width, aExpected.width);
michael@0: do_check_eq(aActual.height, aExpected.height);
michael@0: do_check_eq(aActual.thumbnailURL, aExpected.thumbnailURL);
michael@0: do_check_eq(aActual.thumbnailWidth, aExpected.thumbnailWidth);
michael@0: do_check_eq(aActual.thumbnailHeight, aExpected.thumbnailHeight);
michael@0: do_check_eq(aActual.caption, aExpected.caption);
michael@0: }
michael@0:
michael@0: /**
michael@0: * Check that the actual compatibility override is the same as the expected
michael@0: * compatibility override.
michael@0: *
michael@0: * @param aAction
michael@0: * The actual compatibility override to check.
michael@0: * @param aExpected
michael@0: * The expected compatibility override to check against.
michael@0: */
michael@0: function do_check_compatibilityoverride(aActual, aExpected) {
michael@0: do_check_eq(aActual.type, aExpected.type);
michael@0: do_check_eq(aActual.minVersion, aExpected.minVersion);
michael@0: do_check_eq(aActual.maxVersion, aExpected.maxVersion);
michael@0: do_check_eq(aActual.appID, aExpected.appID);
michael@0: do_check_eq(aActual.appMinVersion, aExpected.appMinVersion);
michael@0: do_check_eq(aActual.appMaxVersion, aExpected.appMaxVersion);
michael@0: }
michael@0:
michael@0: function do_check_icons(aActual, aExpected) {
michael@0: for (var size in aExpected) {
michael@0: do_check_eq(remove_port(aActual[size]), remove_port(aExpected[size]));
michael@0: }
michael@0: }
michael@0:
michael@0: // Record the error (if any) from trying to save the XPI
michael@0: // database at shutdown time
michael@0: let gXPISaveError = null;
michael@0:
michael@0: /**
michael@0: * Starts up the add-on manager as if it was started by the application.
michael@0: *
michael@0: * @param aAppChanged
michael@0: * An optional boolean parameter to simulate the case where the
michael@0: * application has changed version since the last run. If not passed it
michael@0: * defaults to true
michael@0: */
michael@0: function startupManager(aAppChanged) {
michael@0: if (gInternalManager)
michael@0: do_throw("Test attempt to startup manager that was already started.");
michael@0:
michael@0: if (aAppChanged || aAppChanged === undefined) {
michael@0: if (gExtensionsINI.exists())
michael@0: gExtensionsINI.remove(true);
michael@0: }
michael@0:
michael@0: gInternalManager = AM_Cc["@mozilla.org/addons/integration;1"].
michael@0: getService(AM_Ci.nsIObserver).
michael@0: QueryInterface(AM_Ci.nsITimerCallback);
michael@0:
michael@0: gInternalManager.observe(null, "addons-startup", null);
michael@0:
michael@0: // Load the add-ons list as it was after extension registration
michael@0: loadAddonsList();
michael@0: }
michael@0:
michael@0: /**
michael@0: * Helper to spin the event loop until a promise resolves or rejects
michael@0: */
michael@0: function loopUntilPromise(aPromise) {
michael@0: let done = false;
michael@0: aPromise.then(
michael@0: () => done = true,
michael@0: err => {
michael@0: do_report_unexpected_exception(err);
michael@0: done = true;
michael@0: });
michael@0:
michael@0: let thr = Services.tm.mainThread;
michael@0:
michael@0: while (!done) {
michael@0: thr.processNextEvent(true);
michael@0: }
michael@0: }
michael@0:
michael@0: /**
michael@0: * Restarts the add-on manager as if the host application was restarted.
michael@0: *
michael@0: * @param aNewVersion
michael@0: * An optional new version to use for the application. Passing this
michael@0: * will change nsIXULAppInfo.version and make the startup appear as if
michael@0: * the application version has changed.
michael@0: */
michael@0: function restartManager(aNewVersion) {
michael@0: loopUntilPromise(promiseRestartManager(aNewVersion));
michael@0: }
michael@0:
michael@0: function promiseRestartManager(aNewVersion) {
michael@0: return promiseShutdownManager()
michael@0: .then(null, err => do_report_unexpected_exception(err))
michael@0: .then(() => {
michael@0: if (aNewVersion) {
michael@0: gAppInfo.version = aNewVersion;
michael@0: startupManager(true);
michael@0: }
michael@0: else {
michael@0: startupManager(false);
michael@0: }
michael@0: });
michael@0: }
michael@0:
michael@0: function shutdownManager() {
michael@0: loopUntilPromise(promiseShutdownManager());
michael@0: }
michael@0:
michael@0: function promiseShutdownManager() {
michael@0: if (!gInternalManager) {
michael@0: return Promise.resolve(false);
michael@0: }
michael@0:
michael@0: let hookErr = null;
michael@0: Services.obs.notifyObservers(null, "quit-application-granted", null);
michael@0: return MockAsyncShutdown.hook()
michael@0: .then(null, err => hookErr = err)
michael@0: .then( () => {
michael@0: gInternalManager = null;
michael@0:
michael@0: // Load the add-ons list as it was after application shutdown
michael@0: loadAddonsList();
michael@0:
michael@0: // Clear any crash report annotations
michael@0: gAppInfo.annotations = {};
michael@0:
michael@0: // Force the XPIProvider provider to reload to better
michael@0: // simulate real-world usage.
michael@0: let XPIscope = Components.utils.import("resource://gre/modules/addons/XPIProvider.jsm");
michael@0: // This would be cleaner if I could get it as the rejection reason from
michael@0: // the AddonManagerInternal.shutdown() promise
michael@0: gXPISaveError = XPIscope.XPIProvider._shutdownError;
michael@0: do_print("gXPISaveError set to: " + gXPISaveError);
michael@0: AddonManagerPrivate.unregisterProvider(XPIscope.XPIProvider);
michael@0: Components.utils.unload("resource://gre/modules/addons/XPIProvider.jsm");
michael@0: if (hookErr) {
michael@0: throw hookErr;
michael@0: }
michael@0: });
michael@0: }
michael@0:
michael@0: function loadAddonsList() {
michael@0: function readDirectories(aSection) {
michael@0: var dirs = [];
michael@0: var keys = parser.getKeys(aSection);
michael@0: while (keys.hasMore()) {
michael@0: let descriptor = parser.getString(aSection, keys.getNext());
michael@0: try {
michael@0: let file = AM_Cc["@mozilla.org/file/local;1"].
michael@0: createInstance(AM_Ci.nsIFile);
michael@0: file.persistentDescriptor = descriptor;
michael@0: dirs.push(file);
michael@0: }
michael@0: catch (e) {
michael@0: // Throws if the directory doesn't exist, we can ignore this since the
michael@0: // platform will too.
michael@0: }
michael@0: }
michael@0: return dirs;
michael@0: }
michael@0:
michael@0: gAddonsList = {
michael@0: extensions: [],
michael@0: themes: []
michael@0: };
michael@0:
michael@0: if (!gExtensionsINI.exists())
michael@0: return;
michael@0:
michael@0: var factory = AM_Cc["@mozilla.org/xpcom/ini-parser-factory;1"].
michael@0: getService(AM_Ci.nsIINIParserFactory);
michael@0: var parser = factory.createINIParser(gExtensionsINI);
michael@0: gAddonsList.extensions = readDirectories("ExtensionDirs");
michael@0: gAddonsList.themes = readDirectories("ThemeDirs");
michael@0: }
michael@0:
michael@0: function isItemInAddonsList(aType, aDir, aId) {
michael@0: var path = aDir.clone();
michael@0: path.append(aId);
michael@0: var xpiPath = aDir.clone();
michael@0: xpiPath.append(aId + ".xpi");
michael@0: for (var i = 0; i < gAddonsList[aType].length; i++) {
michael@0: let file = gAddonsList[aType][i];
michael@0: if (!file.exists())
michael@0: do_throw("Non-existant path found in extensions.ini: " + file.path)
michael@0: if (file.isDirectory() && file.equals(path))
michael@0: return true;
michael@0: if (file.isFile() && file.equals(xpiPath))
michael@0: return true;
michael@0: }
michael@0: return false;
michael@0: }
michael@0:
michael@0: function isThemeInAddonsList(aDir, aId) {
michael@0: return isItemInAddonsList("themes", aDir, aId);
michael@0: }
michael@0:
michael@0: function isExtensionInAddonsList(aDir, aId) {
michael@0: return isItemInAddonsList("extensions", aDir, aId);
michael@0: }
michael@0:
michael@0: function check_startup_changes(aType, aIds) {
michael@0: var ids = aIds.slice(0);
michael@0: ids.sort();
michael@0: var changes = AddonManager.getStartupChanges(aType);
michael@0: changes = changes.filter(function(aEl) /@tests.mozilla.org$/.test(aEl));
michael@0: changes.sort();
michael@0:
michael@0: do_check_eq(JSON.stringify(ids), JSON.stringify(changes));
michael@0: }
michael@0:
michael@0: /**
michael@0: * Escapes any occurances of &, ", < or > with XML entities.
michael@0: *
michael@0: * @param str
michael@0: * The string to escape
michael@0: * @return The escaped string
michael@0: */
michael@0: function escapeXML(aStr) {
michael@0: return aStr.toString()
michael@0: .replace(/&/g, "&")
michael@0: .replace(/"/g, """)
michael@0: .replace(//g, ">");
michael@0: }
michael@0:
michael@0: function writeLocaleStrings(aData) {
michael@0: let rdf = "";
michael@0: ["name", "description", "creator", "homepageURL"].forEach(function(aProp) {
michael@0: if (aProp in aData)
michael@0: rdf += "" + escapeXML(aData[aProp]) + "\n";
michael@0: });
michael@0:
michael@0: ["developer", "translator", "contributor"].forEach(function(aProp) {
michael@0: if (aProp in aData) {
michael@0: aData[aProp].forEach(function(aValue) {
michael@0: rdf += "" + escapeXML(aValue) + "\n";
michael@0: });
michael@0: }
michael@0: });
michael@0: return rdf;
michael@0: }
michael@0:
michael@0: function createInstallRDF(aData) {
michael@0: var rdf = '\n';
michael@0: rdf += '\n';
michael@0: rdf += '\n';
michael@0:
michael@0: ["id", "version", "type", "internalName", "updateURL", "updateKey",
michael@0: "optionsURL", "optionsType", "aboutURL", "iconURL", "icon64URL",
michael@0: "skinnable", "bootstrap", "strictCompatibility"].forEach(function(aProp) {
michael@0: if (aProp in aData)
michael@0: rdf += "" + escapeXML(aData[aProp]) + "\n";
michael@0: });
michael@0:
michael@0: rdf += writeLocaleStrings(aData);
michael@0:
michael@0: if ("targetPlatforms" in aData) {
michael@0: aData.targetPlatforms.forEach(function(aPlatform) {
michael@0: rdf += "" + escapeXML(aPlatform) + "\n";
michael@0: });
michael@0: }
michael@0:
michael@0: if ("targetApplications" in aData) {
michael@0: aData.targetApplications.forEach(function(aApp) {
michael@0: rdf += "\n";
michael@0: ["id", "minVersion", "maxVersion"].forEach(function(aProp) {
michael@0: if (aProp in aApp)
michael@0: rdf += "" + escapeXML(aApp[aProp]) + "\n";
michael@0: });
michael@0: rdf += "\n";
michael@0: });
michael@0: }
michael@0:
michael@0: if ("localized" in aData) {
michael@0: aData.localized.forEach(function(aLocalized) {
michael@0: rdf += "\n";
michael@0: if ("locale" in aLocalized) {
michael@0: aLocalized.locale.forEach(function(aLocaleName) {
michael@0: rdf += "" + escapeXML(aLocaleName) + "\n";
michael@0: });
michael@0: }
michael@0: rdf += writeLocaleStrings(aLocalized);
michael@0: rdf += "\n";
michael@0: });
michael@0: }
michael@0:
michael@0: rdf += "\n\n";
michael@0: return rdf;
michael@0: }
michael@0:
michael@0: /**
michael@0: * Writes an install.rdf manifest into a directory using the properties passed
michael@0: * in a JS object. The objects should contain a property for each property to
michael@0: * appear in the RDFThe object may contain an array of objects with id,
michael@0: * minVersion and maxVersion in the targetApplications property to give target
michael@0: * application compatibility.
michael@0: *
michael@0: * @param aData
michael@0: * The object holding data about the add-on
michael@0: * @param aDir
michael@0: * The directory to add the install.rdf to
michael@0: * @param aExtraFile
michael@0: * An optional dummy file to create in the directory
michael@0: */
michael@0: function writeInstallRDFToDir(aData, aDir, aExtraFile) {
michael@0: var rdf = createInstallRDF(aData);
michael@0: if (!aDir.exists())
michael@0: aDir.create(AM_Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
michael@0: var file = aDir.clone();
michael@0: file.append("install.rdf");
michael@0: if (file.exists())
michael@0: file.remove(true);
michael@0: var fos = AM_Cc["@mozilla.org/network/file-output-stream;1"].
michael@0: createInstance(AM_Ci.nsIFileOutputStream);
michael@0: fos.init(file,
michael@0: FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | FileUtils.MODE_TRUNCATE,
michael@0: FileUtils.PERMS_FILE, 0);
michael@0: fos.write(rdf, rdf.length);
michael@0: fos.close();
michael@0:
michael@0: if (!aExtraFile)
michael@0: return;
michael@0:
michael@0: file = aDir.clone();
michael@0: file.append(aExtraFile);
michael@0: file.create(AM_Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
michael@0: }
michael@0:
michael@0: /**
michael@0: * Writes an install.rdf manifest into an extension using the properties passed
michael@0: * in a JS object. The objects should contain a property for each property to
michael@0: * appear in the RDFThe object may contain an array of objects with id,
michael@0: * minVersion and maxVersion in the targetApplications property to give target
michael@0: * application compatibility.
michael@0: *
michael@0: * @param aData
michael@0: * The object holding data about the add-on
michael@0: * @param aDir
michael@0: * The install directory to add the extension to
michael@0: * @param aId
michael@0: * An optional string to override the default installation aId
michael@0: * @param aExtraFile
michael@0: * An optional dummy file to create in the extension
michael@0: * @return A file pointing to where the extension was installed
michael@0: */
michael@0: function writeInstallRDFForExtension(aData, aDir, aId, aExtraFile) {
michael@0: var id = aId ? aId : aData.id
michael@0:
michael@0: var dir = aDir.clone();
michael@0:
michael@0: if (TEST_UNPACKED) {
michael@0: dir.append(id);
michael@0: writeInstallRDFToDir(aData, dir, aExtraFile);
michael@0: return dir;
michael@0: }
michael@0:
michael@0: if (!dir.exists())
michael@0: dir.create(AM_Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
michael@0: dir.append(id + ".xpi");
michael@0: var rdf = createInstallRDF(aData);
michael@0: var stream = AM_Cc["@mozilla.org/io/string-input-stream;1"].
michael@0: createInstance(AM_Ci.nsIStringInputStream);
michael@0: stream.setData(rdf, -1);
michael@0: var zipW = AM_Cc["@mozilla.org/zipwriter;1"].
michael@0: createInstance(AM_Ci.nsIZipWriter);
michael@0: zipW.open(dir, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | FileUtils.MODE_TRUNCATE);
michael@0: zipW.addEntryStream("install.rdf", 0, AM_Ci.nsIZipWriter.COMPRESSION_NONE,
michael@0: stream, false);
michael@0: if (aExtraFile)
michael@0: zipW.addEntryStream(aExtraFile, 0, AM_Ci.nsIZipWriter.COMPRESSION_NONE,
michael@0: stream, false);
michael@0: zipW.close();
michael@0: return dir;
michael@0: }
michael@0:
michael@0: /**
michael@0: * Sets the last modified time of the extension, usually to trigger an update
michael@0: * of its metadata. If the extension is unpacked, this function assumes that
michael@0: * the extension contains only the install.rdf file.
michael@0: *
michael@0: * @param aExt a file pointing to either the packed extension or its unpacked directory.
michael@0: * @param aTime the time to which we set the lastModifiedTime of the extension
michael@0: *
michael@0: * @deprecated Please use promiseSetExtensionModifiedTime instead
michael@0: */
michael@0: function setExtensionModifiedTime(aExt, aTime) {
michael@0: aExt.lastModifiedTime = aTime;
michael@0: if (aExt.isDirectory()) {
michael@0: let entries = aExt.directoryEntries
michael@0: .QueryInterface(AM_Ci.nsIDirectoryEnumerator);
michael@0: while (entries.hasMoreElements())
michael@0: setExtensionModifiedTime(entries.nextFile, aTime);
michael@0: entries.close();
michael@0: }
michael@0: }
michael@0: function promiseSetExtensionModifiedTime(aPath, aTime) {
michael@0: return Task.spawn(function* () {
michael@0: yield OS.File.setDates(aPath, aTime, aTime);
michael@0: let entries, iterator;
michael@0: try {
michael@0: let iterator = new OS.File.DirectoryIterator(aPath);
michael@0: entries = yield iterator.nextBatch();
michael@0: } catch (ex if ex instanceof OS.File.Error) {
michael@0: return;
michael@0: } finally {
michael@0: if (iterator) {
michael@0: iterator.close();
michael@0: }
michael@0: }
michael@0: for (let entry of entries) {
michael@0: yield promiseSetExtensionModifiedTime(entry.path, aTime);
michael@0: }
michael@0: });
michael@0: }
michael@0:
michael@0: /**
michael@0: * Manually installs an XPI file into an install location by either copying the
michael@0: * XPI there or extracting it depending on whether unpacking is being tested
michael@0: * or not.
michael@0: *
michael@0: * @param aXPIFile
michael@0: * The XPI file to install.
michael@0: * @param aInstallLocation
michael@0: * The install location (an nsIFile) to install into.
michael@0: * @param aID
michael@0: * The ID to install as.
michael@0: */
michael@0: function manuallyInstall(aXPIFile, aInstallLocation, aID) {
michael@0: if (TEST_UNPACKED) {
michael@0: let dir = aInstallLocation.clone();
michael@0: dir.append(aID);
michael@0: dir.create(AM_Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
michael@0: let zip = AM_Cc["@mozilla.org/libjar/zip-reader;1"].
michael@0: createInstance(AM_Ci.nsIZipReader);
michael@0: zip.open(aXPIFile);
michael@0: let entries = zip.findEntries(null);
michael@0: while (entries.hasMore()) {
michael@0: let entry = entries.getNext();
michael@0: let target = dir.clone();
michael@0: entry.split("/").forEach(function(aPart) {
michael@0: target.append(aPart);
michael@0: });
michael@0: zip.extract(entry, target);
michael@0: }
michael@0: zip.close();
michael@0:
michael@0: return dir;
michael@0: }
michael@0: else {
michael@0: let target = aInstallLocation.clone();
michael@0: target.append(aID + ".xpi");
michael@0: aXPIFile.copyTo(target.parent, target.leafName);
michael@0: return target;
michael@0: }
michael@0: }
michael@0:
michael@0: /**
michael@0: * Manually uninstalls an add-on by removing its files from the install
michael@0: * location.
michael@0: *
michael@0: * @param aInstallLocation
michael@0: * The nsIFile of the install location to remove from.
michael@0: * @param aID
michael@0: * The ID of the add-on to remove.
michael@0: */
michael@0: function manuallyUninstall(aInstallLocation, aID) {
michael@0: let file = getFileForAddon(aInstallLocation, aID);
michael@0:
michael@0: // In reality because the app is restarted a flush isn't necessary for XPIs
michael@0: // removed outside the app, but for testing we must flush manually.
michael@0: if (file.isFile())
michael@0: Services.obs.notifyObservers(file, "flush-cache-entry", null);
michael@0:
michael@0: file.remove(true);
michael@0: }
michael@0:
michael@0: /**
michael@0: * Gets the nsIFile for where an add-on is installed. It may point to a file or
michael@0: * a directory depending on whether add-ons are being installed unpacked or not.
michael@0: *
michael@0: * @param aDir
michael@0: * The nsIFile for the install location
michael@0: * @param aId
michael@0: * The ID of the add-on
michael@0: * @return an nsIFile
michael@0: */
michael@0: function getFileForAddon(aDir, aId) {
michael@0: var dir = aDir.clone();
michael@0: dir.append(do_get_expected_addon_name(aId));
michael@0: return dir;
michael@0: }
michael@0:
michael@0: function registerDirectory(aKey, aDir) {
michael@0: var dirProvider = {
michael@0: getFile: function(aProp, aPersistent) {
michael@0: aPersistent.value = true;
michael@0: if (aProp == aKey)
michael@0: return aDir.clone();
michael@0: return null;
michael@0: },
michael@0:
michael@0: QueryInterface: XPCOMUtils.generateQI([AM_Ci.nsIDirectoryServiceProvider,
michael@0: AM_Ci.nsISupports])
michael@0: };
michael@0: Services.dirsvc.registerProvider(dirProvider);
michael@0: }
michael@0:
michael@0: var gExpectedEvents = {};
michael@0: var gExpectedInstalls = [];
michael@0: var gNext = null;
michael@0:
michael@0: function getExpectedEvent(aId) {
michael@0: if (!(aId in gExpectedEvents))
michael@0: do_throw("Wasn't expecting events for " + aId);
michael@0: if (gExpectedEvents[aId].length == 0)
michael@0: do_throw("Too many events for " + aId);
michael@0: let event = gExpectedEvents[aId].shift();
michael@0: if (event instanceof Array)
michael@0: return event;
michael@0: return [event, true];
michael@0: }
michael@0:
michael@0: function getExpectedInstall(aAddon) {
michael@0: if (gExpectedInstalls instanceof Array)
michael@0: return gExpectedInstalls.shift();
michael@0: if (!aAddon || !aAddon.id)
michael@0: return gExpectedInstalls["NO_ID"].shift();
michael@0: let id = aAddon.id;
michael@0: if (!(id in gExpectedInstalls) || !(gExpectedInstalls[id] instanceof Array))
michael@0: do_throw("Wasn't expecting events for " + id);
michael@0: if (gExpectedInstalls[id].length == 0)
michael@0: do_throw("Too many events for " + id);
michael@0: return gExpectedInstalls[id].shift();
michael@0: }
michael@0:
michael@0: const AddonListener = {
michael@0: onPropertyChanged: function(aAddon, aProperties) {
michael@0: let [event, properties] = getExpectedEvent(aAddon.id);
michael@0: do_check_eq("onPropertyChanged", event);
michael@0: do_check_eq(aProperties.length, properties.length);
michael@0: properties.forEach(function(aProperty) {
michael@0: // Only test that the expected properties are listed, having additional
michael@0: // properties listed is not necessary a problem
michael@0: if (aProperties.indexOf(aProperty) == -1)
michael@0: do_throw("Did not see property change for " + aProperty);
michael@0: });
michael@0: return check_test_completed(arguments);
michael@0: },
michael@0:
michael@0: onEnabling: function(aAddon, aRequiresRestart) {
michael@0: let [event, expectedRestart] = getExpectedEvent(aAddon.id);
michael@0: do_check_eq("onEnabling", event);
michael@0: do_check_eq(aRequiresRestart, expectedRestart);
michael@0: if (expectedRestart)
michael@0: do_check_true(hasFlag(aAddon.pendingOperations, AddonManager.PENDING_ENABLE));
michael@0: do_check_false(hasFlag(aAddon.permissions, AddonManager.PERM_CAN_ENABLE));
michael@0: return check_test_completed(arguments);
michael@0: },
michael@0:
michael@0: onEnabled: function(aAddon) {
michael@0: let [event, expectedRestart] = getExpectedEvent(aAddon.id);
michael@0: do_check_eq("onEnabled", event);
michael@0: do_check_false(hasFlag(aAddon.permissions, AddonManager.PERM_CAN_ENABLE));
michael@0: return check_test_completed(arguments);
michael@0: },
michael@0:
michael@0: onDisabling: function(aAddon, aRequiresRestart) {
michael@0: let [event, expectedRestart] = getExpectedEvent(aAddon.id);
michael@0: do_check_eq("onDisabling", event);
michael@0: do_check_eq(aRequiresRestart, expectedRestart);
michael@0: if (expectedRestart)
michael@0: do_check_true(hasFlag(aAddon.pendingOperations, AddonManager.PENDING_DISABLE));
michael@0: do_check_false(hasFlag(aAddon.permissions, AddonManager.PERM_CAN_DISABLE));
michael@0: return check_test_completed(arguments);
michael@0: },
michael@0:
michael@0: onDisabled: function(aAddon) {
michael@0: let [event, expectedRestart] = getExpectedEvent(aAddon.id);
michael@0: do_check_eq("onDisabled", event);
michael@0: do_check_false(hasFlag(aAddon.permissions, AddonManager.PERM_CAN_DISABLE));
michael@0: return check_test_completed(arguments);
michael@0: },
michael@0:
michael@0: onInstalling: function(aAddon, aRequiresRestart) {
michael@0: let [event, expectedRestart] = getExpectedEvent(aAddon.id);
michael@0: do_check_eq("onInstalling", event);
michael@0: do_check_eq(aRequiresRestart, expectedRestart);
michael@0: if (expectedRestart)
michael@0: do_check_true(hasFlag(aAddon.pendingOperations, AddonManager.PENDING_INSTALL));
michael@0: return check_test_completed(arguments);
michael@0: },
michael@0:
michael@0: onInstalled: function(aAddon) {
michael@0: let [event, expectedRestart] = getExpectedEvent(aAddon.id);
michael@0: do_check_eq("onInstalled", event);
michael@0: return check_test_completed(arguments);
michael@0: },
michael@0:
michael@0: onUninstalling: function(aAddon, aRequiresRestart) {
michael@0: let [event, expectedRestart] = getExpectedEvent(aAddon.id);
michael@0: do_check_eq("onUninstalling", event);
michael@0: do_check_eq(aRequiresRestart, expectedRestart);
michael@0: if (expectedRestart)
michael@0: do_check_true(hasFlag(aAddon.pendingOperations, AddonManager.PENDING_UNINSTALL));
michael@0: return check_test_completed(arguments);
michael@0: },
michael@0:
michael@0: onUninstalled: function(aAddon) {
michael@0: let [event, expectedRestart] = getExpectedEvent(aAddon.id);
michael@0: do_check_eq("onUninstalled", event);
michael@0: return check_test_completed(arguments);
michael@0: },
michael@0:
michael@0: onOperationCancelled: function(aAddon) {
michael@0: let [event, expectedRestart] = getExpectedEvent(aAddon.id);
michael@0: do_check_eq("onOperationCancelled", event);
michael@0: return check_test_completed(arguments);
michael@0: }
michael@0: };
michael@0:
michael@0: const InstallListener = {
michael@0: onNewInstall: function(install) {
michael@0: if (install.state != AddonManager.STATE_DOWNLOADED &&
michael@0: install.state != AddonManager.STATE_AVAILABLE)
michael@0: do_throw("Bad install state " + install.state);
michael@0: do_check_eq(install.error, 0);
michael@0: do_check_eq("onNewInstall", getExpectedInstall());
michael@0: return check_test_completed(arguments);
michael@0: },
michael@0:
michael@0: onDownloadStarted: function(install) {
michael@0: do_check_eq(install.state, AddonManager.STATE_DOWNLOADING);
michael@0: do_check_eq(install.error, 0);
michael@0: do_check_eq("onDownloadStarted", getExpectedInstall());
michael@0: return check_test_completed(arguments);
michael@0: },
michael@0:
michael@0: onDownloadEnded: function(install) {
michael@0: do_check_eq(install.state, AddonManager.STATE_DOWNLOADED);
michael@0: do_check_eq(install.error, 0);
michael@0: do_check_eq("onDownloadEnded", getExpectedInstall());
michael@0: return check_test_completed(arguments);
michael@0: },
michael@0:
michael@0: onDownloadFailed: function(install) {
michael@0: do_check_eq(install.state, AddonManager.STATE_DOWNLOAD_FAILED);
michael@0: do_check_eq("onDownloadFailed", getExpectedInstall());
michael@0: return check_test_completed(arguments);
michael@0: },
michael@0:
michael@0: onDownloadCancelled: function(install) {
michael@0: do_check_eq(install.state, AddonManager.STATE_CANCELLED);
michael@0: do_check_eq(install.error, 0);
michael@0: do_check_eq("onDownloadCancelled", getExpectedInstall());
michael@0: return check_test_completed(arguments);
michael@0: },
michael@0:
michael@0: onInstallStarted: function(install) {
michael@0: do_check_eq(install.state, AddonManager.STATE_INSTALLING);
michael@0: do_check_eq(install.error, 0);
michael@0: do_check_eq("onInstallStarted", getExpectedInstall(install.addon));
michael@0: return check_test_completed(arguments);
michael@0: },
michael@0:
michael@0: onInstallEnded: function(install, newAddon) {
michael@0: do_check_eq(install.state, AddonManager.STATE_INSTALLED);
michael@0: do_check_eq(install.error, 0);
michael@0: do_check_eq("onInstallEnded", getExpectedInstall(install.addon));
michael@0: return check_test_completed(arguments);
michael@0: },
michael@0:
michael@0: onInstallFailed: function(install) {
michael@0: do_check_eq(install.state, AddonManager.STATE_INSTALL_FAILED);
michael@0: do_check_eq("onInstallFailed", getExpectedInstall(install.addon));
michael@0: return check_test_completed(arguments);
michael@0: },
michael@0:
michael@0: onInstallCancelled: function(install) {
michael@0: // If the install was cancelled by a listener returning false from
michael@0: // onInstallStarted, then the state will revert to STATE_DOWNLOADED.
michael@0: let possibleStates = [AddonManager.STATE_CANCELLED,
michael@0: AddonManager.STATE_DOWNLOADED];
michael@0: do_check_true(possibleStates.indexOf(install.state) != -1);
michael@0: do_check_eq(install.error, 0);
michael@0: do_check_eq("onInstallCancelled", getExpectedInstall(install.addon));
michael@0: return check_test_completed(arguments);
michael@0: },
michael@0:
michael@0: onExternalInstall: function(aAddon, existingAddon, aRequiresRestart) {
michael@0: do_check_eq("onExternalInstall", getExpectedInstall(aAddon));
michael@0: do_check_false(aRequiresRestart);
michael@0: return check_test_completed(arguments);
michael@0: }
michael@0: };
michael@0:
michael@0: function hasFlag(aBits, aFlag) {
michael@0: return (aBits & aFlag) != 0;
michael@0: }
michael@0:
michael@0: // Just a wrapper around setting the expected events
michael@0: function prepare_test(aExpectedEvents, aExpectedInstalls, aNext) {
michael@0: AddonManager.addAddonListener(AddonListener);
michael@0: AddonManager.addInstallListener(InstallListener);
michael@0:
michael@0: gExpectedInstalls = aExpectedInstalls;
michael@0: gExpectedEvents = aExpectedEvents;
michael@0: gNext = aNext;
michael@0: }
michael@0:
michael@0: // Checks if all expected events have been seen and if so calls the callback
michael@0: function check_test_completed(aArgs) {
michael@0: if (!gNext)
michael@0: return undefined;
michael@0:
michael@0: if (gExpectedInstalls instanceof Array &&
michael@0: gExpectedInstalls.length > 0)
michael@0: return undefined;
michael@0: else for each (let installList in gExpectedInstalls) {
michael@0: if (installList.length > 0)
michael@0: return undefined;
michael@0: }
michael@0:
michael@0: for (let id in gExpectedEvents) {
michael@0: if (gExpectedEvents[id].length > 0)
michael@0: return undefined;
michael@0: }
michael@0:
michael@0: return gNext.apply(null, aArgs);
michael@0: }
michael@0:
michael@0: // Verifies that all the expected events for all add-ons were seen
michael@0: function ensure_test_completed() {
michael@0: for (let i in gExpectedEvents) {
michael@0: if (gExpectedEvents[i].length > 0)
michael@0: do_throw("Didn't see all the expected events for " + i);
michael@0: }
michael@0: gExpectedEvents = {};
michael@0: if (gExpectedInstalls)
michael@0: do_check_eq(gExpectedInstalls.length, 0);
michael@0: }
michael@0:
michael@0: /**
michael@0: * A helper method to install an array of AddonInstall to completion and then
michael@0: * call a provided callback.
michael@0: *
michael@0: * @param aInstalls
michael@0: * The array of AddonInstalls to install
michael@0: * @param aCallback
michael@0: * The callback to call when all installs have finished
michael@0: */
michael@0: function completeAllInstalls(aInstalls, aCallback) {
michael@0: let count = aInstalls.length;
michael@0:
michael@0: if (count == 0) {
michael@0: aCallback();
michael@0: return;
michael@0: }
michael@0:
michael@0: function installCompleted(aInstall) {
michael@0: aInstall.removeListener(listener);
michael@0:
michael@0: if (--count == 0)
michael@0: do_execute_soon(aCallback);
michael@0: }
michael@0:
michael@0: let listener = {
michael@0: onDownloadFailed: installCompleted,
michael@0: onDownloadCancelled: installCompleted,
michael@0: onInstallFailed: installCompleted,
michael@0: onInstallCancelled: installCompleted,
michael@0: onInstallEnded: installCompleted
michael@0: };
michael@0:
michael@0: aInstalls.forEach(function(aInstall) {
michael@0: aInstall.addListener(listener);
michael@0: aInstall.install();
michael@0: });
michael@0: }
michael@0:
michael@0: /**
michael@0: * A helper method to install an array of files and call a callback after the
michael@0: * installs are completed.
michael@0: *
michael@0: * @param aFiles
michael@0: * The array of files to install
michael@0: * @param aCallback
michael@0: * The callback to call when all installs have finished
michael@0: * @param aIgnoreIncompatible
michael@0: * Optional parameter to ignore add-ons that are incompatible in
michael@0: * aome way with the application
michael@0: */
michael@0: function installAllFiles(aFiles, aCallback, aIgnoreIncompatible) {
michael@0: let count = aFiles.length;
michael@0: let installs = [];
michael@0: function callback() {
michael@0: if (aCallback) {
michael@0: aCallback();
michael@0: }
michael@0: }
michael@0: aFiles.forEach(function(aFile) {
michael@0: AddonManager.getInstallForFile(aFile, function(aInstall) {
michael@0: if (!aInstall)
michael@0: do_throw("No AddonInstall created for " + aFile.path);
michael@0: do_check_eq(aInstall.state, AddonManager.STATE_DOWNLOADED);
michael@0:
michael@0: if (!aIgnoreIncompatible || !aInstall.addon.appDisabled)
michael@0: installs.push(aInstall);
michael@0:
michael@0: if (--count == 0)
michael@0: completeAllInstalls(installs, callback);
michael@0: });
michael@0: });
michael@0: }
michael@0:
michael@0: function promiseInstallAllFiles(aFiles, aIgnoreIncompatible) {
michael@0: let deferred = Promise.defer();
michael@0: installAllFiles(aFiles, deferred.resolve, aIgnoreIncompatible);
michael@0: return deferred.promise;
michael@0:
michael@0: }
michael@0:
michael@0: if ("nsIWindowsRegKey" in AM_Ci) {
michael@0: var MockRegistry = {
michael@0: LOCAL_MACHINE: {},
michael@0: CURRENT_USER: {},
michael@0: CLASSES_ROOT: {},
michael@0:
michael@0: getRoot: function(aRoot) {
michael@0: switch (aRoot) {
michael@0: case AM_Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE:
michael@0: return MockRegistry.LOCAL_MACHINE;
michael@0: case AM_Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER:
michael@0: return MockRegistry.CURRENT_USER;
michael@0: case AM_Ci.nsIWindowsRegKey.ROOT_KEY_CLASSES_ROOT:
michael@0: return MockRegistry.CLASSES_ROOT;
michael@0: default:
michael@0: do_throw("Unknown root " + aRootKey);
michael@0: return null;
michael@0: }
michael@0: },
michael@0:
michael@0: setValue: function(aRoot, aPath, aName, aValue) {
michael@0: let rootKey = MockRegistry.getRoot(aRoot);
michael@0:
michael@0: if (!(aPath in rootKey)) {
michael@0: rootKey[aPath] = [];
michael@0: }
michael@0: else {
michael@0: for (let i = 0; i < rootKey[aPath].length; i++) {
michael@0: if (rootKey[aPath][i].name == aName) {
michael@0: if (aValue === null)
michael@0: rootKey[aPath].splice(i, 1);
michael@0: else
michael@0: rootKey[aPath][i].value = aValue;
michael@0: return;
michael@0: }
michael@0: }
michael@0: }
michael@0:
michael@0: if (aValue === null)
michael@0: return;
michael@0:
michael@0: rootKey[aPath].push({
michael@0: name: aName,
michael@0: value: aValue
michael@0: });
michael@0: }
michael@0: };
michael@0:
michael@0: /**
michael@0: * This is a mock nsIWindowsRegistry implementation. It only implements the
michael@0: * methods that the extension manager requires.
michael@0: */
michael@0: function MockWindowsRegKey() {
michael@0: }
michael@0:
michael@0: MockWindowsRegKey.prototype = {
michael@0: values: null,
michael@0:
michael@0: // --- Overridden nsISupports interface functions ---
michael@0: QueryInterface: XPCOMUtils.generateQI([AM_Ci.nsIWindowsRegKey]),
michael@0:
michael@0: // --- Overridden nsIWindowsRegKey interface functions ---
michael@0: open: function(aRootKey, aRelPath, aMode) {
michael@0: let rootKey = MockRegistry.getRoot(aRootKey);
michael@0:
michael@0: if (!(aRelPath in rootKey))
michael@0: rootKey[aRelPath] = [];
michael@0: this.values = rootKey[aRelPath];
michael@0: },
michael@0:
michael@0: close: function() {
michael@0: this.values = null;
michael@0: },
michael@0:
michael@0: get valueCount() {
michael@0: if (!this.values)
michael@0: throw Components.results.NS_ERROR_FAILURE;
michael@0: return this.values.length;
michael@0: },
michael@0:
michael@0: getValueName: function(aIndex) {
michael@0: if (!this.values || aIndex >= this.values.length)
michael@0: throw Components.results.NS_ERROR_FAILURE;
michael@0: return this.values[aIndex].name;
michael@0: },
michael@0:
michael@0: readStringValue: function(aName) {
michael@0: for (let value of this.values) {
michael@0: if (value.name == aName)
michael@0: return value.value;
michael@0: }
michael@0: return null;
michael@0: }
michael@0: };
michael@0:
michael@0: var WinRegFactory = {
michael@0: createInstance: function(aOuter, aIid) {
michael@0: if (aOuter != null)
michael@0: throw Components.results.NS_ERROR_NO_AGGREGATION;
michael@0:
michael@0: var key = new MockWindowsRegKey();
michael@0: return key.QueryInterface(aIid);
michael@0: }
michael@0: };
michael@0:
michael@0: var registrar = Components.manager.QueryInterface(AM_Ci.nsIComponentRegistrar);
michael@0: registrar.registerFactory(Components.ID("{0478de5b-0f38-4edb-851d-4c99f1ed8eba}"),
michael@0: "Mock Windows Registry Implementation",
michael@0: "@mozilla.org/windows-registry-key;1", WinRegFactory);
michael@0: }
michael@0:
michael@0: // Get the profile directory for tests to use.
michael@0: const gProfD = do_get_profile();
michael@0:
michael@0: const EXTENSIONS_DB = "extensions.json";
michael@0: let gExtensionsJSON = gProfD.clone();
michael@0: gExtensionsJSON.append(EXTENSIONS_DB);
michael@0:
michael@0: const EXTENSIONS_INI = "extensions.ini";
michael@0: let gExtensionsINI = gProfD.clone();
michael@0: gExtensionsINI.append(EXTENSIONS_INI);
michael@0:
michael@0: // Enable more extensive EM logging
michael@0: Services.prefs.setBoolPref("extensions.logging.enabled", true);
michael@0:
michael@0: // By default only load extensions from the profile install location
michael@0: Services.prefs.setIntPref("extensions.enabledScopes", AddonManager.SCOPE_PROFILE);
michael@0:
michael@0: // By default don't disable add-ons from any scope
michael@0: Services.prefs.setIntPref("extensions.autoDisableScopes", 0);
michael@0:
michael@0: // By default, don't cache add-ons in AddonRepository.jsm
michael@0: Services.prefs.setBoolPref("extensions.getAddons.cache.enabled", false);
michael@0:
michael@0: // Disable the compatibility updates window by default
michael@0: Services.prefs.setBoolPref("extensions.showMismatchUI", false);
michael@0:
michael@0: // Point update checks to the local machine for fast failures
michael@0: Services.prefs.setCharPref("extensions.update.url", "http://127.0.0.1/updateURL");
michael@0: Services.prefs.setCharPref("extensions.update.background.url", "http://127.0.0.1/updateBackgroundURL");
michael@0: Services.prefs.setCharPref("extensions.blocklist.url", "http://127.0.0.1/blocklistURL");
michael@0:
michael@0: // By default ignore bundled add-ons
michael@0: Services.prefs.setBoolPref("extensions.installDistroAddons", false);
michael@0:
michael@0: // By default use strict compatibility
michael@0: Services.prefs.setBoolPref("extensions.strictCompatibility", true);
michael@0:
michael@0: // By default don't check for hotfixes
michael@0: Services.prefs.setCharPref("extensions.hotfix.id", "");
michael@0:
michael@0: // By default, set min compatible versions to 0
michael@0: Services.prefs.setCharPref(PREF_EM_MIN_COMPAT_APP_VERSION, "0");
michael@0: Services.prefs.setCharPref(PREF_EM_MIN_COMPAT_PLATFORM_VERSION, "0");
michael@0:
michael@0: // Register a temporary directory for the tests.
michael@0: const gTmpD = gProfD.clone();
michael@0: gTmpD.append("temp");
michael@0: gTmpD.create(AM_Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
michael@0: registerDirectory("TmpD", gTmpD);
michael@0:
michael@0: // Write out an empty blocklist.xml file to the profile to ensure nothing
michael@0: // is blocklisted by default
michael@0: var blockFile = gProfD.clone();
michael@0: blockFile.append("blocklist.xml");
michael@0: var stream = AM_Cc["@mozilla.org/network/file-output-stream;1"].
michael@0: createInstance(AM_Ci.nsIFileOutputStream);
michael@0: stream.init(blockFile, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | FileUtils.MODE_TRUNCATE,
michael@0: FileUtils.PERMS_FILE, 0);
michael@0:
michael@0: var data = "\n" +
michael@0: "\n" +
michael@0: "\n";
michael@0: stream.write(data, data.length);
michael@0: stream.close();
michael@0:
michael@0: // Copies blocklistFile (an nsIFile) to gProfD/blocklist.xml.
michael@0: function copyBlocklistToProfile(blocklistFile) {
michael@0: var dest = gProfD.clone();
michael@0: dest.append("blocklist.xml");
michael@0: if (dest.exists())
michael@0: dest.remove(false);
michael@0: blocklistFile.copyTo(gProfD, "blocklist.xml");
michael@0: dest.lastModifiedTime = Date.now();
michael@0: }
michael@0:
michael@0: // Throw a failure and attempt to abandon the test if it looks like it is going
michael@0: // to timeout
michael@0: function timeout() {
michael@0: timer = null;
michael@0: do_throw("Test ran longer than " + TIMEOUT_MS + "ms");
michael@0:
michael@0: // Attempt to bail out of the test
michael@0: do_test_finished();
michael@0: }
michael@0:
michael@0: var timer = AM_Cc["@mozilla.org/timer;1"].createInstance(AM_Ci.nsITimer);
michael@0: timer.init(timeout, TIMEOUT_MS, AM_Ci.nsITimer.TYPE_ONE_SHOT);
michael@0:
michael@0: // Make sure that a given path does not exist
michael@0: function pathShouldntExist(aPath) {
michael@0: if (aPath.exists()) {
michael@0: do_throw("Test cleanup: path " + aPath.path + " exists when it should not");
michael@0: }
michael@0: }
michael@0:
michael@0: do_register_cleanup(function addon_cleanup() {
michael@0: if (timer)
michael@0: timer.cancel();
michael@0:
michael@0: // Check that the temporary directory is empty
michael@0: var dirEntries = gTmpD.directoryEntries
michael@0: .QueryInterface(AM_Ci.nsIDirectoryEnumerator);
michael@0: var entry;
michael@0: while ((entry = dirEntries.nextFile)) {
michael@0: do_throw("Found unexpected file in temporary directory: " + entry.leafName);
michael@0: }
michael@0: dirEntries.close();
michael@0:
michael@0: var testDir = gProfD.clone();
michael@0: testDir.append("extensions");
michael@0: testDir.append("trash");
michael@0: pathShouldntExist(testDir);
michael@0:
michael@0: testDir.leafName = "staged";
michael@0: pathShouldntExist(testDir);
michael@0:
michael@0: testDir.leafName = "staged-xpis";
michael@0: pathShouldntExist(testDir);
michael@0:
michael@0: shutdownManager();
michael@0:
michael@0: // Clear commonly set prefs.
michael@0: try {
michael@0: Services.prefs.clearUserPref(PREF_EM_CHECK_UPDATE_SECURITY);
michael@0: } catch (e) {}
michael@0: try {
michael@0: Services.prefs.clearUserPref(PREF_EM_STRICT_COMPATIBILITY);
michael@0: } catch (e) {}
michael@0: });
michael@0:
michael@0: /**
michael@0: * Handler function that responds with the interpolated
michael@0: * static file associated to the URL specified by request.path.
michael@0: * This replaces the %PORT% entries in the file with the actual
michael@0: * value of the running server's port (stored in gPort).
michael@0: */
michael@0: function interpolateAndServeFile(request, response) {
michael@0: try {
michael@0: let file = gUrlToFileMap[request.path];
michael@0: var data = "";
michael@0: var fstream = Components.classes["@mozilla.org/network/file-input-stream;1"].
michael@0: createInstance(Components.interfaces.nsIFileInputStream);
michael@0: var cstream = Components.classes["@mozilla.org/intl/converter-input-stream;1"].
michael@0: createInstance(Components.interfaces.nsIConverterInputStream);
michael@0: fstream.init(file, -1, 0, 0);
michael@0: cstream.init(fstream, "UTF-8", 0, 0);
michael@0:
michael@0: let (str = {}) {
michael@0: let read = 0;
michael@0: do {
michael@0: // read as much as we can and put it in str.value
michael@0: read = cstream.readString(0xffffffff, str);
michael@0: data += str.value;
michael@0: } while (read != 0);
michael@0: }
michael@0: data = data.replace(/%PORT%/g, gPort);
michael@0:
michael@0: response.write(data);
michael@0: } catch (e) {
michael@0: do_throw("Exception while serving interpolated file.");
michael@0: } finally {
michael@0: cstream.close(); // this closes fstream as well
michael@0: }
michael@0: }
michael@0:
michael@0: /**
michael@0: * Sets up a path handler for the given URL and saves the
michael@0: * corresponding file in the global url -> file map.
michael@0: *
michael@0: * @param url
michael@0: * the actual URL
michael@0: * @param file
michael@0: * nsILocalFile representing a static file
michael@0: */
michael@0: function mapUrlToFile(url, file, server) {
michael@0: server.registerPathHandler(url, interpolateAndServeFile);
michael@0: gUrlToFileMap[url] = file;
michael@0: }
michael@0:
michael@0: function mapFile(path, server) {
michael@0: mapUrlToFile(path, do_get_file(path), server);
michael@0: }
michael@0:
michael@0: /**
michael@0: * Take out the port number in an URL
michael@0: *
michael@0: * @param url
michael@0: * String that represents an URL with a port number in it
michael@0: */
michael@0: function remove_port(url) {
michael@0: if (typeof url === "string")
michael@0: return url.replace(/:\d+/, "");
michael@0: return url;
michael@0: }
michael@0: // Wrap a function (typically a callback) to catch and report exceptions
michael@0: function do_exception_wrap(func) {
michael@0: return function() {
michael@0: try {
michael@0: func.apply(null, arguments);
michael@0: }
michael@0: catch(e) {
michael@0: do_report_unexpected_exception(e);
michael@0: }
michael@0: };
michael@0: }
michael@0:
michael@0: /**
michael@0: * Change the schema version of the JSON extensions database
michael@0: */
michael@0: function changeXPIDBVersion(aNewVersion) {
michael@0: let jData = loadJSON(gExtensionsJSON);
michael@0: jData.schemaVersion = aNewVersion;
michael@0: saveJSON(jData, gExtensionsJSON);
michael@0: }
michael@0:
michael@0: /**
michael@0: * Load a file into a string
michael@0: */
michael@0: function loadFile(aFile) {
michael@0: let data = "";
michael@0: let fstream = Components.classes["@mozilla.org/network/file-input-stream;1"].
michael@0: createInstance(Components.interfaces.nsIFileInputStream);
michael@0: let cstream = Components.classes["@mozilla.org/intl/converter-input-stream;1"].
michael@0: createInstance(Components.interfaces.nsIConverterInputStream);
michael@0: fstream.init(aFile, -1, 0, 0);
michael@0: cstream.init(fstream, "UTF-8", 0, 0);
michael@0: let (str = {}) {
michael@0: let read = 0;
michael@0: do {
michael@0: read = cstream.readString(0xffffffff, str); // read as much as we can and put it in str.value
michael@0: data += str.value;
michael@0: } while (read != 0);
michael@0: }
michael@0: cstream.close();
michael@0: return data;
michael@0: }
michael@0:
michael@0: /**
michael@0: * Raw load of a JSON file
michael@0: */
michael@0: function loadJSON(aFile) {
michael@0: let data = loadFile(aFile);
michael@0: do_print("Loaded JSON file " + aFile.path);
michael@0: return(JSON.parse(data));
michael@0: }
michael@0:
michael@0: /**
michael@0: * Raw save of a JSON blob to file
michael@0: */
michael@0: function saveJSON(aData, aFile) {
michael@0: do_print("Starting to save JSON file " + aFile.path);
michael@0: let stream = FileUtils.openSafeFileOutputStream(aFile);
michael@0: let converter = AM_Cc["@mozilla.org/intl/converter-output-stream;1"].
michael@0: createInstance(AM_Ci.nsIConverterOutputStream);
michael@0: converter.init(stream, "UTF-8", 0, 0x0000);
michael@0: // XXX pretty print the JSON while debugging
michael@0: converter.writeString(JSON.stringify(aData, null, 2));
michael@0: converter.flush();
michael@0: // nsConverterOutputStream doesn't finish() safe output streams on close()
michael@0: FileUtils.closeSafeFileOutputStream(stream);
michael@0: converter.close();
michael@0: do_print("Done saving JSON file " + aFile.path);
michael@0: }
michael@0:
michael@0: /**
michael@0: * Create a callback function that calls do_execute_soon on an actual callback and arguments
michael@0: */
michael@0: function callback_soon(aFunction) {
michael@0: return function(...args) {
michael@0: do_execute_soon(function() {
michael@0: aFunction.apply(null, args);
michael@0: }, aFunction.name ? "delayed callback " + aFunction.name : "delayed callback");
michael@0: }
michael@0: }
michael@0:
michael@0: /**
michael@0: * A promise-based variant of AddonManager.getAddonsByIDs.
michael@0: *
michael@0: * @param {array} list As the first argument of AddonManager.getAddonsByIDs
michael@0: * @return {promise}
michael@0: * @resolve {array} The list of add-ons sent by AddonManaget.getAddonsByIDs to
michael@0: * its callback.
michael@0: */
michael@0: function promiseAddonsByIDs(list) {
michael@0: let deferred = Promise.defer();
michael@0: AddonManager.getAddonsByIDs(list, deferred.resolve);
michael@0: return deferred.promise;
michael@0: }