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: }